Маршрутизация во фреймворках: CUrlManager в Yii

Стороны света

В первой части мы с вами рассмотрели работу системы маршрутизации, изучили некоторые нюансы и научились правильно составлять шаблоны адресов. В этой же части мы попробуем перейти непосредственно к пониманию работы и нюансам использования менеджера адресов в Yii Framework.

Первая часть: Маршрутизация во фреймворках: Управление адресами URL

Маршрутизация в Yii Framework

Читателя, освоившего первую часть, можно поздравить с тем, что он теперь знает принцип работы маршрутизации в Yii. Да-да, достаточно немного улучшить «придуманный» нами код, и мы получим классы CUrlManager и CUrlRule этого фреймворка.

Компонент Yii::app()->urlManager по умолчанию является экземпляром класса CUrlManager и описывается как и другие компоненты в конфигурационном файле. После создания приложения командой yiic webapp секция этого компонента в конфигурационном файле имеет вид:

return array(
    ...
    'components'=>array(
        ...
 
        'urlManager'=>array(
            'urlFormat' => 'path',
            'rules'=>array(
                '<controller:\w+>/<id:\d+>' => '<controller>/view',
                '<controller:\w+>/<action:\w+>/<id:\d+>' => '<controller>/<action>',
                '<controller:\w+>/<action:\w+>' => '<controller>/<action>',
            ),
        ),
 
        ...
    ),
);

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

Если вообще убрать имеющиеся правила, то маршрут post/view подставится в первозданном виде и параметр id просто передастся в GET строке:

http://site.com/index.php/post/view?id=52

А если не убирать, то сгенерированный адрес для маршрута post/view в соответствие с первым правилом будет выглядеть так:

http://site.com/index.php/post/52

Если же поменять значение 'urlFormat'=>'path' на 'urlFormat'=>'get', то адресация будет производиться простыми GET параметрами:

http://site.com/index.php?r=post&id=52

Для передачи маршрута в обычном GET запросе в Yii по умолчанию используется параметр r.

Настройка человекопонятных адресов в Yii

Чтобы наиболее эффективно воспользоваться преобразованиями адресов мы выберем вариант с 'urlFormat'=>'path', но уберём index.php из адреса.

Сначала нужно настроить соответствующим образом наш сервер на перенаправление всех запросов в файл index.php, как мы делали это вначале:

Конфигурация Apache (.htaccess):

RewriteEngine on
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . index.php

Конфигурация Nginx:

server {
    ...
    location / {
        index index.html index.php;
        try_files $uri $uri/ /index.php?$args;
    }
}

Теперь можем убрать имя файла index.php из адреса, указав 'showScriptName'=>false:

'urlManager'=>array(
    'urlFormat' => 'path',
    'showScriptName'=>false,
    'urlSuffix' => '',
    'rules'=>array(
        'feed.xml' => array('post/feed', 'urlSuffix' => ''),
        '<controller:\w+>/<id:\d+>' => '<controller>/view',
        '<controller:\w+>/<action:\w+>/<id:\d+>' => '<controller>/<action>',
        '<controller:\w+>/<action:\w+>' => '<controller>/<action>',
    ),
),

Теперь если мы зайдём по адресу

http://site.com/post/update/1

то согласно третьему правилу выполнится действие post/update и GET-параметру id присвоится значение 1. Для удобства в Yii сделано так, что параметры для подстановки можно указывать как аргументы метода действия:

class PostController extends Controller
{
    public function actionUpdate($id) {
        echo $id; // выведет '1'
    }
}

Заодно мы попробовали добавить правило для обработки feed.xml:

'feed.xml' => array('post/feed', 'urlSuffix' => ''),

Почему же мы сделали это таким способом? Мы же могли это сделать так:

'feed.xml' => 'post/feed',

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

Для всего менеджера можно указать, например, значение .html, и всюду добавится этот суффикс. Например, страница авторизации и запись блога будет открываться теперь по адресу:

http://site.com/site/login.html
http://site.com/post/52.html

А теперь представим, что мы вписали в шаблон правила непосредственно feed.xml и захотели добавить глобальный суффикс .html:

'urlManager'=>array(
    ...
    'urlSuffix' => '.html',
    'rules'=>array(
        'feed.xml' => 'post/feed',
    ),
),

Не обнули мы суффикс для нашего правила, система сгенерировала бы адрес:

http://site.com/feed.xml.html

что не очень хорошо.

Так что эти способы правильны и равнозначны:

'feed.xml' => array('post/feed', 'urlSuffix' => ''),
'feed' => array('post/feed', 'urlSuffix' => '.xml'),
array('template' => 'feed', 'route' => 'post/feed', 'urlSuffix' => '.xml'),
array('template' => 'feed.xml', 'route' => 'post/feed', 'urlSuffix' => ''),

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

Дабы посмотреть все возможные настройки откройте файлы CUrlManager и CUrlRule. Посмотрите имеющиеся у них публичные поля. Любое из них можно настраивать подобным образом. Некоторые параметры мы рассмотрим ниже.

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

Расположение методов createUrl и createAbsoluteUrl

Как видно из настроек компонента, менеджер адресов доступен в приложение как компонент

Yii::app()->urlManager

Метод parseUrl нам вряд ли понадобится вызывать. Его приложение вызывает само для определения того, какое действие какого контроллера нужно выполнить. Метод createUrl есть как в самом менеджере, так и в классах правил. Методы правил мы вручную не вызываем (этим занимается менеджер, обходя по очереди все правила). Соответственно, нам понадобится только метод createUrl менеджера маршрутизации.

Его мы можем вызвать прямо у компонента парой способов:

echo Yii::app()->urlManager->createUrl('post/feed');
echo Yii::app()->getUrlManager()->createUrl('post/feed');

Для удобства использования аналогичный вспомогательный метод есть в классе самого приложения:

