Перенос конфигурации в модули 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
<?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',
            )
        )
    ),
);

Правила модулей из этого списка будут подключаться всегда.

Результат

Проделав такую автоматизацию и отказавшись от простыней строк в конфигурационном файле мы упростили добавление новых модулей. Кроме того, мы отказались от одновременного импортирования сотен правил, что может значительно снизить нагрузку на загрузчик классов и менеджер адресов.

Комментарии

 

rar

происходит полное создание модуля с вызовом метода init(). Это значит, что модуль создастся и импортирует свои модели или сделает ещё что-то в методе init() без нашего ведома. Поэтому лучше метод с правилами сделать статическим:

Так если мы определяем по URL название модуля, это значит что мы УЖЕ его запустили и все импорту ВЫПОЛНИЛИСЬ. И поэтому смысла в статическом методе для DModuleUrlRulesBehavior не вижу.

Ответить

 

Дмитрий Елисеев

Так-то да, в поведении подойдёт и обычный метод. Но дальше для оптимизации динамической загрузки правил уже из других модулей мы всё равно заменим методы на статические, поэтому и поведение придется переделать. Поэтому лучше всё делать статическим сразу. Не будем же мы оставлять два метода (один обычный для поведения, а другой статический для хэлпера).

Ответить

 

Виталий Иванов

Дмитрий, как всегда - просто замечательно, как раз для меня сейчас актуальная тема, ибо модулей много и файл конфигурации уже не один килобайт.
Извини за офтоп, какой редактор ты используешь при написании статей и как подсвечиваешь код?

Ответить

 

Елисеев Дмитрий

Из блога я вообще редактор выбросил. Использую Markdown компонент со стандартной подсветкой из Yii, а именно своё поведение. Статьи набираю в блокноте и никаких морок с HTML сущностями (код можно вставлять прямо так). Скачайте отсюда zip архив и посмотрите как там файл README.md написан.

Ответить

 

Виталий Иванов

Спасибо, сделал аналогично. Понравилось

Ответить

 

Елисеев Дмитрий

В регистрозависимой файловой системе вылезала ошибка, что файл модуля не найден. Добавил преобразование первых символов имён классов модулей в верхний регистр функцией ucfirst().

Ответить

 

Виталий Иванов

Угу, у меня были те же грабли...

Ответить

 

xar

что за вызов Yii::app()->moduleManager ?
это самописный компонент ?

Ответить

 

Дмитрий Елисеев

Да, скопипастил нечаянно. Уже убрал. Спасибо.

Ответить

 

Аурел

> Обратите внимание, что стандартных правил по умолчанию... в нашем списке нет. Это отсутствие обусловлено тем, что метод addRules() дописывает правила в конец списка.

В API говорят, что можно передать второй параметр false и добавляемые правила пойдут в начало списка. Так я решил свою проблему... А вы ? :)

Ответить

 

Дмитрий Елисеев

Да, кстати, можно оставить правила по умолчанию и «перевернуть» очередь этим параметром.

Но я отказался от использования общих правил (кроме приведённых правил для админки), прописал в модулях все адреса конкретно и включил useStrictParsing=true. Например

'blog'=>'blog/default/index',

Таким образом, у каждого роута имеется только один вариант вызова.

Иначе если оставить стандартные правила, то они потенциально добавят к каждому роуту по нескольку дублей. Например, главная страница модуля блога благодаря им будет открываться по любому адресу вида

/blog
/blog/default
/blog/default/index

Аналогично будет целый «букет» таких вариантов адресов для каждого экшена.

Так что стандартные правила по умолчанию решают одну проблему, но добавляют другую.

Ответить

 

helloworld

Скажите, а почему не срабатывает последнее правило, а остальные работают, при чем во всех модулях?

''=>'post/post/index',            
'post/<slug:.*?>'=>'post/post/view',
'post/create'=>'post/post/create', 
Ответить

 

Дмитрий Елисеев

Просто Yii проходит сверху вниз, а адрес «post/create» тоже подпадает под выражение slug:.*?, из-за чего открывается post/post/view с параметром slug=create. Поставьте общее правило последним:

''=>'post/post/index',      
'post/create'=>'post/post/create',       
'post/<slug:.*?>'=>'post/post/view',
Ответить

 

helloworld

И в модели

public function getUrl(){
    return Yii::app()->createUrl('/news/news/view', array('slug'=>$this->slug));
}

