All about namespaces in yii1

  • Tutorial

Introduction


Hello everyone, this article has been in my drafts for a year now. I kept putting it off for later, when there would be more time, but, in connection with the imminent release of yii2, I decided to finalize it and put it on the reader's review.

For 3 years now I have been working on one very large project in megaflowers. And, at some point in development when classes become too much, and their names have become kind ContentDiscount, ItemDiscountI realized that I need this to do something, and decided to introduce namespaces into our project. Well, as they say, if you enter for a walk like that, then everywhere at once, and not just a little there, but not in other places.

So, let's look at how to “cook” the main types of classes in an application.

The basics


Since I decided to use the namespace everywhere, I chose the root namespace app(well, too long application). However, yii does not understand it, so I had to define it in the config (you can also use index.php), but since the config was connected along the path to it and couldn’t use it at the time of initialization Yii::setPathOfAlias(maybe the situation has changed now?), modify index.php.

This is how my index.php began to look
$yii=dirname(__FILE__).'/yii/framework/yii.php';
$config=dirname(__FILE__).'/protected/config/main.php';
// remove the following lines when in production mode
defined('YII_DEBUG') or define('YII_DEBUG',true);
// specify how many levels of call stack should be shown in each log message
defined('YII_TRACE_LEVEL') or define('YII_TRACE_LEVEL',3);
// Вначале подключаем Yii, чтоб можно было воспользоваться автолоадером
require_once($yii);
// Затем, подключаем конфиг, иначе мы не сможем установить альяс
$config=require($config);
Yii::createWebApplication($config)->run();


And accordingly a config
// Из-за глюка в yii, мы не можем использовать Yii::getPathOfAlias
Yii::setPathOfAlias('app', dirname(__FILE__) . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR);
// а так же констануту для удобства
define(NS_SEPARATOR,NAMESPACE_SEPARATOR);
// сам конфиг
return array(
// ....
);



Controllers


It would seem that everything should be simple here - controllerNamespace indicated in the config, for example, our alias app, and everything works well. And no! For the time being, for a while all this falls in the case when the controller is in some kind of folder, for example, test and in the namespace app\test. Yii looks for it in the test folder, but in the namespace app. Since it is necessary to work, and there was no time to write a bug report and do a pool request (but you can do it), I decided to write my decision. To do this, I inherited from CWepApplicationand redefined the method createController. It didn’t turn out quite beautifully, as I had to duplicate a lot of code, but I still had to block this method to solve the internal problems of the project.

Wepapplication
	class WebApplication extends CWebApplication {
		// неймспейс для контроллеров чтоб не писать в конфиге
		public $controllerNamespace='app';
		public function createController($route,$owner=null)
		{
			if($owner===null)
				$owner=$this;
			if(($route=trim($route,'/'))==='')
				$route=$owner->defaultController;
			$caseSensitive=$this->getUrlManager()->caseSensitive;
			$route.='/';
			while(($pos=strpos($route,'/'))!==false)
			{
				$id=substr($route,0,$pos);
				if(!preg_match('/^\w+$/',$id))
					return null;
				if(!$caseSensitive)
					$id=strtolower($id);
				$route=(string)substr($route,$pos+1);
				if(!isset($basePath))  // first segment
				{
					if(isset($owner->controllerMap[$id]))
					{
						return array(
							\Yii::createComponent($owner->controllerMap[$id],$id,$owner===$this?null:$owner),
							$this->parseActionParams($route),
						);
					}
					/** @var $module \base\BaseModule */
					if(($module=$owner->getModule($id))!==null){
						return $this->createController($route,$module);
					}
					$basePath=$owner->getControllerPath();
					$controllerID='';
				}
				else
					$controllerID.='/';
				$className=ucfirst($id).'Controller';
				$classFile=$basePath.DIRECTORY_SEPARATOR.$className.'.php';
				// только здесь логика меняется
				if($owner->controllerNamespace!==null)
					$className=$owner->controllerNamespace.NS_SEPARATOR.str_replace('/',NS_SEPARATOR,$controllerID).$className;
				if(is_file($classFile))
				{
					if(!class_exists($className,false))
						require($classFile);
					if(class_exists($className,false) && is_subclass_of($className,'CController'))
					{
						$id[0]=strtolower($id[0]);
						return array(
							new $className($controllerID.$id,$owner===$this?null:$owner),
							$this->parseActionParams($route),
						);
					}
					return null;
				}
				$controllerID.=$id;
				$basePath.=DIRECTORY_SEPARATOR.$id;
			}
		}
	}