class CApplication
{
    ...
    public function createUrl($route, $params=array(), $ampersand='&')
    {
        return $this->getUrlManager()->createUrl($route, $params, $ampersand);
    }
    ...
}

Он позволяет записывать обращение короче:

<a href="<?php echo Yii::app()->createUrl('post/feed'); ?>">RSS</a>

Это правило сгенерирует относительный адрес от корня сайта:

<a href="/feed.xml">RSS</a>

По соседству в менеджере присутствует метод createAbsoluteUrl, который достраивает адрес до абсолютного:

<a href="<?php echo Yii::app()->createAbsoluteUrl('post/feed'); ?>">RSS</a>

Это правило сгенерирует абсолютный адрес:

<a href="http://site.com/feed.xml">RSS</a>

Указание полного адреса бывает полезно для построения карты сайта, адресов в RSS и для встраивания кнопок «Мне нравится» социальных сетей.

Третий способ немного отличается от первых двух. Его мы рассмотрим подробнее.

Метод createUrl контроллера

Третьим способом генерации ссылок является вызов одноимённого вспомогательного метода в контроллере:

class CController
{
    ...
    public function createUrl($route,$params=array(),$ampersand='&')
    {
        if($route==='')
            $route=$this->getId().'/'.$this->getAction()->getId();
        elseif(strpos($route,'/')===false)
            $route=$this->getId().'/'.$route;
        if($route[0]!=='/' && ($module=$this->getModule())!==null)
            $route=$module->getId().'/'.$route;
        return Yii::app()->createUrl(trim($route,'/'),$params,$ampersand);
    }
}

Пока не будем обращать внимания на его содержимое.

Его мы можем вызывать от от контроллера, то есть от указателя $this в контроллере или в представлении:

<a href="<?php echo $this->createUrl('/post/feed'); ?>">RSS</a>

В других компонентах (например, в виджетах) нужно сначала получить сам контроллер, который хранится в Yii::app()->controller, и вызвать метод у него:

<a href="<?php echo Yii::app()->controller->createUrl('/post/feed'); ?>">RSS</a>

или воспользоваться вызовом Yii::app()->createUrl(...).

Теперь рассмотрим назначение добавленного в метод CController::createUrl функционала.

Если ваше приложение использует модули, то можно указать маршруты по умолчанию для них:

'urlManager'=>array(
    'urlFormat' => 'path',
    'showScriptName'=>false,
    'urlSuffix' => '',
    'rules'=>array(
 
        ''=>array('site/index', 'urlSuffix' => ''),
        '<action:login|logout|register>' => 'site/<action>',
        'feed.xml'=>array('blog/post/feed', 'urlSuffix' => ''),
        '<module:\w+>/<controller:\w+>/<id:\d+>' => '<module>/<controller>/view',
        '<module:\w+>/<controller:\w+>/<action:\w+>/<id:\d+>' => '<module>/<controller>/<action>',
        '<module:\w+>/<controller:\w+>/<action:\w+>' => '<module>/<controller>/<action>',
 
    ),
),

Обратите внимание на первое правило. По умолчанию при входе на главную страницу сайта сработает действие по умолчанию index контроллера, указанного в параметре defaultController приложения (по умолчанию это site). Чтобы изменить домашнюю страницу сайта можно либо указать другое значение параметру defaultController в конфигурациооном файле, либо добавить указанное первое правило и указать там свой маршрут.

Метод Yii::app()->createUrl всегда генерирует независимую ссылку по переданному ему маршруту.

Представим, что мы используем модули и у нас есть модуль blog.

Тогда вызов:

echo Yii::app()->createUrl('blog/post/update', array('id'=>1));

сгенерирует ссылку от корневого адреса приложения:

/blog/post/update/1

А метод $this->createUrl(...) находится в контроллере и обучен производить относительную адресацию от текущего модуля, контроллера и действия.

Если вы находитесь в blog/post/index, то, например, запись:

echo $this->createUrl('blog/post/update', array('id'=>1));

сгенерирует то же самое:

/blog/post/update/1

Теперь попробуем поэкспериментировать, убрав часть blog/ и оставив только post/update:

echo $this->createUrl('post/update', array('id'=>1));

Даже в этом случае мы получим такой же адрес.

Продолжим эксперимент:

echo $this->createUrl('update', array('id'=>1));

И сейчас адрес не изменился, хотя мы и оставили только имя действия.

Но поробуем запустить этот же код в действии actionIndex контроллера ProductController модуля ShopModule:

echo $this->createUrl('update', array('id'=>1)) . PHP_EOL; // (1)
echo $this->createUrl('post/update', array('id'=>1)) . PHP_EOL; // (2)
echo $this->createUrl('blog/post/update', array('id'=>1)) . PHP_EOL; // (3)
echo $this->createUrl('', array('id'=>1)) . PHP_EOL; // (4)
echo $this->createUrl('site/login') . PHP_EOL; // (5)
echo $this->createUrl('/site/login') . PHP_EOL; // (6)

Вот такие адреса мы увидим на экране:

/shop/product/update/1
/shop/post/update/1
/blog/post/update/1
/blog/product/index/1
/shop/site/login
/login

Метод CController::createUrl генерирует ссылку относительно текущего действия. Случай (1) сохранил текущий контроллер и модуль и построил новый адрес /shop/product/update/1 (вроде относительной адресации в консоли cd ../update). Случай (2) поднялся от текущего действия на два уровня вверх (как cd ../../post/update) и сохранил только текущий модуль. Соответственно, пустой $this->createUrl('') в случае (3) по принципам относительной адресации сгенерирует ссылку на тот же маршрут shop/product/index.

Относительная маршрутизация удобна для упрощения копировании кода CRUD одного модуля в другой, так как в маршрутах можно не указывать имя модуля. Вызов $this->createUrl('index') будет правильно ссылаться на actionIndex того контроллера, в котором он производится. Кнопки CButtonColumn виджета CGridView, например, используют контекст контроллера:

