Yii
January 2014 18

Все о неймспейсах в yii1

Tutorial

Введение


Всем привет, эта статья лежит у меня в черновиках уже год. Я все откладывал её на потом, когда будет побольше времени, но, в связи с скорым выходом yii2, я решил доработать её и выложить на обозрение читателя.

Вот уже как 3 года я работаю над одним очень крупным проектом в megaflowers. И, в какой-то момент разработки, когда классов стало слишком много, а их названия стали вида ContentDiscount, ItemDiscount, я понял, что надо с этим что-то делать, и решил ввести неймспейсы в наш проект. Ну, как говорится, гулять так гулять если вводить, то везде сразу, а не там чуть-чуть и там, а в остальных местах нет.

Итак, давайте рассмотрим как «готовить» основные типы классов в приложении.

Основы


Так как я решил использовать везде неймспейсы, то я выбрал корневым неймспейсом app (ну уж слишком длинный application). Однако yii его не понимает, поэтому пришлось определить его в конфиге(можно и в index.php), но, так как конфиг подключался по пути к нему, и, в момент инициализации не смог использовать Yii::setPathOfAlias (может сейчас ситуация изменилась?), то пришлось видоизменить index.php.

Вот так стал выглядеть мой index.php
$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();


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



Контроллеры


Казалось бы тут все должно быть просто — указал controllerNamespace в конфиге, например наш альяс app, и все работает хорошо. А вот и нет! До поры, до времени и валится все это в случае когда контроллер лежит в какой-то папке, например, test и в неймспейсе app\test. Yii ищет его в папке test, но в неймспейсе app. Так как работать надо, а времени писать баг-репорт и делать пул-реквест не было (но вы можете это сделать), то я решил написать своё решение. Для этого я унаследовался от CWepApplication и переопределил метод createController. Вышло не совсем красиво, так как пришлось дублировать уйму кода, но, мне все равно надо было этот метод перекрыть для решения внутренних задач проекта.

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;
			}
		}
	}



А вот так стал выглядеть наш index.php:
// 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();



Модули


Контролеры у нас уже есть, можно писать дальше, но что делать если мы хотим положить их в модули? Модуль определяется конфигом для Yii::createComponent, то есть его можно использовать, указав вручную имя класса.

конфиг
array(
	'modules'=>array(
		'front'=>array(
			'class'=>'front\FrontModule'
		)
	)
)



Такой способ не сработает, так как yii ничего не знает про альяс front. Можно по тому же принципу, что и для альяса app, прописать его в конфиге, но мне такой способ не очень понравился в виду избыточности писанины кода (хотелось писать только имена модулей), поэтому я поступил проще и изменил своего потомка 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);
	}
}



Решение не идеально, да и баг-репорт бы составить (почему, указывая имя класса модуля, yii не может его найти? приходится писать его вида app.modules.ModuleClass). Сейчас же я думаю, все это делать поменять и поменьше трогать CWebApplication, например, вынести в конфиг в папке с модулем установку альяса и подключать его к основному конфигу.

С модулями мы разобрались, но, как только дело дойдет до подмодулей, то мы столкнемся с той же проблемой. Да и, для корректной работы контроллеров в модуле, нужно вручную для каждого модуля указать controllerNamespace. Исправим это, определив базовый класс для всех модулей.

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);
	}
}



Часть кода можно вынести в трейты, но, я оставлю это вам.

Консольные команды


С первого раза у меня не вышло запустить «неймспейсную» команду, ничего похожего на commandNampespace я не обнаружил ни в `CConsoleApplication`, ни в `CConsoleCommandRunner` (может стоит запрос о фиче написать?). Стал копать в сторону commandMap, но и тут меня ждало разочарование.

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



Код валился ругаясь на на то что не может найти класс ImportCom.

Методом проб и ошибок все же было найдено рабочее решение.

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



Из минусов этого способа можно отметить необходимость указания в конфиге абсолютного имени для всех команд.

На сегодняшний день это единственное решение проблемы, других мне обнаружить не удалось.

Модели


Вот мы и добрались до моделей. Казалось бы тут все просто должно быть, ведь модели и так можно использовать в неймспейсах, но, когда я увидел как стал выглядеть метод relations, я решил это исправить. Сперва я определял в каждой модели константу с именем класса: const CLASS_NAME=__CLASS_NAME__;.

Потом решил поступить проще, определив базовую модель(решение подсмотрено в yii2).

Базовая модель NamespaceRecord
class NamespaceRecord extends CActiveRecord
{
	public static function className()
	{
		return get_called_class();
	}
}



После этих действиях наши модели стали проще и «красивее».

Было:
	public function relations(){
		return array(
			'country'=>'app\location\Country',
		)
	}



Стало:
	public function relations(){
		return array(
			'country'=>Country::className(),
		)
	}



Были еще проблемы с формами, но, в yii уже исправили это.

Виджеты


Долгое время я писал в своих вьюхах $this->widget('Мой длиный неймспейс виджета\имя класса'), однако, с выходом yii2, я сделал свои виджеты более похожими на yii2. Для этого я определил базовый класс для всех виджетов.

Базовый виджет NSWidget
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);
	}
}




Все, теперь мы можем писать во вьюхах
echo MyWidgetNS\MyWidget::begin($options);
echo MyWidgetNS\MyWidget::end();
//...
echo MyWidget2NS\MyWidget2::runWidget($options);



На этом все, если у вас будут какие-то замечания иди предложения по статье — пишите, поправлю.
UPD. Сегодня пофиксили баг с контроллерами в подпапке. Больше не надо переопределять CWebApplication. Для альясов так же можно (уже нужно, так как с приложением пофиксили) использовать опцию в конфиге:
конфиг
'aliases'=>array(
'app'=>'application'
),


Через эту же опцию можно выкинуть «костыли» для модулей
+14
12.4k 73
Comments 17
Top of the day