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

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

Результат

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

Другие статьи

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

В системах управления контентом (CMS) интернет-магазинов (да и часто для других нужд) можно встретить возможность добавлять неограниченное число полей к различным сущностям. Это знакомые всем списки характеристик товаров, а иногда имеется целая система создания новых типов контента (CCK), которой, кстати, славится Drupal. До полноценной системы CCK нам далеко, но реализовать динамические поля для товаров своего интернет-магазина на Yii всё-таки стоит.

Раньше мы рассматривали возможность повторного использования стандартных CRUD операций путём выноса их в отдельные классы (наследники класса CAction в Yii). У этого способа есть альтернатива – наследование общих действий от базового контроллера. Рассмотрим этот способ подробнее, а также попробуем найти и решить некоторые его проблемы.

На многих новых сайтах всё чаще встречается вывод списка новостей или других сущностей в виде бесконечно подгружающейся ленты. На некоторых сайтах подгрузка выполняется автоматически (на twitter.com или vk.com), на других – вручную, то есть в конце списка вместо стандартного переключателя страниц имеется кнопка «Показать ещё». Освежим в памяти работу с ClistView и попробуем реализовать подобный функционал на своём сайте.

Комментарии

 

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

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

Ответить

 

Евгений

Прочитал статью, подход действительно интересный, но вы зря ругаете 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',
)

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

Ответить

 

Евгений

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

Ответить

 

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

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

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

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

Ответить

 

Евгений

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

Ответить

 

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

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

Ответить

 

Евгений

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

Ответить

 

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

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

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

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

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

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

Ответить

 

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

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

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

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

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

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

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

Ответить

 

Евгений

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

Ответить

 

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

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

Ответить

 

Евгений

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

Ответить

 

Евгений

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

Ответить

 

Ярослав

Я просто не мог пройти мимо.)
Евгений, поставьте 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 не будет.

Ответить

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

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


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



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