Yii::app()->controller->createUrl('update', array('id'=>$data->primaryKey));

Это позволяет им по умолчанию ссылаться на действие actionUpdate в любом текущем контроллере любого модуля.

Соответственно, нужно быть осторожным при использовании $this, так как относительная от текущего модуля ссылка $this->createUrl('site/login') в случае (5) может вас из модуля магазина нечаянно завести на несуществующий адрес /shop/site/login. Так что ссылаясь из одного модуля в другой нужно либо использовать беспристрастный Yii::app()->createUrl('site/login'), либо не забыть заставить маршрутизатор генерировать адрес от корня, добавив слэш вначале: $this->createUrl('/site/login'). Как в случае (6) вкупе со вторым правилом маршрутизации:

'<action:login|logout|register>' => 'site/<action>',

Помошник CHtml::link

Для генерации той же ссылки на действие редактирования записи post/update из действия post/admin можно воспользоваться как непосредственным вызовом метода createUrl:

<a href="<?php echo Yii::app()->createUrl('post/update', array('id'=>1)); ?>">Редактировать</a>
<a href="<?php echo $this->createUrl('update', array('id'=>1)); ?>">Редактировать</a>

Так и воспользовавшись помощником CHtml::link:

<?php echo CHtml::link('Редактировать', $this->createUrl('update', array('id'=>1))); ?>
<?php echo CHtml::link('Редактировать', array('update', 'id'=>1)); ?>

В первой строке мы сами сгенерировали адрес и передали этому методу для построения тега <a href="...">...</a>, а во второй мы передали всё необходимое для построения адреса самому помошнику. Это можно сделать в форме массива:

<?php echo CHtml::link('Редактировать', array('update', 'id'=>$model->id)); ?>
<?php echo CHtml::link('Все записи', array('index')); ?>

Рассмотрим код меню из шаблона официального демо-блога:

<div id="mainmenu">
    <?php $this->widget('zii.widgets.CMenu',array(
        'items'=>array(
            array('label'=>'Home', 'url'=>array('post/index')),
            array('label'=>'About', 'url'=>array('site/page', 'view'=>'about')),
            array('label'=>'Contact', 'url'=>array('site/contact')),
            ...
        ),
    )); ?>
</div><!-- mainmenu -->

Мы можем заметить, что адреса элементов для виджетов CMenuCBreadcrumbs тоже) передаются аналогично в виде массива. Внутри этих виджетов для построения ссылок вызывается тот же помошник CHtml::link.

Если изучить исходный код метода CHtml::link, то можно увидеть, что по умолчанию он использует контекст контроллера. Если ему вместо адреса передаётся массив

array('site/page', 'view'=>'about')

то он извлекает первый элемент и использует его как маршрут при вызове метода createUrl контроллера:

Yii::app()->getController()->createUrl('site/page', array('view'=>'about'))

Способы построения адресов и ссылок мы рассмотрели. Перейдём к обзору некоторых дополнительный возможностей.

Дополнительные параметры маршрутизатора CUrlManager

Дополнительные опции маршрутизатора можно найти в самом классе CUrlManager. Рассмотрим здесь некоторые из них.

Предположим, что мы сделали наше приложение и прописали правила маршрутизации. Нас сразу же поджидают некоторые неприятности.

Во-первых, мы можем прописывать в адресной строке любой регистр:

http://site.com/blog/post/1
http://site.com/BLOg/POst/1

На это можно и не обращать внимания, но можно включить строгую слежку за регистром, добавив в параметры компонента urlManager значение 'caseSensitive'=>true.

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

http://site.com/blog/post/1
http://site.com/blog/post/view?id=1
http://site.com/index.php/blog/post/1
http://site.com/index.php?r=/blog/post/1
http://site.com/index.php?r=/blog/post/view?id=1

Также главную страницу сайта можно открыть несколькими способами:

http://site.com/
http://site.com/site
http://site.com/site/index
http://site.com/index.php/site
http://site.com/index.php/site/index
http://site.com/index.php?r=/site
http://site.com/index.php?r=/site/index

Конечно же на самом сайте таких ссылок не будет, так как они у нас будут генерироваться по правилам, но наш сосед (который знает, что мы сделали сайт на Yii) может нарочно надобавлять десятки таких ссылок в свои сообщения на форумах и в Twitter. В итоге в индексе поисковых систем вместо ста страниц сайта будут тысячи копий одних и тех же текстов, что может расцениваться поисковиками как спам.

Чтобы избежать дублирования контента можно заставить менеджер маршрутизации работать только по правилам опцией 'useStrictParsing'=>true.

Если у нас есть правила по умолчанию в таком виде:

'<module:\w+>' => '<module>/default/index',
'<module:\w+>/<controller:\w+>' => '<module>/<controller>/index',
'<module:\w+>/<controller:\w+>/<id:\d+>' => '<module>/<controller>/view',
'<module:\w+>/<controller:\w+>/<action:\w+>/<id:\d+>' => '<module>/<controller>/<action>',
'<module:\w+>/<controller:\w+>/<action:\w+>' => '<module>/<controller>/<action>',

То теперь главная страница блога может открываться только тремя вариантами по первому, второму и пятому правилу:

http://site.com/blog
http://site.com/blog/default
http://site.com/blog/default/index

Чтобы избежать такого дублирования следует отказаться от правил по умолчанию вовсе и прописывать все возможные варианты вручную:

'blog' => 'blog/default/index',
'blog/category/<id:\d+>' => 'blog/default/category',
'blog/tag/<tag:[\w-]+>' => 'blog/default/tag',
'blog/<action:update|delete>/<id:\d+>' => 'blog/post/<action>',
'blog/<action:create>' => 'blog/post/<action>',
'blog/<id:\d+>' => 'blog/post/view',

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