Генерация ссылки срабатывает без подключения правил, а в модели Post нужно подгружать правила?

public function getUrl(){
    DUrlRulesHelper::import('post');
    return Yii::app()->createUrl('/post/post/view', array('slug'=>$this->slug));
}
Ответить

 

Дмитрий Елисеев

Если Вы используете createUrl в коде представлений (например, чтобы вывести ссылку «Наши новости»), то конструкцию

DUrlRulesHelper::import('post');

нужно прописывать каждый раз перед использованием самому.

А в данном случае лучше добавить для надёжности импорт во все методы getUrl(). Иначе можно легко забыть импортировать правила, и какой-либо виджет выведет ссылки не так, как надо. Например, на странице блога виджет последних комментариев собирается правильно, а на странице магазина этот же виджет уже генерируется неверно, так как в блогах правила импортировались сами по себе, а в магазине про подключение правила блога забыли.

Ответить

 

helloworld

спасибо, все работает

Ответить

 

Dmitry

Привет, у вас есть урл правило для записи в блоге

'blog/<id:[\d+_-]+>' => 'blog/post/view', // site.com/blog/57, 

Я сделал похожее правило

'blog/<id:[\w+_-]+>' => 'blog/post/view', 

т.е. id это алиас (транслитерированный заголовок поста) в екшене проверяю наличие этого алиаса в бд

if(!$id) throw new CHttpException(404,'Ой, такой записи в блоге нет :(');

И тут у меня возникла следующая проблема, круд генератор на основе имени модели сгенерировал вью post (Post, модель которая хранит записи блога) и теперь по адресу blog/post выпадает 404 ошибка. Я переименовал модуль blog в blogs, но это конечно не решение проблемы. Как быть в такой ситуации?

Ответить

 

Дмитрий Елисеев

Да, экшен подпадает здесь под не то правило. Можно либо все частные случаи прописать выше:

'blog/<action:create|update|delete>' => 'blog/post/<action>'
'blog/<id:[\w+_-]+>.html' => 'blog/post/view', 

либо добавить в шаблон вывода записи что-то уникальное. Это, например, добавить суффикс «.html»:

'blog/<id:[\w+_-]+>.html' => 'blog/post/view', 

или оставить числовой ID в адресе с псевдонимом:

'blog/<id:\d+>/<alias:[\w+_-]+>' => 'blog/post/view',

На этом сайте, например, сделано вторым способом.

Ответить

 

Евгений – akulikov.org.ua

Прочитал статью, подход действительно интересный, но вы зря ругаете Yupe за выбранное решение. Что же касается вашего подхода у него есть множество минусов, которые возможно вы можете решить, но вопрос лишь в том, не создадут ли эти решения очередные костыли. Собственно, чтобы не быть голословным попытаюсь показать на реальных примерах:

    protected function _getCurrentModuleName()
    {
        $route = Yii::app()->getRequest()->getPathInfo();
        $domains = explode('/', $route);
        $moduleName = array_shift($domains);
        return $moduleName;
    }

Замечатьльно, вот только что если мы используем иной подход к адресации? К примеру:

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:

$moduleName = array_shift($domains);
if (in_array($moduleName, Yii::app()->params['languages'])) {
    $moduleName = array_shift($domains);
}

и получаем модуль blog даже если указан язык.

>> ru/news/my-new-post
>> ru/programming/my-new-post-about-programming
>> de/story/this-story-about-my-summer

Поступаем как ранее или вырезаем языки из адреса вообще + создаём класс-правило BlogUrlRule и подключаем его принудительно вместе со страницами:

'afterCurrentModule'=>array(
    'blog',
    'page',
)

Проблемы решены и не возникнув.

Ответить

 

Евгений – akulikov.org.ua

Постепенно это превратится в лапшекод (множественные if-ы), я бы не был так строг к регулярным выражениям, тем более что они нативны, в отличии от php-кода, что уже говорит о их производительности, согласны?

Ответить

 

Дмитрий Елисеев

Вообще-то все маршруты из списка rules используются не сами по себе, а по умолчанию оборачиваются в класс CUrlRule и там уже преобразоваваются в регулярное выражения и обрабатываются теми же самыми if-ами. Загляните в класс CUrlRule. Там десятки if-ов и операций со строками. Поэтому наше примитивное BlogUrlRule будет работать быстрее стандартного CUrlRule (конечно же если оно не будет прямо обращаться к БД без кэширования).

