Введение
Всем привет, эта статья лежит у меня в черновиках уже год. Я все откладывал её на потом, когда будет побольше времени, но, в связи с скорым выходом yii2, я решил доработать её и выложить на обозрение читателя.
Вот уже как 3 года я работаю над одним очень крупным проектом в megaflowers. И, в какой-то момент разработки, когда классов стало слишком много, а их названия стали вида ContentDiscount
, ItemDiscount
, я понял, что надо с этим что-то делать, и решил ввести неймспейсы в наш проект. Ну, как говорится, гулять так гулять если вводить, то везде сразу, а не там чуть-чуть и там, а в остальных местах нет.
Итак, давайте рассмотрим как «готовить» основные типы классов в приложении.
Основы
Так как я решил использовать везде неймспейсы, то я выбрал корневым неймспейсом app
(ну уж слишком длинный application
). Однако yii его не понимает, поэтому пришлось определить его в конфиге(можно и в index.php), но, так как конфиг подключался по пути к нему, и, в момент инициализации не смог использовать Yii::setPathOfAlias
(может сейчас ситуация изменилась?), то пришлось видоизменить 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
. Вышло не совсем красиво, так как пришлось дублировать уйму кода, но, мне все равно надо было этот метод перекрыть для решения внутренних задач проекта.
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; } } }
// 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
.
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
. Исправим это, определив базовый класс для всех модулей.
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
, но и тут меня ждало разочарование.
// нужен абсолютный путь, иначе альяс будет ссылаться не туда Yii::setPathOfAlias('app',dirname(__FILE__).DIRECTORY_SEPARATOR.'..'.DIRECTORY_SEPARATOR); //... 'commandMap'=>array( 'import'=>'\app\commands\ImportCommand', ),
Код валился ругаясь на на то что не может найти класс ImportCom
.
Методом проб и ошибок все же было найдено рабочее решение.
// нужен абсолютный путь, иначе альяс будет ссылаться не туда Yii::setPathOfAlias('app',dirname(__FILE__).DIRECTORY_SEPARATOR.'..'.DIRECTORY_SEPARATOR); //... 'commandMap'=>array( // наша команда 'import'=>array( 'class'=>'\app\commands\ImportCommand', ) ),
Из минусов этого способа можно отметить необходимость указания в конфиге абсолютного имени для всех команд.
На сегодняшний день это единственное решение проблемы, других мне обнаружить не удалось.
Модели
Вот мы и добрались до моделей. Казалось бы тут все просто должно быть, ведь модели и так можно использовать в неймспейсах, но, когда я увидел как стал выглядеть метод relations, я решил это исправить. Сперва я определял в каждой модели константу с именем класса: const CLASS_NAME=__CLASS_NAME__;
.
Потом решил поступить проще, определив базовую модель(решение подсмотрено в yii2).
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. Для этого я определил базовый класс для всех виджетов.
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);
На этом все, если у вас будут какие-то замечания иди предложения по статье — пишите, поправлю.
ссылка на оригинал статьи http://habrahabr.ru/post/209526/
Добавить комментарий