Итак, чтобы избежать дублирования страниц необходимо прописать все возможные варианты адресов вручную и установить 'useStrictParsing'=>true. Может получиться так:

'urlManager'=>array(
    'urlFormat' => 'path',
    'showScriptName' => false,
    'useStrictParsing' => true,
    'caseSensitive' => true,
    'urlSuffix' => '',
    'rules'=>array(
        ...
    ),
),

Прописывание всех правил и включение useStrictParsing – это надёжный, но не единственный вариант. Можно пойти весьма элегантным путём, перенаправляя пользователя со всех неправильных адресов на единственный правильный:

ksort($_GET);
$url = $this->createUrl('', $_GET);
if (Yii::app()->request->getUrl() != $url) {
    $this->redirect($url, 301);
}

Здесь мы собираем правильный адрес (он будет собираться всегда одинаково) и сравниваем его с текущим. Если адреса не совпадают, то делаем редирект на наш правильный.

Дополнительные параметры правила CUrlRule

Из первой части мы помним, что правила можно задавать несколькими способами:

'feed.xml' => 'post/feed',
'feed' => array('post/feed', 'urlSuffix' => '.xml'),
array('template' => 'feed', 'route' => 'post/feed', 'urlSuffix' => '.xml'),

При обработке этих правил менеджер для каждого создаёт экземпляр класса CUrlRule и присваивает его открытым полям urlSuffix, caseSensitive, defaultParams, matchValue, verb, parsingOnly указанные значения.

Мы уже ознакомились с параметрами urlSuffix и caseSensitive менеджера. Внутри правила они работают таким же образом.

Ещё из полей класса CUrlRule в специфических задачах порой используют параметр verb. Он служит для указания метода HTTP-запроса, с которым это правило должно работать.

Это применимо для приобретающей популярность архитектуры REST. Суть её в том, что вместо повсеместного использования форм с method="post" c передачей данных на сервер методом POST воскресают и используются по их исконному прямому назначению все методы GET, PUT, DELETE и POST. При этом они могут ссылаться на один и тот же адрес, но сервером должны обрабатываться в зависимости от типа запроса.

Соответственно, для реализации REST-архитектуры необходимо добавить группу правил для каждого типа запроса:

array('template' => 'blog/posts', 'route' => 'blog/default/index', 'verb' => 'GET'),
array('template' => 'blog/posts/<id:\d+>', 'route' => 'blog/post/view', 'verb' => 'GET'),
array('template' => 'blog/posts/<id:\d+>', 'route' => 'blog/post/update', 'verb' => 'PUT'),
array('template' => 'blog/posts/<id:\d+>', 'route' => 'blog/post/delete', 'verb' => 'DELETE'),
array('template' => 'blog/posts', 'route' => 'blog/post/create', 'verb' => 'POST'),

Теперь в зависимости от метода сработает подходящее ему правило. При необходимости чтобы не копировать одно и то же правило мы можем указывать несколько типов запроса через запятую: 'verb'=>'GET,POST'.

Другие опции используются реже.

Создание своих классов правил

В первой части мы создавали своё правило для маршрутизации статических страниц:

http://site.com/about
http://site.com/services
http://site.com/delivery-information

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

class PageUrlRule extends CBaseUrlRule
{
    public function parseUrl($manager, $request, $pathInfo, $rawPathInfo)
    {
        if (preg_match('#^([\w-]+)#i', $pathInfo, $matches)) {
            $page = Page::model()->find(array(
                'condition' => 'alias = :alias',
                'params' => array('alias'=>$matches[1]),
            ));
            if ($page !== null) {
                $_GET['id'] = $page->getPrimaryKey();
                return 'page/default/view';
            }
        }
        return false;
    }
 
    public function createUrl($manager, $route, $params, $ampersand)
    {
        if ($route == 'page/default/view') {
            if (!empty($params['id'])) {
                if ($page = Page::model()->findByPk($params['id'])) {
                    return $page->alias;
                }
            }
        }
        return false;
    }
}

Теперь добавим наше правило с указанием класса перед правилами по умолчанию:

...
array('class'=>'PageUrlRule'),
'<module:\w+>' => '<module>/default/index',

Это правило сработает только для существующих страницы /about и пропустит мимо себя запрос модуля /blog.

При создании правила мы отнаследовались от класса CBaseUrlRule вместо CUrlRule, поэтому никакие опции вроде urlSuffix и verb у в нашем правиле работать не будут, так как их в нашем классе нет. При желании мы можем самостоятельно добавить любые параметры в свой класс:

class PageUrlRule extends CBaseUrlRule
{
    public $urlSuffix = '';
    public $verb = '';
    ....
}

и реализовать их поддержку вручную в методах parseUrl и createUrl. Например, если urlSuffix не указан, то его по умолчанию можно взять равным значению Yii::app()->urlManager->urlSuffix.

Работа с поддоменами

В шаблонах правил маршрутизации именованные параметры можно использовать в каком угодно месте. Даже при необходимости можно указывать их на месте поддомена сайта. Это может пригодиться, например, если вы захотите разместить профили пользователей на поддоменах:

'http://<username:\w+>.site.com/profile' => 'user/profile',

Так сделано на многих блог-платформах, например LiveJournal.com. Достаточно лишь настроить сервер так, чтобы запросы ко всем поддоменам ссылались на одну директорию с нашим файлом index.php на сервере.

Но как быть с остальными правилами? Ведь если мы перейдём на поддомен пользователя, то с нашими правилами:

''=>array('site/index', 'urlSuffix' => ''),
'<action:login|logout|register>' => 'site/<action>',
 
'blog' => 'blog/default/index',
'blog/<action:update|delete>/<id:\d+>' => 'blog/post/<action>',
'blog/<action:create>' => 'blog/post/<action>',
'blog/<id:\d+>' => 'blog/post/view',
 