Следовательно, если мы подключаем одним списком сразу 100 правил из 15 модулей, то Yii создаёт массив из 100 объектов класса CUrlRule и при каждом вызове createUrl() каждый раз обходит этот массив и вызывает метод createUrl() каждого объекта, пока кто-то не вернёт результат.

Так что с нативностью «обычных» правил не согласен. А мой подход вместо постоянной загрузки сотен лишних правил для крупного проекта позволяет импортировать по требованию всего пару десятков правил только для нужных в данный момент модулей. При этом в проекте может существовать почти неограниченное число модулей без снижения производительности.

Ответить

 

Евгений – akulikov.org.ua

Вы можете предоставить бенчмарки где будет показано не на представлении автора, а на графиках ваше превосходство?

Ответить

 

Дмитрий Елисеев

Бенчмарк на проверку 100 createUrl() c «1000 правил из конфига» vs «50 правил по требованию»?

Ответить

 

Евгений – akulikov.org.ua

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

Ответить

 

Евгений – akulikov.org.ua
$moduleName = array_shift($domains);
if (in_array($moduleName, Yii::app()->params['languages'])) {
    $moduleName = array_shift($domains);
}

Ок, у меня настройки языков (к примеру) хранятся в БД, простым обращением к параметра - я уже не выкручусь.

> BlogUrlRule и подключаем его принудительно вместе со страницами:

то есть, я правильно понимаю, вы просто дополнительно подключаете ещё модули, что бы... опять же распарсить результат?

Отлично, но я вёл не к тому. Что если у вас (как вы уже говорили) +100500 модулей, у которых хоть один но есть собственный уникальный адрес (не module/controller/action, а чпу)?

Выходом из ситуации не есть вариант городить велосипеды, а правильнее использовать нативные (родные для языка) методы и функции. Поправьте, если я ошибаюсь.

Ответить

 

Дмитрий Елисеев

>> подключаете ещё модули

Просто в правила по умолчанию добавляю нестандартные правила из статического метода класса модуля. Никаких сложных подключений модулей при этом не происходит.

>> 100500 модулей, у которых хоть один но есть собственный уникальный адрес

Тогда без разницы. Всё равно придётся подключать все.

>> а правильнее использовать нативные (родные для языка) методы и функции

Об отсутствии нативных для языка методов в Yii рассказал выше.

Ответить

 

Евгений – akulikov.org.ua

Повторюсь, это лишь ваше мнение, которое не обоснованно какими бы то ни было графиками и/или бенчмарками, потому я останусь при своём мнении.
Но вы также упустили то, что Yii кеширует правила

Ответить

 

Дмитрий Елисеев

Серилизует и кэширует он только сам массив объектов правил. На результативность createUrl() при каждом обходе привил кэширование не влияет. Также можно легко закэшировать языки из БД.

Ответить

 

Евгений – akulikov.org.ua

Ок, что касательно бенчмарков, вы можете заявить по ним о том, что ваш метод более продуктивен?
Я не спорю касательно импортов и прочего, разговор лишь касательно правил маршрутизации.

Ответить

 

Евгений – akulikov.org.ua

Повторюсь, пожалуй, ведь мне показалось, что вы меня совсем не верно поняли. Подход может быть и интересный, но вы заявили, что подход используемый в Юпи - не оправдан и, ваше решение даёт огромный прирост. Поэтому хотелось бы видеть действительно цифры, которые это покажут, а не рассуждения автора.
Только прошу учесть, что необходимо иметь информацию о всех имеющихся в системе модулях, с их личными настройками.
Если ваш подход действительно способен сделать это лучше и более правильно - не вопрос, тем более если это покажут цифры.

Ответить

 

Ярослав

Я просто не мог пройти мимо.)
Евгений, поставьте xhprof, проведите нужные бенчмарки сами.

Подход описанный в статье - очень хорош.

Пример:
5 модулей, кучка достаточно специализированных правил, строк так на 200.

Скрин стат. с главной ДО - http://joxi.ru/4HZkUtg5CbBZUYIfESQ
Скрин стат. с главной ПОСЛЕ - http://joxi.ru/hnhkUtg5CbAeTM-nJu4

Если что, тут статистика с сортировкой по Excl. CPU (microsec). Так что... думайте сами, решайте сами...

