Стандартный класс CUrlManager в Yii (да и в других фреймворках) позволяет собирать URL динамически на основе правил маршрутизации. В интернет-магазинах и блогах часто используются многоуровневые категории. Попробуем использовать их в Yii.

Правила маршрутизации для категорий

Предположим, что в нашем магазине (блоге, портфолио или в любом другом разделе сайта) нужно обеспечить работу, например, с такими адресами:

http://site.com/shop/computers - категория "Компьютеры"
http://site.com/shop/computers/23 - товар с ID = 23

Для разбора и создания таких адресов нам достаточно добавить в конфигурацию urlManagerтакие правила:

'shop/<action:cart|order>'=>'shop/<action>',
'shop/<category:[\w_-]+>/<id:[\d]+>'=>'shop/show',
'shop/<category:[\w_-]+>'=>'shop/category',
'shop'=>'shop/index',

В первой строке мы предусмотрительно описали действия контроллера, которые не нужно путать с категориями (чтобы по адресу shop/cart мы попадали в корзину, а не в товары категории cart). От этого можно избавиться также предварив адрес категории ключевым словом category. Тогда наши адреса вместо shop/... примут вид shop/category/...:

'shop/category/<category:[\w_\/-]+>/<id:[\d]+>'=>'shop/show',
'shop/category/<category:[\w_\/-]+>'=>'shop/category',
'shop'=>'shop/index',

В любом случае, эти правила позволят работать с нашим контроллером:

class ShopController extends Controller
{
    public function actionIndex()
    {
        // Вывод списка всех товаров
    }
 
    public function actionCategory($category)
    {
        // Вывод списка товаров категории
    }
 
    public function actionShow($category, $id)
    {
        // Отображение страницы товара    
    }
}

Но что делать, если у нас должна быть поддержка вложенных категорий? Разберём такой пример:

http://site.com/shop/computers/printers/laser - вложенная категория "Лазерные принтеры" 
http://site.com/shop/computers/printers/laser/37 - товар с ID = 37

Предыдущие правила для такого случая не подойдут. Категория должна передаваться в контроллер целиком (в виде computers/printers/laser), то есть именованный параметр <category> должен включать в себя и обратный слэш «/». Добавим этот символ в наши шаблоны:

'shop/<action:cart|order>'=>'shop/<action>',
'shop/<category:[\w_\/-]+>/<id:[\d]+>'=>'shop/show',
'shop/<category:[\w_\/-]+>'=>'shop/category',
'shop'=>'shop/index',

В крайнем случае можно разрешить все символы, используя точку:

'shop/<action:cart|order>'=>'shop/<action>',
'shop/<category:.+>/<id:[\d]+>'=>'shop/show',
'shop/<category:.+>'=>'shop/category',
'shop'=>'shop/index',

Заметьте, что жадный шаблон category:[w_/-]+ «проглотит» весь адрес до конца строки, поэтому дополнительные параметры должны либо располагаться в начале адреса в виде shop/тип/...категория...

'shop/<type:\w+>/<category:[\w_\/-]+>/<id:[\d]+>'=>'shop/show',

либо должны быть предварены любым каким-нибудь легко распознаваемым префиксом вродеshop/...категория.../type/... или shop/...категория.../type_..., соответственно правила будут такими

'shop/category/<category:[\w_\/-]+>/type/<type:\w+>'=>'shop/category',

Теперь при переходе по адресу http://site.com/shop/computers/printers/laser мы будем попадать на действие ShopController::actionCategory, параметр $category которого будет содержать путь computers/printers/laser. Остаётся лишь найти нужную модель категории по этому пути и вывести список товаров:

class ShopController extends Controller
{    
    const PRODUCTS_PER_PAGE = 20;
 
    public function actionCategory($category)
    {
        // Ищем категорию по переданному пути
        $category = ShopCategory::model()->findByPath($category);
        if ($category === null)
            throw new CHttpException(404, 'Not found');
 
        $criteria = new CDbCriteria();
        $criteria->addInCondition('t.category_id', array($category->id) + $category->getChildsArray());
 
        $dataProvider = new CActiveDataProvider(ShopProduct::model()->cache(3600), array(
            'criteria'=>$criteria,
            'pagination'=> array(
                'pageSize'=>self::PRODUCTS_PER_PAGE,
                'pageVar'=>'page',
            )
        ));
 
        $this->render('category', array(
            'dataProvider'=>$dataProvider,
            'category'=>$category,
        ));
    }    
 
    public function actionShow($category, $id)
    {
        $model = $this->loadModel($id)
 
        $this->render('show', array('model'=>$model));            
    }
}

Здесь мы воспользовались методом findByPath и getChildsArray модели ShopCategory. Условие

$criteria->addInCondition('t.category_id', array($category->id) + $category->getChildsArray());

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

Эти методы можно создать в модели самому, а можно подключить для этих целей поведение.

Созданние URL для вложенных категорий

Воспользуемся стандартным методом CUrlManager::createUrl или CUrlManager::createAbsoluteUrlдля сборки адреса для категории computers:

echo $this->createAbsoluteUrl('shop/index'); 
echo $this->createAbsoluteUrl('shop/category', array('category'=>'computers')); 
echo $this->createAbsoluteUrl('shop/show', array('category'=>'computers', 'id'=>12));

Мы получим адреса в полном соответствии с заданными нами маршрутами:

http://site.com/shop
http://site.com/shop/computers
http://site.com/shop/computers/12

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