'http://<username:\w+>.site.com/profile' => 'user/profile',

мы сможем прочитать блог этого пользователя, но не сможем вернуться обратно на главный поддомен, например на страницу http://www.site.com/login. Чтобы это было возможно нам необходимо будет явно указать основной поддомен www.site.com у необходимых правил:

'http://www.site.com/' => 'site/index',
'http://www.site.com/<action:login|logout|register>' => 'site/<action>',
 
'blog' => 'blog/default/index',
'blog/<action:update|delete>/<id:\d+>' => 'blog/post/<action>',
'blog/<action:create>' => 'blog/post/<action>',
'blog/<id:\d+>' => 'blog/post/view',
 
'http://<username:\w+>.site.com/profile' => 'user/profile',

Теперь на каком бы поддомене мы ни находились, пункт меню

<a href="<?php echo Yii::app()->createUrl('site/register'); ?>">Регистрация</a>

всегда будет вести на основной сайт:

<a href="http://www.site.com/register">Регистрация</a>

Ресурсы по теме

Здесь мы рассмотрели только основные моменты работы с маршрутами в Yii. Дополнительную информацию можно получить из статьи Управление URL официального руководства Yii.

Также могут быть полезными другие материалы на этом сайте:

В любом случае эта тема достаточно обширная. Многие общие и специфические моменты также описаны в книгах по Yii Framework.

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

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

В этот знаменательный день, 15-е ноября, автор данного сайта справляет очень хороший праздник. В общем, у меня отличное настроение и хочется всем дарить подарки. В связи с данным событием на эти два дня объявляю маленький конкурс. Присоединяйтесь!

Каждый разработчик, знакомящийся с каким-либо фреймворком, проходит некий путь знакомства с системой маршрутизации запросов в нём. Данный компонент присутствует во многих системах и служит для использования «красивых» адресов страниц. В этой статье мы познакомимся с работой этой системы.

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

Комментарии

 

Шамшур Иван

Спасибо за подробную статью. Как всегда все правильно и доступно написано. С удовольствием читаю Ваши статьи.

Ответить

 

standalone

Помогите со следующей проблемой. Есть таблица Type (типы сущностей: новости, статьи и т.д.) и таблица Objects (материалы). Хочу сделать урл такого вида: type_slug/id/slug. Т.е, чтобы выборка шла от типа материала. Тут нужно свое правило наверное. Спасибо.

Ответить

 

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

Чтобы не было конфликтов лучше сделать своё правило-класс.

Ответить

 

Tom

Ты походу шаришь в yii чпу.
Подскажи по моему вопросу:
http://www.yiiframework.ru/forum/viewtopic.php?f=19&t=14900&start=10#p92924

Цель - полный контроль контент менеджера над ЧПУ синонимами.
Т.е. не только над хвостовиками типа как со slug, а чтобы от корня синоним задавался.

Ответить

 

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

Аналогично PageUrlRule создаёте свой супер-преобразователь адреса в контроллер-действие и подключаете только его:

array('class'=>'SuperUrlRule');

А он уже по вашей таблице соответствий или ещё как будет извлекать имя нужного контроллера и действия.

Ответить

 

Tom

Так я же про это и писал что я создаю свое правило.

Ваше PageUrlRule написано только для одного пути. И вы знаете что для него нужен параметр 'id'.
А меня интересует как для всех страниц сайта такое сделать.

А насчет таблицы соответствий или еще как - вот в том то и вопрос, что нет никакой таблицы соответствий, брать эту инфу неоткуда.

Скачал я модуль слайдера например.
Есть у него путь

slider/view?idslider=10
actionView($idslider){};

Как я накачу на этот путь синоним через мою общую таблицу синонимов?
Создается путь так:

$url = createUrl('slider/view', ['addonforjs'=> 7, 'idslider' => 5]);

Внутри UrlRule::createUrl я не буду знать по какому запросу искать синоним. Вы то в вашем примере знаете что по 'id'

Ответить

 

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

Переименуйте $idslider в $id в этом и всех других действиях.

Таблицу синонимов сделайте так:

| route         | id  | url        |
------------------------------------
| site/index    |     | /          |
| slider/view   | 10  | /weekend   |
| page/view     | 5   | /about/me  |

Тогда у вас тоже будет поле 'id' для всех действий.
Остальные параметры конкатенируйте через '?' и разбирайте с parse_str().

Ответить

 

Tom

Во первых необходимых параметром может быть не только один, а и 2, или 3,....

Во вторых если бы я их все мог попереименовывать, то вопроса бы не стояло.
Но:
1) Вы что мне предлагаете, для любого модуля который я поставил с package.org сидеть и переименовывать параметры?
2) Параметр может быть и $type к примеру и вместо него использовать $id - некрасиво по коду.

Ответить

 

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

Для определения обязательных параметров используйте рефлексию, а в таблице поле id переделайте на хранение сериализованного массива основных параметров на её основе:

| route         | params         | url         |
------------------------------------------------
| site/index    |                | /           |
| slider/view   | [idslider:10]  | /weekend    |
| shop/view     | [type:5;id:10] | /iphone/5c  |

Потом по адресу находите строку, десереализуйте массив параметров и примешивайте в $_GET.

Ответить

 

Tom

К сожалению рефлексия не подходит.
Получится что при просто создании ссылки мне придется загрузить класс (аутолоад). Это очень накладно. Ссылок то могут быть сотни на странице.

Ответить

 

Tom

да и к тому же этот класс с нужным действием надо найти по роуту (slider/view). По сути все то что при поиске контроллера и действия при разборе запроса.

Ответить

 

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

Ну просчитайте параметры у всех контроллеров один раз и закэшируйте.

Ответить

 

Tom