Ответить

 

standalone

Интересный момент. Добавляю в модуль:

public static function rules()
{
    return array(
        '/' => 'page/page/index',
        'pages/<url:\w+>' => array('page/page/view', 'urlSuffix' => '.html'),
    );
}

Правило генерируется, но при переходе по ссылке в действие view не попадает (404). Если же это правило добавить в конфиг общий - работает или если исправить на

'page/<url:\w+>' => array('page/page/view', 'urlSuffix' => '.html')

то работает

Ответить

 

standalone

Добавил в конфиге

'afterCurrentModule'=>array(
    'page',
)

тогда заработало, так как метод _getCurrentModuleName возвращает имя текущего модуля, разбирая урл, что есть не хорошо

Ответить

 

Дмитрий Елисеев

Оно так и задумывалось, чтобы разбирать модуль по адресу, а несовпадающие специфические варианты добавлять вручную.

Ответить

 

standalone

А как быть с errorAction в таком случае, у меня никак не выходит перенаправить все ошибки, которые идут из backend на свой errorHandler. Всегда отправляет на стандартный site/error. Пробовал переопределить AdminController, тщетно.

Ответить

 

Дмитрий Елисеев

У меня нормально работает определённый в конфигурации:

'errorHandler'=>array(
    'errorAction'=>'/main/error/index',
),
Ответить

 

Руслан

Отличная статья! Подскажите что нужно поменять/дописать в DModuleUrlRulesBehavior и в DUrlRulesHelper что бы корректно все это работало с namespaces в модулях?

Ответить

 

Дмитрий Елисеев

Нужно добавить пространство имён в строку получения класса:

$class = ucfirst($moduleName) . 'Module';
Ответить

 

Алексей

Подскажите, пожалуйста, как задать для приложения контроллером по-умолчанию контроллер DefaultController из модуля Test, чтобы при обращении по адресу site.local вызывалось действие Index этого контроллера?

Ответить

 

Дмитрий Елисеев
'rules' => array(
    '' => 'test/default/index',
),

или в конфигурации выставить параметр defaultController.

Ответить

 

Сергей

Дмитрий, а как сделать, чтобы в модуле заработало расширение bootstrap? Оно находится в папке extensions. Пытался сделать через setComponents, но ничего не вышло. В итоге js файлы у меня загружаются из assets'а родительского приложения. А хочется, чтобы модуль был по максимуму самостоятельным. Спасибо заранее.

Ответить

 

Дмитрий Елисеев

Не могу сразу сказать, так как с ним не работал.

Ответить

 

Adil

Здравствуйте! Отличная статья!
Но мне еще интересно как можно реализовать для модулей общую админку как на eximus commerce к примеру у каждого модуля в контроллере будет директория admin а в ней свои контроллеры и переходить к ней можно будет используля url admin/blog и мы в админке модуля blog

Ответить

 

Andrey

Добрый день, очень занимательно, у меня вопрос, есть ли исходники по Вашим урокам, а то для новичка не совсем понятно, в каких файлах использовать тот или иной код, Вы бы так упростили понимание материала!

Ответить

 

Дмитрий Елисеев

Есть. Может быть выложу на GitHub.

Ответить

 

Дмитрий

Здравствуйте, скорее всего тут у вас ошибка.

'<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>',

А именно

<controller:\w+[Aa]dmin>

тут лишнее "\w+".

Ответить

 

Дмитрий

Точнее у вас так

<controller:\w+[Aa]dmin>

а должно быть так

<controller:[Aa]dmin>
Ответить

 

Дмитрий Елисеев

Как раз у меня были контроллеры postAdmin, categoryAdmin и т.д.

Ответить

 

Дмитрий

Сразу не заметил, извините.
Я почему-то подумал что контроллеры "admin" без префикса.

Ответить

 

Дмитрий

Да и вообще, есть ли смысл подгружать только правила текущего модуля и те которые импортируются по необходимости?

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

А вот что касается вынести все правила - каждое в свой модуль, эта идея хорошая.

Ответить

 

Andrey

Спасибо за статью

Как можно переключаться с одного подшаблон на другой в разных модулях?

Хочу чтобы в созданном модуле админки были несколько подшаблонов. Создал пока 4 - По умолчанию column1 и column2


class AdminModule extends CWebModule
{
  public function init()
  {
        $this->layout =  '/layouts/main';
  }
}