echo $this->createAbsoluteUrl('shop/category', array('category'=>'computers/printers/laser')); 
echo $this->createAbsoluteUrl('shop/show', array('category'=>'computers/printers/laser', 'id'=>12));

Мы получим не совсем то, что хотели:

http://site.com/shop/computers%2Fprinters%2Flaser
http://site.com/shop/computers%2Fprinters%2Flaser/12

Это происходит из-за того, что все параметры метод CUrlRule::createUrl кодирует функцией urlencode. Соответственно, в нашей категории перекодируются и все слэши.

Есть несколько способов исправить это неудобство:

  • Создать класс ShopUrlRule и использовать его вместо маршрутов;
  • Переопределить CUrlManager и убрать из его метода createUrl экранирование слэшей;
  • Не использовать createUrl для создания адресов, а конкатенировать их вручную.

Первый способ требует некоторых усилий, но он не универсальный, так как нужно будет создавать клоны этого класса для каждого раздела сайта. Третий способ не подойдёт, так как createUrl требуется для генерации ссылок на страницы виджетом CListPager.

Рассмотрим второй способ. Создадим класс-декоратор UrlManager, который будет заменять код «%2F» обратно на слэш:

class UrlManager extends CUrlManager
{
    public function createUrl($route, $params=array(), $ampersand='&')
    {
        return $this->fixPathSlashes(parent::createUrl($route, $params, $ampersand));
    }
 
    protected  function fixPathSlashes($url)
    {
        return preg_replace('|\%2F|i', '/', $url);
    }
}

Это весь код. Нужно указать наш класс UrlManager в параметре class конфигурации компонента Yii::app()->urlManager:

return array(
    'components'=>array(
        'urlManager'=>array(
            'class'=>'UrlManager',
            'urlFormat'=>'path',
            'showScriptName'=>false,
            'rules'=>array(
 
                'shop/<category:[\w_\/-]+>/<id:[\d]+>'=>'shop/show',
                'shop/<category:[\w_\/-]+>'=>'shop/category',
                'shop'=>'shop/index',
                // ...
 
            ),
        ),
    ),
);

Теперь подобные адреса у нас будут строиться правильно на всём сайте.

Упрощение создания адресов

Как известно, в представлениях мы можем использовать $this->createUrl, а в виджетах и моделях Yii::app()->createUrl или Yii::app()->controller->createUrl для создания адресов ссылок.

Если у нас есть модель

class ShopProduct extends CActiveRecord
{    
    public function relations()
    {
        return array(
            'category' => array(self::BELONGS_TO, 'ShopCategory', 'category_id'),
        );
    }
}

и если у категории есть метод getPath(), который генерирует полную строку вида parent/parent/category, то в представлении мы можем «легко» генерировать адреса ссылок:

<h2><a href="<?php echo $this->createUrl('shop/show', array('category'=>$product->category->getPath(), 'id'=>$product->id)); ?>"><?php echo CHtml::encode($product->title); ?></h2>
<p>Категория: <a href="<?php echo $this->createUrl('shop/category', array('category'=>$product->category->getPath())); ?>"><?php echo CHtml::encode($product->category->title); ?></h2>

Действительно «легко»? Чтобы не запоминать каждый раз маршруты и не путаться в них проще добавить геттер getUrl в наши модели. Для работы ShopCategory::getPath возьмём то же поведение:

class ShopCategory extends CActiveRecord
{    
    public function behaviors()
    {
        return array(
            'CategoryBehavior'=>array(
                'class'=>'DCategoryTreeBehavior',
                'titleAttribute'=>'title',
                'aliasAttribute'=>'alias',
                'parentAttribute'=>'parent_id',
                'parentRelation'=>'parent',
                'requestPathAttribute'=>'category',
                'defaultCriteria'=>array(
                    'order'=>'t.sort ASC, t.title ASC'
                ),
            ),
        );
    }
 
    private $_url;
 
    public function getUrl()
    {
        if ($this->_url === null)
            $this->_url = Yii::app()->createUrl('shop/category', array('category'=>$this->cache(3600)->getPath()));
        return $this->_url;
    }
}
class ShopProduct extends CActiveRecord
{    
    public function relations()
    {
        return array(
            'category' => array(self::BELONGS_TO, 'ShopCategory', 'category_id'),
        );
    }
 
    private $_url;
 
    public function getUrl()
    {
        if ($this->_url === null)
            $this->_url = Yii::app()->createUrl('shop/show', array('category'=>$this->category->cache(3600)->getPath(), 'id'=>$this->id));
        return $this->_url;
    }
}

Теперь можно использовать метод $model->getUrl() или эквивалентное свойство $model->url. Код представления предельно упрощается:

<h2><a href="<?php echo $product->url; ?>"><?php echo CHtml::encode($product->title); ?></h2>
<p>Категория: <a href="<?php echo $product->category->url; ?>"><?php echo CHtml::encode($product->category->title); ?></h2>

Это теперь можно использовать для защиты от дублирования адресов товаров:

class ShopController extends Controller
{        
    public function actionShow($category, $id)
    {
        $model = $this->loadModel($id)
 
        if (Yii::app()->request->getUrl() != $model->url)
            $this->redirect($model->url, true, 301);        
 
        $this->render('show', array('model'=>$model));            
    }
}

Теперь если будет неправильно указана категория

http://site.com/shop/computers/printers/laser/37
http://site.com/shop/computers/laser/37
http://site.com/shop/anypath/37

то произойдёт перенаправление на правильный URL.

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

Взято отсюда

Комментарии

comments powered by Disqus