Ну вот и я о том же. Из-за так сделанного роутинга, проблема ЧПУ превращается в пытку прям какую то, со спорными алгоритмами типа "Пройти по всем модулям и их контроллерам, а в папке контроллера еще в подпапках контроллеры..", и пошло поехало. И потом это все в память грузить.

Ответить

 

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

Ну поищите другую систему, где это есть. Посмотрите как там сделано.

Ответить

 

Tom

Да ну не шутите так. Другую систему. Я в yii столько времени инвестировал. Про эту проблему я случайно узнал, даже как то не думал что такое может быть. В друпал все правильно сделано. В кохане глянул - вроде тоже правильно.
Тут все из-за того что роут состоит только из контроллера/действия.
Странно, что никто этого раньше не замечал.
Даже вон по вашему примеру с PageUrlRule очевидна эта проблема:
Если такое управление адресом ОТ КОРНЯ будет требоваться для разных роутов то придется создавать свои правила типа - PageUrlRule - NewsUrlRule , UsersUrlRule и т.д.
И если эти УРЛ правила будут искать , как у вас сделано, каждый в своей таблице, то не факт же что первое правило найдет соответствие. Станет работать второе, третье...
А это все множество излишних запросов к базе.
Выход один - одна таблица для синонимов для всего сайта.
Распарсить запрос становиться легко.
А для создания ссылок уже и приходим к моей проблеме.

Ответить

 

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

Ну вот и перепишите сам urlManager так, чтобы он как в Cohana работал.

Ответить

 

Tom

urlManager не отвечает за роутинг. Он не передает данные в контроллер, а только парсит их туда-сюда.

Да и переписать его полностью нельзя, т.к. если синонима нет, то он же должен уметь работать с теми путями что по умолчанию.

Ответить

 

cartrege

Здравствуйте!

Спасибо большое за статью, но хотел задать следующий вопрос: есть ли возможность (и если есть, то как) узнать на какой модуль/контроллер/действие ссылается конкретный url?

Если он есть в правилах, то можно посмотреть там, но что, если его нет там?

Приведу простой пример:

Есть модуль admin. У него главный контроллер и действие index/index. При этом роут на /admin не описан в правилах. Можно ли как-нибудь из когда узнать, что роут /admin ссылается на модуль admin, котроллер index, экшн index?

Ответить

 

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

Можно попробовать вызвать CUrlManager::parseUrl.

Ответить

 

cartrege

Да, я смотрел уже в сторону этого метода, но он принимает CHttpRequest в качестве единственного параметра.

Затем (на сколько я понял) смотрит на pathInfo, если urlFormat - path. Дальше бежит по правилам, ищет подходящее. Свойство pathInfo у CHttpRequest доступно только для чтения. Хотя, можно попробовать и самому вручную пробежаться по правилам.

Ответить

 

Akulenok

Подскажите пожалуйста, ставлю 'useStrictParsing'=>true и перестает работать модуль админки.

Например /backend/category работает (index), а /backend/category/create уже нет

Ошибка 404
Невозможно обработать запрос "backend/category/create".
Ответить

 

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

Добавьте правила по умолчанию только для модуля backend:

'<module:backend>' => '<module>/default/index',
'<module:backend>/<controller:\w+>' => '<module>/<controller>/index',
'<module:backend>/<controller:\w+>/<id:\d+>' => '<module>/<controller>/view',
'<module:backend>/<controller:\w+>/<action:\w+>' => '<module>/<controller>/<action>',
Ответить

 

Akulenok

и может подскажите как правильно сделать ссылку в таком виде /category/news
и чтобы обрабатывало как контроллер=post экшин=show id = news

Ответить

 

Дмитрий Елисеев
'category/<id:\w+>' => 'post/show',
Ответить

 

Абай

