Введение
Всем привет, эта статья лежит у меня в черновиках уже год. Я все откладывал её на потом, когда будет побольше времени, но, в связи с скорым выходом 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 и в неймспейсе apptest
. 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 baseBaseModule */
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 appcomponentsWebApplication($config);
$app->run();
Модули
Контролеры у нас уже есть, можно писать дальше, но что делать если мы хотим положить их в модули? Модуль определяется конфигом для Yii::createComponent
, то есть его можно использовать, указав вручную имя класса.
array(
'modules'=>array(
'front'=>array(
'class'=>'frontFrontModule'
)
)
)
Такой способ не сработает, так как 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'=>'appcommandsImportCommand',
),
Код валился ругаясь на на то что не может найти класс ImportCom
.
Методом проб и ошибок все же было найдено рабочее решение.
// нужен абсолютный путь, иначе альяс будет ссылаться не туда
Yii::setPathOfAlias('app',dirname(__FILE__).DIRECTORY_SEPARATOR.'..'.DIRECTORY_SEPARATOR);
//...
'commandMap'=>array(
// наша команда
'import'=>array(
'class'=>'appcommandsImportCommand',
)
),
Из минусов этого способа можно отметить необходимость указания в конфиге абсолютного имени для всех команд.
На сегодняшний день это единственное решение проблемы, других мне обнаружить не удалось.
Модели
Вот мы и добрались до моделей. Казалось бы тут все просто должно быть, ведь модели и так можно использовать в неймспейсах, но, когда я увидел как стал выглядеть метод 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'=>'applocationCountry',
)
}
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 MyWidgetNSMyWidget::begin($options);
echo MyWidgetNSMyWidget::end();
//...
echo MyWidget2NSMyWidget2::runWidget($options);
На этом все, если у вас будут какие-то замечания иди предложения по статье — пишите, поправлю.
Автор: Ekstazi