Перенос конфигурации в модули Yii
При переделывании своего проекта на модульную структуру сразу же захотелось сделать модули минимально связанными. При этом многим приходится сталкиваться с организацией выноса настроек модулей из главного конфигурационного файла в сами модули. Рассмотрим несколько приёмов и подводных камней.
Действительно, при усложнении проекта и переходе к модульной структуре главный конфигурационный файл превращается в что-то похожее на это, то есть списки modules
, import
и rules
вырастают до монструозных размеров:
'modules' => array( 'user', 'page', 'news', // ... 'social', ),
'import' => array( 'application.components.*', 'application.modules.user.components', 'application.modules.user.models.*', 'application.modules.user.forms.*', 'application.modules.user.widgets.*', 'application.modules.page.components.*', 'application.modules.page.models.*', 'application.modules.page.widgets.*', 'application.modules.news.components.*', 'application.modules.news.models.*', 'application.modules.news.widgets.*', // ... 'application.modules.social.components.*', 'application.modules.social.models.*', 'application.modules.social.widgets.*', ),
'rules' => array( '/' => 'site/index', '/login' => 'user/account/login', '/logout' => 'user/account/logout', '/registration' => 'user/account/registration', '/recovery' => 'user/account/recovery', '/feedback' => 'feedback/feedback', '/pages/<slug>' => 'page/page/show', '/story/<title>' => 'news/news/show/', // ... 'user/<username:\w+>/' => 'user/people/userInfo', '<module:\w+>/<controller:\w+>/<action:\w+>/<id:\d+>' => '<module>/<controller>/<action>', '<module:\w+>/<controller:\w+>/<action:\w+>' => '<module>/<controller>/<action>', '<module:\w+>/<controller:\w+>' => '<module>/<controller>/index', '<controller:\w+>/<action:\w+>' => '<controller>/<action>', '<controller:\w+>' => '<controller>/index', ),
'params' => array( 'adminEmail'=>'admin@site.com', 'blogPostsPerPage'=>10, 'newsNewsPerPage'=>10, // ... 'social'=>array( 'facebook'=>'...', 'twitter'=>'...', ), ),
Как мы видим, при добавлении/удалении модулей нам каждый раз придётся вручную добавлять/удалять строки в эти списки. А наша система, естественно, должна быть гибкой и до предела автоматизированной.
Итак, нашей задачей будет удаление всего этого безобразия из конфигурационных файлов во славу великой автоматизации.
Отказ от раздела «params»
Вписывать настройки сайта в файл немного негуманно по отношению к заказчику и к себе. Намного удобнее хранить настройки в базе данных и изменять их в панели управления. Это не очень сложно исправить.
А в файле пусть останется заглушка
'params' => array(),
Может она в экстренном случае пригодится...
Упрощение раздела «import»
Как бы привычно ни было импортировать сразу все сотни моделей и компонентов, но при работе с модульным проектом всё же лучше перебороть себя и вместо импортирования всей кучи использовать метод Yii::import()
для подключения только нужных компонентов и моделей по требованию. Это воспитывает внимательность и здорово помогает переходу к пространствам имён. Конечно, общие компоненты и модели можно оставить импортированными заранее. Оставим только самое необходимое:
'import' => array( 'application.components.*', // общие компоненты 'application.modules.main.components.*', // компоненты нашего главного модуля 'application.modules.user.models.*', // чтобы модель User была доступной отовсюду ),
Для доступа к конкретным классам в конфигурационном файле тоже нужно будет указывать полные пути в виде module.components.Сlass
.
'components' => array( 'authManager'=>array( 'class'=>'user.components.PhpAuthManager', 'defaultRoles'=>array('guest'), ), )
Импортировать свои же модели в контроллерах модуля не нужно. Для этого достаточно производить импорт при инициализации модуля:
class BlogModule extends CWebModule { public function init() { parent::init(); // импортируется при запуске любого контроллера этого модуля $this->setImport(array( 'blog.components.*', 'blog.models.*', )); } }
Все контроллеры модуля уже будут иметь доступ к своим моделям и компонентам. Если контроллер использует модели или компоненты из другого модуля, то их нужно будет импортировать в этом контроллере вручную.
Будьте внимательны при работе с виджетами. Их вызов не запускает инициализацию модуля, поэтому в виджетах нужно импортировать модели всегда:
// импортируем модель поста Yii::import('blog.models.Post'); class LastPostsWidget extends Widget { public $limit = 10; public function run() { $posts = Post::model()->findAll(array('order'=>'date DESC', 'limit'->$this->limit); $this->render('LastPosts', array('posts'=>$posts); } }
И, как было сказано выше, нужно указывать полный путь к классу виджета:
<?php $this->widget('blog.widgets.LastPostsWidget', array('limit'=>5));
Динамическое подключение модулей в «modules»
Предположим, что в этом разделе у нас простой список имён модулей без опций, то есть раздел такой:
'modules' => array( 'user', 'page', 'news', // ... 'social', ),
что, фактически, равнозначно определению
'modules' => array( 'user'=>array('class'=>'application.modules.user.UserModule'), 'page'=>array('class'=>'application.modules.page.PageModule'), 'news'=>array('class'=>'application.modules.news.NewsModule'), // ... 'social'=>array('class'=>'application.modules.social.SocialModule'), ),
Имена модулей в Yii совпадают с именами папок, поэтому можно воспользоваться простым перечислением директорий в папке protected/modules
:
// получаем список директорий в protected/modules $dirs = scandir(dirname(__FILE__).'/../modules'); // строим массив $modules = array(); foreach ($dirs as $name){ if ($name[0] != '.') $modules[$name] = array('class'=>'application.modules.' . $name . '.' . ucfirst($name) . 'Module'); } // строка вида 'news|page|user|...|socials' // пригодится для подстановки в регулярные выражения общих правил маршрутизации define('MODULES_MATCHES', implode('|', array_keys($modules))); return array( // ... // только прожиточный минимум 'import'=>array( 'application.components.*', 'application.modules.main.components.*', 'application.modules.user.models.*', ), // сливаем наш массив $modules 'modules'=>array_replace($modules, array( // если какой-либо модуль нуждается в переопределении для этого проекта, то пропишите его здесь 'user' => array( 'class' => 'application.modules.user.UserModule', 'documentRoot' => $_SERVER['DOCUMENT_ROOT'], // ... ), /* ну и добавим gii если нужен 'gii'=>array( 'class'=>'system.gii.GiiModule', 'password'=>'admin', 'ipFilters'=>array('127.0.0.1','::1'), ), */ )), 'components'=>array( // ... 'urlManager'=>array( 'urlFormat'=>'path', 'showScriptName'=>false, 'urlSuffix'=>'', 'rules'=>array( // небольшая защита от дублирования адресов '<module:' . MODULES_MATCHES . '>/default/index'=>'main/error/error', '<module:' . MODULES_MATCHES . '>/default'=>'main/error/error', // остальные правила '/' => 'blog/default/index', '<action:login|logout|register>' => 'user/default/<action>', 'blog/<slug:[\w+_-]+>' => 'blog/post/view', 'user/<username:\w+>/' => 'user/people/userInfo', // правила по умолчанию '<module:\w+>/<controller:\w+>/<action:\w+>/<id:\d+>' => '<module>/<controller>/<action>', '<module:\w+>/<controller:\w+>/<action:\w+>' => '<module>/<controller>/<action>', '<module:\w+>/<controller:\w+>' => '<module>/<controller>/index', '<controller:\w+>/<action:\w+>' => '<controller>/<action>', '<controller:\w+>' => '<controller>/index', ), ), // ... ), );
Теперь нам достаточно удалить папку модуля из проекта или добавить новый модуль, и он автоматически подключится. Также остаётся возможность переопределить настройки любого модуля для кастомизации под каждый проект.
В нашем конфигурационном файле остались неразделёнными только правила маршрутизации для urlManager.
Выносим правила «urlManager» в модули
В наших примерах все правила роутинга скинуты в общую кучу.
Мы можем поступить с правилами как и со списком модулей, то есть обойти модули, собрать из них массив правил в переменную $rules
и склеить со стандартными правилами:
'rules' => array_merge($rules, array( '<module:\w+>/<controller:\w+>/<action:\w+>/<id:\d+>' => '<module>/<controller>/<action>', '<module:\w+>/<controller:\w+>/<action:\w+>' => '<module>/<controller>/<action>', '<module:\w+>/<controller:\w+>' => '<module>/<controller>/index', '<controller:\w+>/<action:\w+>' => '<controller>/<action>', '<controller:\w+>' => '<controller>/index', )),
Для этого достаточно, чтобы модуль имел метод getUrlRules
:
class BlogModule extends CWebModule { public function init() { parent::init(); $this->setImport(array( 'blog.models.*', )); } public function getUrlRules() { return array( 'blog'=>'blog/default/index', 'blog/feed'=>'blog/feed/index', 'blog/search'=>'blog/default/search', 'blog/tag/<tag:[\w-]+>'=>'blog/default/tag', 'blog/date/<date:[\w-]+>'=>'blog/default/date', 'blog/<id:[\d]+>'=>'blog/post/view', 'blog/category/<category:.+>'=>'blog/default/category', ); } }
В системе Yupe на момент написания статьи у каждого модуля имеются файлы параметров, и цикл в конфигурационном файле склеивает параметры всех модулей в единые массивы.
Недостаток этого подхода в том, что каждый раз импортируются правила всех модулей. Представьте, что у вас 50 модулей с 2-10 правилами в каждом. При каждом вызове метода createUrl()
менеджер будет обходить сотни регулярных выражений!
Попробуем найти способ так не делать.
Динамическая подгрузка правил маршрутизации
Удалим все правила, относящиеся к модулям, из конфигурационного файла, оставив только общие правила для контроллеров админки (вида PostAdminController
):
return array( // ... 'components'=>array( // ... 'urlManager'=>array( 'urlFormat'=>'path', 'showScriptName'=>false, 'urlSuffix'=>'', 'rules'=>array( // небольшая защита от дублирования адресов '<module:' . MODULES_MATCHES . '>/default/index'=>'main/error/error404', '<module:' . MODULES_MATCHES . '>/default'=>'main/error/error404', // правила для экшенов админки '<module:' . MODULES_MATCHES . '>/<controller:\w+[Aa]dmin>/<action:\w+>/<id:\d+>'=>'<module>/<controller>/<action>', '<module:' . MODULES_MATCHES . '>/<controller:\w+[Aa]dmin>'=>'<module>/<controller>/index', '<module:' . MODULES_MATCHES . '>/<controller:\w+[Aa]dmin>/<action:\w+>'=>'<module>/<controller>/<action>', ), ), // ... ), );
и поместим правила в классах модулей:
class BlogModule extends CWebModule { public function init() { parent::init(); $this->setImport(array( 'blog.models.*', )); } public function getUrlRules() { return array( 'blog'=>'blog/default/index', 'blog/feed'=>'blog/feed/index', 'blog/search'=>'blog/default/search', 'blog/tag/<tag:[\w-]+>'=>'blog/default/tag', 'blog/date/<date:[\w-]+>'=>'blog/default/date', 'blog/<id:[\d]+>'=>'blog/post/view', 'blog/category/<category:.+>'=>'blog/default/category', ); } }
Известная возможность менеджера Yii динамически добавлять правила вызовом CUrlManager::addRules
позволит разрешить проблему подключения нужных правил маршрутизации.
По ссылке приведён пример подключения правил для текущего модуля. Вот немного видоизменённый вариант навешивания на событие onBeginRequest
:
return array( // ... 'onBeginRequest' => function($event){ $route=Yii::app()->getRequest()->getPathInfo(); $domains = explode('/', $route); $moduleName = array_shift($domains); if(Yii::app()->hasModule($moduleName)) { $module=Yii::app()->getModule($moduleName); if(isset($module->urlRules)) { $urlManager=Yii::app()->getUrlManager(); $urlManager->addRules($module->urlRules); } } return true; }, );;
Функция срабатывает перед разбором текущего адреса, что гарантирует тот факт, что нужные правила подгрузятся вовремя. Если, например, текущий адрес http://site.com/blog/view
или просто http://site.com/blog
, то подгрузятся только правила модуля «blog».
Лучше не занимать весь onBeginRequest
всего одной функцией, а переместить её код в поведение:
class DModuleUrlRulesBehavior extends CBehavior { public function events() { return array_merge(parent::events(),array( 'onBeginRequest'=>'beginRequest', )); } public function beginRequest($event) { $moduleName = $this->_getModuleName(); if(Yii::app()->hasModule($moduleName)) { $module = Yii::app()->getModule($moduleName); if(isset($module->urlRules)) { $urlManager = Yii::app()->getUrlManager(); $urlManager->addRules($module->urlRules); } } } protected function _getModuleName() { $route = Yii::app()->getRequest()->getPathInfo(); $domains = explode('/', $route); $moduleName = array_shift($domains); return $moduleName; } }
и вместо onBeginRequest
подключить его в главном конфигурационном файле:
return array( // ... 'behaviors'=> array( array( 'class'=>'DModuleUrlRulesBehavior', ) ), );
Заметим, что в строке
$module = Yii::app()->getModule($moduleName);
происходит полное создание модуля с вызовом метода init()
. Это значит, что модуль создастся и импортирует свои модели или сделает ещё что-то в методе init()
без нашего ведома. Для текущего модуля это не критично, но если в ходе выполнения скрипта мы будем обращаться к правилам другим модулей, то это будет лишним. Поэтому для избегания полной инициализации каждого модуля метод с правилами лучше сделать статическим:
class BlogModule extends CWebModule { public function init() { parent::init(); $this->setImport(array( 'blog.models.*', )); } public static function rules() { return array( 'blog'=>'blog/default/index', 'blog/feed'=>'blog/feed/index', 'blog/search'=>'blog/default/search', 'blog/tag/<tag:[\w-]+>'=>'blog/default/tag', 'blog/date/<date:[\w-]+>'=>'blog/default/date', 'blog/<id:[\d]+>'=>'blog/post/view', 'blog/category/<category:.+>'=>'blog/default/category', ); } }
Тогда процесс добавления правил немного изменится:
class DModuleUrlRulesBehavior extends CBehavior { public function events() { return array_merge(parent::events(),array( 'onBeginRequest'=>'beginRequest', )); } public function beginRequest($event) { $moduleName = $this->_getCurrentModuleName(); if(Yii::app()->hasModule($moduleName)) { $class = ucfirst($moduleName) . 'Module'; Yii::import($moduleName . '.' . $class); if(method_exists($class, 'rules')) { $urlManager = Yii::app()->getUrlManager(); $urlManager->addRules(call_user_func($class .'::rules')); } } } protected function _getCurrentModuleName() { $route = Yii::app()->getRequest()->getPathInfo(); $domains = explode('/', $route); $moduleName = array_shift($domains); return $moduleName; } }
С подключением правил текущего модуля (определяемого по URL) мы разобрались. Пойдём дальше.
Импортирование правил маршрутизации для других модулей и виджетов
Предыдущее поведение импортирует правила текущего модуля. Но как сгенерировать ссылку из одного модуля в другой? Как быть с генерацией ссылок в виджетах, ведь на одной странице могут быть подключены виджеты из разных модулей?
Для правильной генерации ссылок перед использованием метода createUrl()
необходимо добавлять правила соответствующего модуля:
Yii::app()->getUrlManager()->addRules('BlogModule::rules')); $url = Yii::app()->createUrl('/blog/post/view', array('id'=>$item->id));
Но будет сложно избежать повторного подключения одних и тех же правил, если виджетов на странице несколько. Вынесем этот код в умный хэлпер:
/** * @author ElisDN <mail@elisdn.ru> * @link https://elisdn.ru */ class DUrlRulesHelper { protected static $data = array(); public static function import($moduleName) { if($moduleName && Yii::app()->hasModule($moduleName)) { if (!isset(self::$data[$moduleName])) { $class = ucfirst($moduleName) . 'Module'; Yii::import($moduleName . '.' . $class); if(method_exists($class, 'rules')) { $urlManager = Yii::app()->getUrlManager(); $urlManager->addRules(call_user_func($class .'::rules')); } self::$data[$moduleName] = true; } } } }
Он умеет подгружать правила из указанного модуля и подключает правила каждого модуля только один раз (чтобы не было повторных подключений).
Теперь в любом месте можно подгрузить правила нужного модуля по его имени командой
DUrlRulesHelper::import('blog');
Например, код виджета теперь будет выглядеть так:
Yii::import('blog.models.*'); DUrlRulesHelper::import('blog'); class LastPostsWidget extends CWidget { // ... }
Обратите внимание, что стандартных правил по умолчанию вида
<module:\w+>/<controller:\w+>/<action:\w+>
(которые записываются после всех пользовательских правил) в нашем списке нет. Есть только правила такого вида для контроллеров панели управления. Это отсутствие обусловлено тем, что методaddRules()
дописывает правила в конец списка. Если же в какой-либо момент универсальные правила попадут в список, то все правила после них (а следовательно, ниже) будут проигнорированы.
Разбор нестандартных маршрутов
Правил только одного текущего модуля часто бывает мало для корректного разбора текущего запроса. Рассмотрим следующий пример:
'rules' => array( // модуль main '/' => 'main/default/index', // site.com // модуль sitemap 'sitemap.xml' => 'sitemap/default/index', // site.com/sitemap.xml // модуль user '<action:login|logout|register>' => 'user/default/<action>', // site.com/login 'user/<username:\w+>/' => 'user/people/userInfo', // site.com/user/Admin // модуль blog 'blog' => 'blog/default/index', // site.com/blog 'blog/category/<category:[\w+_-]+>' => 'blog/default/category', // site.com/blog/category/programming 'blog/<id:[\d+_-]+>' => 'blog/post/view', // site.com/blog/57 // модуль page array('class' => 'page.components.PageUrlRule', 'cache'=>3600), // site.com/about, site.com/author и др. ),
Мы видим, что некоторые адреса не содержат имени модуля. В каком бы модуле мы ни находились, для полноценного разбора адресов типа /login
или /about
(из которых нельзя получить имя модуля) необходимо всегда подключать правила модулей main
, user
и page
. Правила некоторых модулей порой должны быть в начале списка, поэтому их нужно будет импортировать вне очереди. Их нельзя будет просто взять и добавить потом, так как они должны уже находиться в списке в определённой последовательности до момента разбора текущего адреса (задолго до работы контроллера).
За начальную загрузку модулей у нас отвечает рассмотренное ранее поведение DModuleUrlRulesBehavior
. Модифицируем его:
/** * @author ElisDN <mail@elisdn.ru> * @link https://elisdn.ru */ class DModuleUrlRulesBehavior extends CBehavior { public $beforeCurrentModule = array(); public $afterCurrentModule = array(); public function events() { return array_merge(parent::events(),array( 'onBeginRequest'=>'beginRequest', )); } public function beginRequest($event) { $module = $this->_getCurrentModuleName(); $list = array_merge( $this->beforeCurrentModule, array($module), $this->afterCurrentModule ); foreach ($list as $name) DUrlRulesHelper::import($name); } protected function _getCurrentModuleName() { $route = Yii::app()->getRequest()->getPathInfo(); $domains = explode('/', $route); $moduleName = array_shift($domains); return $moduleName; } }
Теперь достаточно указать ему два массива модулей, правила маршрутизации которых будут загружаться соответственно до и после текущего:
return array( 'behaviors'=> array( array( 'class'=>'DModuleUrlRulesBehavior', 'beforeCurrentModule'=>array( 'main', 'sitemap', 'user', ), 'afterCurrentModule'=>array( 'page', ) ) ), );
Правила модулей из этого списка будут подключаться всегда.
Результат
Проделав такую автоматизацию и отказавшись от простыней строк в конфигурационном файле мы упростили добавление новых модулей. Кроме того, мы отказались от одновременного импортирования сотен правил, что может значительно снизить нагрузку на загрузчик классов и менеджер адресов.
происходит полное создание модуля с вызовом метода init(). Это значит, что модуль создастся и импортирует свои модели или сделает ещё что-то в методе init() без нашего ведома. Поэтому лучше метод с правилами сделать статическим:
Так если мы определяем по URL название модуля, это значит что мы УЖЕ его запустили и все импорту ВЫПОЛНИЛИСЬ. И поэтому смысла в статическом методе для DModuleUrlRulesBehavior не вижу.
Так-то да, в поведении подойдёт и обычный метод. Но дальше для оптимизации динамической загрузки правил уже из других модулей мы всё равно заменим методы на статические, поэтому и поведение придется переделать. Поэтому лучше всё делать статическим сразу. Не будем же мы оставлять два метода (один обычный для поведения, а другой статический для хэлпера).
Дмитрий, как всегда - просто замечательно, как раз для меня сейчас актуальная тема, ибо модулей много и файл конфигурации уже не один килобайт.
Извини за офтоп, какой редактор ты используешь при написании статей и как подсвечиваешь код?
Из блога я вообще редактор выбросил. Использую Markdown компонент со стандартной подсветкой из Yii, а именно своё поведение. Статьи набираю в блокноте и никаких морок с HTML сущностями (код можно вставлять прямо так). Скачайте отсюда zip архив и посмотрите как там файл README.md написан.
Спасибо, сделал аналогично. Понравилось
В регистрозависимой файловой системе вылезала ошибка, что файл модуля не найден. Добавил преобразование первых символов имён классов модулей в верхний регистр функцией ucfirst().
Угу, у меня были те же грабли...
что за вызов Yii::app()->moduleManager ?
это самописный компонент ?
Да, скопипастил нечаянно. Уже убрал. Спасибо.
> Обратите внимание, что стандартных правил по умолчанию... в нашем списке нет. Это отсутствие обусловлено тем, что метод addRules() дописывает правила в конец списка.
В API говорят, что можно передать второй параметр false и добавляемые правила пойдут в начало списка. Так я решил свою проблему... А вы ? :)
Да, кстати, можно оставить правила по умолчанию и «перевернуть» очередь этим параметром.
Но я отказался от использования общих правил (кроме приведённых правил для админки), прописал в модулях все адреса конкретно и включил useStrictParsing=true. Например
Таким образом, у каждого роута имеется только один вариант вызова.
Иначе если оставить стандартные правила, то они потенциально добавят к каждому роуту по нескольку дублей. Например, главная страница модуля блога благодаря им будет открываться по любому адресу вида
Аналогично будет целый «букет» таких вариантов адресов для каждого экшена.
Так что стандартные правила по умолчанию решают одну проблему, но добавляют другую.
Скажите, а почему не срабатывает последнее правило, а остальные работают, при чем во всех модулях?
Просто Yii проходит сверху вниз, а адрес «post/create» тоже подпадает под выражение slug:.*?, из-за чего открывается post/post/view с параметром slug=create. Поставьте общее правило последним:
И в модели
Генерация ссылки срабатывает без подключения правил, а в модели Post нужно подгружать правила?
Если Вы используете createUrl в коде представлений (например, чтобы вывести ссылку «Наши новости»), то конструкцию
нужно прописывать каждый раз перед использованием самому.
А в данном случае лучше добавить для надёжности импорт во все методы getUrl(). Иначе можно легко забыть импортировать правила, и какой-либо виджет выведет ссылки не так, как надо. Например, на странице блога виджет последних комментариев собирается правильно, а на странице магазина этот же виджет уже генерируется неверно, так как в блогах правила импортировались сами по себе, а в магазине про подключение правила блога забыли.
спасибо, все работает
Привет, у вас есть урл правило для записи в блоге
Я сделал похожее правило
т.е. id это алиас (транслитерированный заголовок поста) в екшене проверяю наличие этого алиаса в бд
И тут у меня возникла следующая проблема, круд генератор на основе имени модели сгенерировал вью post (Post, модель которая хранит записи блога) и теперь по адресу blog/post выпадает 404 ошибка. Я переименовал модуль blog в blogs, но это конечно не решение проблемы. Как быть в такой ситуации?
Да, экшен подпадает здесь под не то правило. Можно либо все частные случаи прописать выше:
либо добавить в шаблон вывода записи что-то уникальное. Это, например, добавить суффикс «.html»:
или оставить числовой ID в адресе с псевдонимом:
На этом сайте, например, сделано вторым способом.
сейчас разбираю такое решение http://habrahabr.ru/post/155927/
Прочитал статью, подход действительно интересный, но вы зря ругаете Yupe за выбранное решение. Что же касается вашего подхода у него есть множество минусов, которые возможно вы можете решить, но вопрос лишь в том, не создадут ли эти решения очередные костыли. Собственно, чтобы не быть голословным попытаюсь показать на реальных примерах:
Замечатьльно, вот только что если мы используем иной подход к адресации? К примеру:
ru/blog/show...
то есть в данном примере имя модуля будет "ru" (O_o?)
а как насчёт иного подхода к адресам?
ru/news/my-new-post
ru/programming/my-new-post-about-programming
de/story/this-story-about-my-summer
к примеру эти все адреса должны вести на модуль Blog.
Мне кажется, что назвать ваш подход идеальным - тоже не совсем верно, так как при каждом варианте будут свои как положительные, так и отрицательные стороны.
Комментарий не со зла, а справедливости ради. Просто вы не описали подводные камни, с которыми может столкнуться разработчик используя данный поход.
>> ru/blog/show...
Добавляем всего один if:
и получаем модуль blog даже если указан язык.
>> ru/news/my-new-post
>> ru/programming/my-new-post-about-programming
>> de/story/this-story-about-my-summer
Поступаем как ранее или вырезаем языки из адреса вообще + создаём класс-правило BlogUrlRule и подключаем его принудительно вместе со страницами:
Проблемы решены и не возникнув.
Постепенно это превратится в лапшекод (множественные if-ы), я бы не был так строг к регулярным выражениям, тем более что они нативны, в отличии от php-кода, что уже говорит о их производительности, согласны?
Вообще-то все маршруты из списка rules используются не сами по себе, а по умолчанию оборачиваются в класс CUrlRule и там уже преобразоваваются в регулярное выражения и обрабатываются теми же самыми if-ами. Загляните в класс CUrlRule. Там десятки if-ов и операций со строками. Поэтому наше примитивное BlogUrlRule будет работать быстрее стандартного CUrlRule (конечно же если оно не будет прямо обращаться к БД без кэширования).
Следовательно, если мы подключаем одним списком сразу 100 правил из 15 модулей, то Yii создаёт массив из 100 объектов класса CUrlRule и при каждом вызове createUrl() каждый раз обходит этот массив и вызывает метод createUrl() каждого объекта, пока кто-то не вернёт результат.
Так что с нативностью «обычных» правил не согласен. А мой подход вместо постоянной загрузки сотен лишних правил для крупного проекта позволяет импортировать по требованию всего пару десятков правил только для нужных в данный момент модулей. При этом в проекте может существовать почти неограниченное число модулей без снижения производительности.
Вы можете предоставить бенчмарки где будет показано не на представлении автора, а на графиках ваше превосходство?
Бенчмарк на проверку 100 createUrl() c «1000 правил из конфига» vs «50 правил по требованию»?
Нет, бенчмарк который покажет, что ваш метод действительно столь продуктивен и слова оправданы. Не хочется попросту голословия
Ок, у меня настройки языков (к примеру) хранятся в БД, простым обращением к параметра - я уже не выкручусь.
> BlogUrlRule и подключаем его принудительно вместе со страницами:
то есть, я правильно понимаю, вы просто дополнительно подключаете ещё модули, что бы... опять же распарсить результат?
Отлично, но я вёл не к тому. Что если у вас (как вы уже говорили) +100500 модулей, у которых хоть один но есть собственный уникальный адрес (не module/controller/action, а чпу)?
Выходом из ситуации не есть вариант городить велосипеды, а правильнее использовать нативные (родные для языка) методы и функции. Поправьте, если я ошибаюсь.
>> подключаете ещё модули
Просто в правила по умолчанию добавляю нестандартные правила из статического метода класса модуля. Никаких сложных подключений модулей при этом не происходит.
>> 100500 модулей, у которых хоть один но есть собственный уникальный адрес
Тогда без разницы. Всё равно придётся подключать все.
>> а правильнее использовать нативные (родные для языка) методы и функции
Об отсутствии нативных для языка методов в Yii рассказал выше.
Повторюсь, это лишь ваше мнение, которое не обоснованно какими бы то ни было графиками и/или бенчмарками, потому я останусь при своём мнении.
Но вы также упустили то, что Yii кеширует правила
Серилизует и кэширует он только сам массив объектов правил. На результативность createUrl() при каждом обходе привил кэширование не влияет. Также можно легко закэшировать языки из БД.
Ок, что касательно бенчмарков, вы можете заявить по ним о том, что ваш метод более продуктивен?
Я не спорю касательно импортов и прочего, разговор лишь касательно правил маршрутизации.
Повторюсь, пожалуй, ведь мне показалось, что вы меня совсем не верно поняли. Подход может быть и интересный, но вы заявили, что подход используемый в Юпи - не оправдан и, ваше решение даёт огромный прирост. Поэтому хотелось бы видеть действительно цифры, которые это покажут, а не рассуждения автора.
Только прошу учесть, что необходимо иметь информацию о всех имеющихся в системе модулях, с их личными настройками.
Если ваш подход действительно способен сделать это лучше и более правильно - не вопрос, тем более если это покажут цифры.
Я просто не мог пройти мимо.)
Евгений, поставьте xhprof, проведите нужные бенчмарки сами.
Подход описанный в статье - очень хорош.
Пример:
5 модулей, кучка достаточно специализированных правил, строк так на 200.
Скрин стат. с главной ДО - http://joxi.ru/4HZkUtg5CbBZUYIfESQ
Скрин стат. с главной ПОСЛЕ - http://joxi.ru/hnhkUtg5CbAeTM-nJu4
Если что, тут статистика с сортировкой по Excl. CPU (microsec). Так что... думайте сами, решайте сами...
Интересный момент. Добавляю в модуль:
Правило генерируется, но при переходе по ссылке в действие view не попадает (404). Если же это правило добавить в конфиг общий - работает или если исправить на
то работает
Добавил в конфиге
тогда заработало, так как метод _getCurrentModuleName возвращает имя текущего модуля, разбирая урл, что есть не хорошо
Оно так и задумывалось, чтобы разбирать модуль по адресу, а несовпадающие специфические варианты добавлять вручную.
А как быть с errorAction в таком случае, у меня никак не выходит перенаправить все ошибки, которые идут из backend на свой errorHandler. Всегда отправляет на стандартный site/error. Пробовал переопределить AdminController, тщетно.
У меня нормально работает определённый в конфигурации:
Отличная статья! Подскажите что нужно поменять/дописать в DModuleUrlRulesBehavior и в DUrlRulesHelper что бы корректно все это работало с namespaces в модулях?
Нужно добавить пространство имён в строку получения класса:
Подскажите, пожалуйста, как задать для приложения контроллером по-умолчанию контроллер DefaultController из модуля Test, чтобы при обращении по адресу site.local вызывалось действие Index этого контроллера?
или в конфигурации выставить параметр defaultController.
Дмитрий, а как сделать, чтобы в модуле заработало расширение bootstrap? Оно находится в папке extensions. Пытался сделать через setComponents, но ничего не вышло. В итоге js файлы у меня загружаются из assets'а родительского приложения. А хочется, чтобы модуль был по максимуму самостоятельным. Спасибо заранее.
Не могу сразу сказать, так как с ним не работал.
Здравствуйте! Отличная статья!
Но мне еще интересно как можно реализовать для модулей общую админку как на eximus commerce к примеру у каждого модуля в контроллере будет директория admin а в ней свои контроллеры и переходить к ней можно будет используля url admin/blog и мы в админке модуля blog
Добрый день, очень занимательно, у меня вопрос, есть ли исходники по Вашим урокам, а то для новичка не совсем понятно, в каких файлах использовать тот или иной код, Вы бы так упростили понимание материала!
Есть. Может быть выложу на GitHub.
Здравствуйте, скорее всего тут у вас ошибка.
А именно
тут лишнее "\w+".
Точнее у вас так
а должно быть так
Как раз у меня были контроллеры postAdmin, categoryAdmin и т.д.
Сразу не заметил, извините.
Я почему-то подумал что контроллеры "admin" без префикса.
Да и вообще, есть ли смысл подгружать только правила текущего модуля и те которые импортируются по необходимости?
Вот допустим есть у нас меню сайта, в котором есть ссылки если даже не на все модули приложения то на большую их часть - тогда получается что каждый раз все равно мы подгружаем все правила только уже хитрым методом "через хелперы и т. д.". Я считаю что это не совсем красиво и трудоемко по отношению к приложению. И все таки меню то отображается на всех страницах сайта.
А вот что касается вынести все правила - каждое в свой модуль, эта идея хорошая.
Спасибо за статью
Как можно переключаться с одного подшаблон на другой в разных модулях?
Хочу чтобы в созданном модуле админки были несколько подшаблонов. Создал пока 4 - По умолчанию column1 и column2
column1.php
column2.php
У меня в контроллере default ни на одном экшене не выходит слово: column1 или column2.
Пробовал в init модуля прописать как тут vispyanskiy.name/ru/kak-zadat-shablon-dlya-modulya-v-yii :
$this->layoutPath = Yii::getPathOfAlias('admin.views.layouts');
$this->layout = 'main';
Потом пробовал установить путь из экшенов DefaultController:
$this->layout = 'admin.views.layouts.main.column1';
Влетаю в текст с моим контентом, но без стилей, т.е. тема не подключается.
По Совету этого китайца https://www.youtube.com/watch?v=Nc0ED8_VsT4 :
Закомментировал в protected/components/Controller.php :
// public $layout = '//layouts/column1';
Но это не помогло.
К данному виду я спокойно могу подключиться:
admin.views.layouts.main
Но так не интересно, хочется брать еще и подшаблоны.
Помогите пожалуйста.
Вопрос ещё актуален?
Разобрался как переключаться в column в модулях, но при этом main берется из темы , интересуют 2 вещи:
Как сделать главным для текущего модуля - main из его представления
И
Как наоборот для одного модуля использовать column - ы из темы, привязаной к модулю
Спасибо, поэкспериментирую с этим.
Спасибо за статью.
Но как организовать это дело "Динамическая подгрузка правил маршрутизации" в Yii2 ?
Вот это в Yii2 уже не работает:
В Yii2 поведения подключаются через as:
Про них, кстати, вебинар был.
Хорошо было бы эту же статью написать под Yii2 или же вебинар провести на эту тему - модульная структура, загрузка конфигурации модулей. Будет круто!
Вообщем, в итоге сделал следующим образом.
Процедуру beginRequest() повесил в bootstrap($app) класса CoreBootstrap, а bootstrap у нас идет раньше EventBeforeRequest.
Как быть в Yii2 с классом DUrlRulesHelper? В Yii2 нет процедуры Yii::import.
И как в Yii2 быть вот здесь $urlManager->addRules(call_user_func($class .'::rules')) ?
Ведь для вызова статического метода класса необходим полный путь (с namespace) класса.
Уберите строку Yii::import. В Yii2 всё подключается через автозагрузчик.
А вместо строки:
используйте:
Спасибо, Дмитрий.
Я так понял, что в этом случае инициализации Модуля не будет?
Через getModules не будет.
А язык как сменить для модуля?
Язык берётся всегда из Yii::app()->language. Можно его присвоить в beforeAction модуля.
Где лайки ставить?