Здравствуйте Дмитрий! Проконсультируйте пожалуйста, вопрос у меня следующий, как мне организовать проверку вводимого адреса? Т.е. валидацию надо сделать, да, только незнаю как лучше.
Адреса могут быть следующие:
- стандартные маршруты
- /урл_страницы (урл_категория)
- /урл_категория/урл_категория/урл_страницы (вложенные)
- внешний адрес (http://address)
- ссылка на файл.

Ответить

 

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

Регулярными выражениями, например. А потом при совпадении проверяем правильность путей:

if (preg_match(...)) {
    // проверяем наличие контроллера
    // ...
} elseif (preg_match(...)) {
    // проверяем наличие категории
    // ...
} elseif ...

В любом случае оформить это как класс-валидатор.

Ответить

 

Жамшиддин


Помогите пожалуйста разобраться с urlManager...


Нужно из URL вот такого типа:

http://navmarket.uz/announcement/index/6/27/274/language/ru

Сделать URL вот такого типа:

http://navmarket.uz/ru/announcement/index/6/27/274	

Уже пытался настраивать вот так:

'urlManager'=>array(
            'class'=>'application.components.UrlManager',
			'urlFormat'=>'path',
			'rules'=>array(
				'<language:(uz|ru)>/<controller:\w+>/<id:\d+>'=>'<controller>/view',
				'<language:(uz|ru)>/<controller:\w+>/<action:\w+>/<id:\d+>'=>'<controller>/<action>',
				'<language:(uz|ru)>/<controller:\w+>/<action:\w+>'=>'<controller>/<action>',
                '<language:(uz|ru)>/<controller:\w+>/<action:\w+>/<id:\d+>/<idctg:\d+>'=>'<controller>/<action>',
                '<language:(uz|ru)>/<controller:\w+>/<action:\w+>/<id:\d+>/<idctg:\d+>/<idtype:\d+>'=>'<controller>/<action>',
                '<language:(uz|ru)>/<controller:\w+>/<action:\w+>/<id:\d+>/<idctg:\d+>/<idtype:\d+>/<idann:\d+>'=>'<controller>/<action>',
                
				'<language:(uz|ru)>/<module:\w+>' => '<module>',
                '<language:(uz|ru)>/<module:\w+>/<controller:\w+>/<id:\d+>'=>'<module>/<controller>/view',
                '<language:(uz|ru)>/<module:\w+>/<controller:\w+>/<action:\w+>/<id:\d+>'=>'<module>/<controller>/<action>',
                '<language:(uz|ru)>/<module:\w+>/<controller:\w+>/<action:\w+>'=>'<module>/<controller>/<action>',
                '<language:(uz|ru)>/<module:\w+>/<controller:\w+>/<action:\w+>/<id:\d+>/<idctg:\d+>'=>'<module>/<controller>/<action>',
                '<language:(uz|ru)>/<module:\w+>/<controller:\w+>/<action:\w+>/<id:\d+>/<idctg:\d+>/<idtype:\d+>'=>'<module>/<controller>/<action>',
                '<language:(uz|ru)>/<module:\w+>/<controller:\w+>/<action:\w+>/<id:\d+>/<idctg:\d+>/<idtype:\d+>/<idann:\d+>'=>'<module>/<controller>/<action>',
			),
            'showScriptName'=>false,
		),
Ответить

 

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

Странно. Вроде правило

'<language:uz|ru>/<controller:\w+>/<action:\w+>/<id:\d+>/<idctg:\d+>/<idtype:\d+>/<idann:\d+>'=>'<controller>/<action>',

должно подходить.

Ответить

 

Жамшиддин

Не получилось :(

Ответить

 

Akulenok

а как сделать чтобы обрабатывались вот такие ссылки
/blog/kakoeto_imya-1 то есть контрллер blog actionView

Ответить

 

Дмитрий Елисеев
'blog/<name:[\w\d_-]+>-<id:\d+>' => 'blog/view',
Ответить

 

Akulenok

вы не сталкивались с такой проблемой у Ulogin
file_get_contents(): php_network_getaddresses: getaddrinfo failed: Name or service not known

Ответить

 

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

Не сталкивался.

Ответить

 

RichWeber

Здравствуйте!

Нет ли готового примера реализации мультиязычности на Yii2 ?

Ответить

 

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

Не знаю. У меня нет.

Ответить

 

RichWeber

Большое спасибо!

Ответить

 

Дмитрий

Никак не получается настроить URL

есть модуль pages
контроллер default
и экшн view

сейчас url строится
domain.ru/pages/default/view/id/1

хочу чтобы было
domain.ru/about (about - как алиас)

в правилах urlManager пишу

'<alias:\w+>'=>'pages/default/view',

в моделе Pages

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

но не работает.
показывает 400 - некорректный запрос.
доступ к модулю по domain.ru/pages - тоже накрывается

однако когда с id, то работает, как
domain.ru/1/about

Ответить

 

Дмитрий Елисеев
return Yii::app()->createUrl('pages/default/view', array(...));
Ответить

 

Дмитрий

так тоже пробовал не помогало, а вот со своим классом все получилось

Ответить

 

Дмитрий

Спасибо, пропустил пример по созданию своего класса

и сразу же на заметку ошибки: где

'params' => array('alias'=>$matches[1],
if (!empty($params['id'])

пропущены скобки.

Ответить

 

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

Исправил. Спасибо!

Ответить

 

Алексей

Интересная статейка, почерпнул много нового для себя. Мне вот интересно про метод PUT. С Yii ещё плохо знаком и из статьи не понял где вот это прописать и как использовать

array('template' => 'blog/posts/<id:\d+>', 'route' => 'blog/post/update', 'verb' => 'PUT')

Просто стоит задача сделать загрузку на Яндекс.Диск. Там как раз используется метод PUT для загрузки.

Можно подробнее про PUT? Зарание спасибо.

Ответить

 

Илья

Здраствуйте, Дмитрий, Вот вроде роутинг в yii должен быть гибким, а вот у меня ну ни как не получается сделать так чтобы для ссылок statistic/order и statistic/order/564564 было одно правило. Два знаю как сделать, а вот одно бы?! Не подскажете как это сделать?

Ответить

 

Илья

Забыл написать
statistic - контроллер
order - Экшн
564564 - параметр

Ответить

 

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

Проще два.

Ответить

 

Денис

Дмитрий, спасибо вы очень качественно пишете. Многое позаимствовал у вас в блоге. Столкнулся с такой проблемой : согласно правилам в htaccess, если файл например /zzz/xxx.css не найден, то запрос отправляется в index.php. Контрлллера zzz у нас нет и нужно чтобы выдавалась 404 ошибка, а у нас исполняется дефолтный контроллер. Как сделать чтобы если не найден контроллер выдавалась ошибка?

Ответить

 

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

Можно просто не отправлять их на index.php:

RewriteCond %{REQUEST_URI} !\.(css|js|jpe?g|gif|png|ico|txt)$
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . index.php
Ответить

 

Andrey

Добрый день! Очень интересная статья! Можете подсказать, как получить доступ к модели, отправленной в шаблон отображения из контроллера в файле layout/column2.php ? Почему то у меня это получилось сделать только в product/item.php (контроллер/action)

Контроллер - ProductController
Action - actionItem

Ответить

 

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

При рендеринге данные из контроллера передаются в product/item, потом сгенерированный HTML-код присваивается переменной $content, передаваемой в layout/column2. Чтобы что-то перебросить в шаблон можно сохранить это что-то в контроллере ($this). Из готовых вещей подойдут клипы: в представлении присваиваем $this->clips['model'] = $model и в шаблоне используем $this->clips['model'].

Но передача модели – это решение весьма странное. Логичнее всё-таки передавать готовый HTML-код. В шаблоне определить место:

<?php echo $this->clips['myclip']; ?>

а в представлении уже сгенерировать результат с использованием нашей модели и передать его наверх:

<?php $this->beginClip('myclip'); ?>
     <?php $this->widget('MyWidget', array('model' => $model)); ?>
<?php $this->endClip(); ?>
Ответить

 

Andrey

Спасибо огромное. Самый лучший сайт про yii и отзывчивый автор!

Ответить

 

Roman

Подскажите пожалуйста такую историю! написал класс на основе CBaseUrlRule все хорошо работает но появилась нужда создавать ссылки на субдомен! но почему то ссылки создаются

http://mysite.ru/http://subdomen.mysite.ru

как от этого избавиться?

Ответить

 

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

Добавьте в свой класс:

public $hasHostInfo = true;
Ответить

 

kitt

Огромное спасибо за статью, и в частности за эту строчку:

'feed.xml' => array('post/feed', 'urlSuffix' => '')

нашёл решение своей проблемы, когда при установленном 'urlSuffix' => '.html' по умолчанию, вызов CreateAbsoluteUrl('img/image.jpg') генерирует site.com/img/image.jpg.html

Ответить

 

Илья

Спасибо, Дмитрий, за эти две статьи.
Стало все понятно и ясно, благодаря вашим разъяснениям, чего не получалось с официального сайта и тд. Не хватало полной картины этого всего :).

Ответить

 

Николай

Дмитрий, подскажите, пожалуйста, где копать:
Использую useStrictParsing. При переходе по не указанному URL ошибку обрабатывает accessControl и сперва просит авторизоваться. После авторизации пишет ошибку "у вас не достаточно прав..."

Должен, же, по идее перебрасывать на 404...

Ответить

 

Николай

всё оказалось просто - не открыл в accessRules доступ к действию error в siteController

Ответить

 

Andrey

Спасибо!

Подскажите пожалуйста, где писать это:

ksort($_GET);
$url = $this->createUrl('', $_GET);
if (Yii::app()->request->getUrl() != $url) {
    $this->redirect($url, 301);
}
Ответить

 

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

Можно в действии контроллера или даже в его методе beforeAction.

Ответить

 

Andrey

Спасибо!

Ответить

 

Andrey

Этот вариант не будет работать с camelCase?
Например с CategoryProductController и StatusOrderController?

Ответить

 

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

Ну поставьте преобразование:

strtolower(Yii::app()->request->getUrl()) != strtolower($url)
Ответить

 

Andrey

Спасибо, попробую.

Ответить

 

Максим

Есть такой вопрос по rules в yii2

у меня в rules правилах есть такие два правила

'diagnostic/<service:[a-z0-9_\-.]+>'  => 'diagnostic/index',
'diagnostic/<category:[a-z0-9_\-.]+>' => 'diagnostic/index',

и такая загвоздка, в экшене index принимается только параметр service
хоть в ссылке я указываю название параметра
Например:

Url::to(['/diagnostic/index, 'service' => 'string'])
Url::to(['/diagnostic/index, 'category' => 'string'])

в обоих случаях в экшн приходит только service

Ответить

 

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

Правила проверяются сверху вниз, поэтому diagnostic/string подпадает под первый шаблон.

Ответить

 

Максим

Тобишь Yii не смотрит на название параметров, тогда правило

'diagnostic/<service:[a-z0-9_\-.]+>'

и параметр service в Url::to() я могу назвать его как угодно и он все равно его примет?

Ответить

 

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

Смотрит. Если в Url::to укажете category, то cформирует по второму правилу. Но обратно спарсит по первому.

Ответить

 

Максим Никитенко

Извиняюсь не совсем понимаю, подскажите тогда если мне нужно несколько параметров что-бы различать их что есть что, как быть тогда? я это обошел с помощью суфикса типа

'diagnostic/<service:s-[a-z0-9_\-.]+>'

но мне кажется это не совсем хорошо.

Ответить

 

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

Либо добавить префикс:

'diagnostic/service/<service:[a-z0-9_\-.]+>'  => 'diagnostic/service',
'diagnostic/<category:[a-z0-9_\-.]+>' => 'diagnostic/category',

либо cделать составной путь:

'diagnostic/<category:[a-z0-9_\-.]+>/<service:[a-z0-9_\-.]+>'  => 'diagnostic/service',
'diagnostic/<category:[a-z0-9_\-.]+>' => 'diagnostic/category',

либо сделать класс-правило DiagnosticUrlRule и в его методе parseUrl запросом в базу определять, категория это или сервис, и возвращать нужный маршрут.

Ответить

 

Максим Никитенко

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

Ответить

 

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

Если найдётся доброволец, который их в текст под диктовку перепишет, то будут. Не желаете этим заняться?

Ответить

 

Максим Никитенко

В принципе можно, на выходных.

Ответить

 

Ник

Здравствуйте. У меня ошибка 404 Невозможно обработать запрос "blogController/blogj".

А вот кусок кода как я его вызываю.

<?php $form = $this->beginWidget('CActiveForm', array(
    'id' => 'comments-comment_form-form',
    'enableAjaxValidation' => false,
	'action' => Yii::app()->createUrl('//blogController/blogj'),
)); ?>

И когда обрабатываю этот запрос, пишет вышеизложенную ошибку. В конфигах написаны url адреса

'<controller:\w+>/<id:\d+>' => '<controller>/view',
'<controller:\w+>/<action:\w+>/<id:\d+>' => '<controller>/<action>',
'<controller:\w+>/<action:\w+>' => '<controller>/<action>',
Ответить

 

Дмитрий Елисеев
'action' => ['/blog/blogj']
Ответить

 

Roman

Здравствуйте.
Можно ли в роутинге в качестве шаблона использовать регулярное выражение, без указания контроллера. Мне нужно проверить на соответствие весь урл, за исключение домена.
И если не с ложно, примерчик ))

Ответить

 

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

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

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


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



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