And this is how our index.php began to look:
// change the following paths if necessary
$yii=dirname(__FILE__).'/yii/framework/yii.php';
$config=dirname(__FILE__).'/protected/config/main.php';
// remove the following lines when in production mode
defined('YII_DEBUG') or define('YII_DEBUG',true);
// specify how many levels of call stack should be shown in each log message
defined('YII_TRACE_LEVEL') or define('YII_TRACE_LEVEL',3);
// Вначале подключаем Yii, чтоб можно было воспользоваться автолоадером
require_once($yii);
// Затем, подключаем конфиг
$config=require($config);
// И, запускаем приложение
$app=new app\components\WebApplication($config);
$app->run();



Modules


We already have controllers, we can write further, but what if we want to put them in modules? The module is determined by the config for Yii::createComponent, that is, it can be used by manually specifying the class name.

config
array(
	'modules'=>array(
		'front'=>array(
			'class'=>'front\FrontModule'
		)
	)
)


This method will not work, since yii does not know anything about alias front. You can appwrite it in the config by the same principle as for alias , but I didn’t really like this method because of the redundancy of the code (I wanted to write only the names of the modules), so I did it easier and changed my descendant CWebApplication.

Webapplication
class WebApplication extends CWebApplication {
// ....
	/**
	 * Принудительно ставит неймспейс для модулей с дефолтным описанием(кратким, без массива)
	 * @param array $modules
	 */
	public function setModules($modules)
	{
		$modulesConfig=array();
		foreach($modules as $id=>$module){
			if(is_int($id))
			{
				$id=$module;
				$module=array();
			}
			if(!isset($module['class']))
			{
				// ставим альяс
				\Yii::setPathOfAlias($id,$this->getModulePath().DIRECTORY_SEPARATOR.$id);
				$module['class']=NS_SEPARATOR.$id.NS_SEPARATOR.ucfirst($id).'Module';
			}
			$modulesConfig[$id]=$module;
		}
		parent::setModules($modulesConfig);
	}
}


The solution is not perfect, and a bug report would be compiled (why, specifying the class name of the module, yii cannot find it? I have to write it in the form app.modules.ModuleClass). Now, I think, change all this and change it a little CWebApplication, for example, put the installation of the alias in the config in the folder with the module and connect it to the main config.

We figured out the modules, but as soon as it comes to submodules, we will face the same problem. Yes, and for the correct operation of the controllers in the module, you must manually specify for each module controllerNamespace. We fix this by defining a base class for all modules.

Basemodule
class BaseModule extends \CWebModule
{
	/**
	 * Фикс для неймспейсов + импорт
	 */
	protected function init()
   	{
		parent::init();
		// устанавливаем неймспейсы контроллерам, чтоб не прописывать в конфиге
		$namespace=implode(NS_SEPARATOR, array_slice(explode(NS_SEPARATOR,get_class($this)),0,-1));
		$this->controllerNamespace=$namespace.NS_SEPARATOR.'controllers';
   	}
	/**
	 * Принудительно ставит неймспейс для модулей с дефолтным описанием(кратки, без массива)
	 * @param array $modules
	 */
	public function setModules($modules)
	{
		$modulesConfig=array();
		foreach($modules as $id=>$module){
			if(is_int($id))
			{
				$id=$module;
				$module=array();
			}
			if(!isset($module['class']))
			{
				\Yii::setPathOfAlias($id,$this->getModulePath().DIRECTORY_SEPARATOR.$id);
				$module['class']=NS_SEPARATOR.$id.NS_SEPARATOR.ucfirst($id).'Module';
			}
			$modulesConfig[$id]=$module;
		}
		parent::setModules($modulesConfig);
	}
}