class DefaultController extends Controller
{
  public function actionIndex()
  {
        $this->layout = '//layouts/column1';
    $this->render('index');
  }

    public function actionView()
    {
        $this->layout = '//layouts/column2';
        $this->render('index');
    }
}

column1.php

<?php $this->beginContent('//layouts/main'); ?>
<div id="content">
    column1
  <?php echo $content; ?>
</div><!-- content -->
<?php $this->endContent(); ?>

column2.php

<?php $this->beginContent('//layouts/main'); ?>
<div class="span-19">
  <div id="content">
        column2
    <?php echo $content; ?>
  </div><!-- content -->
</div>
<div class="span-5 last">

  <div id="sidebar">

  <?php
    $this->beginWidget('zii.widgets.CPortlet', array(
      'title'=>'Operations',
    ));
    $this->widget('zii.widgets.CMenu', array(
      'items'=>$this->menu,
      'htmlOptions'=>array('class'=>'operations'),
    ));
    $this->endWidget();
  ?>
  </div><!-- sidebar -->
</div>
<?php $this->endContent(); ?>

У меня в контроллере 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

Но так не интересно, хочется брать еще и подшаблоны.

Помогите пожалуйста.

Ответить

 

Дмитрий Елисеев

Вопрос ещё актуален?

Ответить

 

Andrey

Разобрался как переключаться в column в модулях, но при этом main берется из темы , интересуют 2 вещи:

Как сделать главным для текущего модуля - main из его представления

И

Как наоборот для одного модуля использовать column - ы из темы, привязаной к модулю

Ответить

 

Дмитрий Елисеев
<?php $this->beginContent('main'); ?>
$this->layout = 'column1';
Ответить

 

Andrey

Спасибо, поэкспериментирую с этим.

Ответить

 

Рамиль

Спасибо за статью.
Но как организовать это дело "Динамическая подгрузка правил маршрутизации" в Yii2 ?

Вот это в Yii2 уже не работает:

return array(
 
    // ...
 
    'behaviors'=> array(
        array(
            'class'=>'DModuleUrlRulesBehavior',
        )
    ),
);
Ответить

 

Дмитрий Елисеев

В Yii2 поведения подключаются через as:

'as ModuleUrlRules' => [
    'class' => 'app\components\ModuleUrlRulesBehavior',
],

Про них, кстати, вебинар был.

Ответить

 

Рамиль

Хорошо было бы эту же статью написать под Yii2 или же вебинар провести на эту тему - модульная структура, загрузка конфигурации модулей. Будет круто!

Ответить

 

Рамиль

Вообщем, в итоге сделал следующим образом.
Процедуру beginRequest() повесил в bootstrap($app) класса CoreBootstrap, а bootstrap у нас идет раньше EventBeforeRequest.

'bootstrap' => [
    [
        /* Set current Theme, load current module url rules */
        'class' => 'app\modules\core\components\CoreBootstrap',
        'theme' => '', // if empty then get Theme from database
    ],
]
Ответить

 

Рамиль

Как быть в Yii2 с классом DUrlRulesHelper? В Yii2 нет процедуры Yii::import.

И как в Yii2 быть вот здесь $urlManager->addRules(call_user_func($class .'::rules')) ?
Ведь для вызова статического метода класса необходим полный путь (с namespace) класса.

Ответить

 

Дмитрий Елисеев

Уберите строку Yii::import. В Yii2 всё подключается через автозагрузчик.

А вместо строки:

$class = ucfirst($moduleName) . 'Module';

используйте:

$modules = Yii::$app->getModules();
$module = $modules[$moduleName];
if ($module instanceof \yii\base\Module) {
    $class = get_class($module);
} else {
    $class = $module['class'];
}
Ответить

 

Рамиль

Спасибо, Дмитрий.
Я так понял, что в этом случае инициализации Модуля не будет?

Ответить

 

Дмитрий Елисеев

Через getModules не будет.

Ответить

 

Игорь

А язык как сменить для модуля?

Ответить

 

Дмитрий Елисеев

Язык берётся всегда из Yii::app()->language. Можно его присвоить в beforeAction модуля.

Ответить

 

Mat

Где лайки ставить?

Ответить

Оставить комментарий

Войти | Завести аккаунт | Войти через


(никто не увидит)





Можно использовать теги <p> <ul> <li> <b> <i> <a> <pre>