Part of the code can be put into traits, but I will leave it to you.

Console commands


The first time I couldn’t run the “namespace” command, commandNampespaceI didn’t find anything like it in either `CConsoleApplication` or` CConsoleCommandRunner` (maybe you should write a feature request?). I began to dig to the side commandMap, but even here disappointment awaited me.

config console.php
// нужен абсолютный путь, иначе альяс будет ссылаться не туда
Yii::setPathOfAlias('app',dirname(__FILE__).DIRECTORY_SEPARATOR.'..'.DIRECTORY_SEPARATOR);
//...
	'commandMap'=>array(
		'import'=>'\app\commands\ImportCommand',
	),


The code fell swearing at the fact that it could not find the class ImportCom.

By trial and error, a working solution was nevertheless found.

console.php
:
// нужен абсолютный путь, иначе альяс будет ссылаться не туда
Yii::setPathOfAlias('app',dirname(__FILE__).DIRECTORY_SEPARATOR.'..'.DIRECTORY_SEPARATOR);
//...
	'commandMap'=>array(
		// наша команда
		'import'=>array(
			'class'=>'\app\commands\ImportCommand',
		)
	),


Of the minuses of this method, the need to specify the absolute name for all teams in the config should be noted.

Today this is the only solution to the problem, I could not find others.

Models


So we got to the models. It would seem that everything should just be here, because the models can be used in namespace anyway, but when I saw how the relations method began to look, I decided to fix it. At first I was determined in each model a constant with the class name: const CLASS_NAME=__CLASS_NAME__;.

Then I decided to do it easier by defining the basic model (the solution is spied on in yii2).

Base Model NamespaceRecord
class NamespaceRecord extends CActiveRecord
{
	public static function className()
	{
		return get_called_class();
	}
}



After these actions, our models have become simpler and “more beautiful”.

It was:
	public function relations(){
		return array(
			'country'=>'app\location\Country',
		)
	}


It became:
	public function relations(){
		return array(
			'country'=>Country::className(),
		)
	}


There were still problems with the forms, but yii already fixed this.

Widgets


For a long time I wrote in my views $this->widget('Мой длиный неймспейс виджета\имя класса'), however, with the release of yii2, I made my widgets look more like yii2. To do this, I defined a base class for all widgets.

NSWidget Base Widget
class NSWidget extends \CWidget{
	/**
	 * @param array $options
	 * @return \CWidget
	 */
	public static function begin($options=array())
	{
		return \Yii::app()->controller->beginWidget(get_called_class(),$options);
	}
	/**
	 * @return \CWidget
	 */
	public static function end()
	{
		return \Yii::app()->controller->endWidget();
	}
	/**
	 * @param array $options
	 * @return string widget content
	 */
	public static function runWidget($options=array())
	{
		return \Yii::app()->controller->widget(get_called_class(),$options,true);
	}
}



Everything, now we can write in views
echo MyWidgetNS\MyWidget::begin($options);
echo MyWidgetNS\MyWidget::end();
//...
echo MyWidget2NS\MyWidget2::runWidget($options);


That's all, if you have any comments or suggestions for the article - write, I will correct it.
UPD Today we fixed a bug with controllers in a subfolder. No more redefining CWebApplication. For aliases, it is also possible (already necessary, since the application is fixed) to use the option in the config:
config
'aliases'=>array(
'app'=>'application'
),


Through the same option, you can throw out “crutches” for the modules

Also popular now: