Генерация URL для вложенных категорий в Yii
Стандартный класс 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_merge(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_merge(array($category->id), $category->getChildsArray()));
позволяет выбрать товары из текущей категории и всех её дочерних.
Эти методы можно создать в модели самому, а можно подключить для этих целей поведение DCategoryTreeBehavior.
Созданние 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=" echo $this->createUrl('shop/category', array('category'=>$product->category->getPath())); "> echo CHtml::encode($product->category->title); </h2>
Действительно «легко»? Чтобы не запоминать каждый раз маршруты и не путаться в них проще добавить геттер getUrl
в наши модели. Для работы ShopCategory::getPath
возьмём то же поведение DCategoryTreeBehavior:
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=" echo $product->category->url; "> 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.
После этого можно спокойно переносить товар между категориями и не беспокоиться о редиректах.
А как дела с пагинацией и сортировками?
Указывать их через "?"
Если важны ЧПУ, то можно, например, указать все сочетания в маршруте:
Есть несколько способов исправить это неудобство ...
есть 4ый способ, у подключив к CUrlManager поведение (behaviors) в конфиге, добавив например метод createPathUrl()
но вызывать придется так: Yii::app()->urlManager->createPathUrl();
Да, это будет работать, но только при генерации ссылок вручную. Метод createUrl используем не только мы, но и сам Yii. Если мы оставим оригинальный createUrl, то CLinkPager и CSorter (используемые в ClistView и CGridView) будут по-прежнему вызывать его для генерации сортировки и паджинации, и всё сломается.
не подумал. хотя в своем проекте использую вариант изложенный в статье.
Спасибо за статью, очень помогло. Можно вас попросить, не могли бы написать про УРЛ на поддоменах, к примеру пользователей вынести на поддомены, или разделы, или города?
Спасибо. На похожий вопрос я отвечал уже на форуме здесь. Если не сможете разобраться, то могу подробнее с примерами кода изложить.
Отличная статья, равно как и все. Радует что yii описывайте именно Вы. Лаконично, полно, вообщем не просто "лишь бы написать".
Спасибо.
Спасибо за отзыв.
Сделал все как описано, но если категория родительская, то она даже не попадает в метод actionCategory и показывается Error 404, думаю это из-за urlManager, но там все как описано у вас, включая переопределенный класс UrlManager
А какие маршруты для них написаны? Используются ли дефисы или подчёркивания?
Все как у вас, только вместо ShopController у меня CategoryController.
если ссылка /science то получаю 404.
если /science/cosmos то все нормально. где-то на маршрутах вызов спотыкается.
Вроде второе правило должно срабатывать на адреса
А как вы думаете в чем может быть ошибка?
Даже не знаю. Попробуйте переставлять местами эти и другие правила, разрбраться, какой именно контроллер генерирует ошибку 404.
А насколько важна в методе findByPath($path) строчка
$this->parentAttribute . '=0'
Потому что без неё родительские категории работают. В базе данных у меня parent_id определенно как NULL а не 0. Как лучше решить эту проблему?
Чтобы при переходе по адресу /shoes он по слову shoes не нашёл случайно какую-нибудь одноимённую вложенную вроде /man/winter/shoes.
Исправил в поведении этот фрагмент на
Теперь должно работать.
Спасибо. Теперь работает
А как быть с nestedset категориями?
Также организовать в моделях рекурсивные меоды getPath(), findByPath() и подобные.
есть у меня маленькое поведение реализующее findByPath(), getPath() и некоторые другие методы, которое подключается одновременно с NestedSetBehavior. закинул сюда http://codepad.org/XBhyQfxb
з.ы. некоторые моменты не доработаны для универсальности, но всегда же можно допилить)
Абалдеть. Все так классно придумано и устроено, что смог все у себя сделать используя ваши поведения, но со своим контентом и другими адресами, но все почему-то) заработало. Теперь надо разобраться где я так все удачно угадал).
Вы убили сразу два зайца, даже три. И так все коротко и ясно. Теперь мы знаем что такое поведения, как сделать вложенные категории и работать с ЧПУ. Вы гений!
Подскажите пожалуйста, а можно как нибудь избавиться от названия контроллера? Что бы ур выглядел вот так: http://site.com/computers/23
Спасибо
Подскажите, пожалуйста, почему не отрабатывает правило:
если только явно не прописать правило:
где admin_news:
Хотя бы подскажите направление, куда копать.
Сначала определитесь, где admin/news или admin_news у Вас. Что справа, а что слева. Посчитайте число слешэй в правилах.
У меня модуль - admin, контроллер - news и действие - create. Не отрабатывает стандартное правило, в чем может быть проблема, не пойму..
Не срабатывает по адресу /admin/news/create?
Да
Не поможете?
Напишите, какие вообще есть правила до стондартных и какая ошибка отображается.
Ошибка по url: /admin/news/create - "Системе не удалось найти запрашиваемое действие "news"."
если закомментировать
то ошибка, не срабатывает правило последнее правило (module/controller/action)
Так не работает, в этом весь и вопрос..
Если у Вас только один модуль admin, то сделайте так:
front - у меня тоже отдельным модулем (2 модуля)
Тогда для чего нужны правила для простых контроллеров? Вот эти:
Если удалить все правила кроме
выдает ошибку: Невозможно обработать запрос /admin/news/create
$criteria->addInCondition('t.category_id', array($category->id) + $category->getChildsArray());
У меня + затер первый массив с category->id осталось только то что из getChildsArray()!!! пришлось воспользоваться array_merge()
Спасибо! Исправил.
Здравствуйте.
Буду очень благодарен если объясните такой момент. Никак не получается разобраться.
Желаемая структура url:
Такое вообще возможно?
Какой тогда должен быть контроллер с какими экшенами и какие правила писать в urlManager?
Спасибо
Вопрос решил. Сделал так.
Получил свой желаемый вид
Статья хорошая, но мне кажется что тут есть недоработка и давольно таки серьезная. Допустим есть у нас 3 категории вложенные, в поле бд в последней будет url "category1/category2/category3", и тут мы решаем что нужно переименовать "category2" на "test" ее alias автоматически тоже меняется и теперь нам нужно найти ее дочерние категории и все дочерние дочерних и заменить у них url - в донной ситуации мы должны получить "category1/test/category3". А у вас это совсем не упоменается. Как быть в этой ситуации?
У меня нет поля url в БД.
А где вы берете "alias" для каждой из категорий?
Поле alias как раз есть.
Так в "alias" хранится толь псевдоним. А откуда вы берете данные для генерации url - например второго уровня и выше? Или вы используете "Nested Sets" для этого?
Посмотрите на методы getUrl() в статье.
Насколько я понимаю, в методе "ShopProduct->getUrl()" вы обращаетесь к методу "ShopCategory->getPath()" который в свою очередь находится в поведении "DCategoryTreeBehavior", а вот в поведении уже вы в цикле получаете родителей и добавляете их в массив, который в свою очередь преобразовываете в строку - URI.
Правильно я вас понимаю?
Если правильно - тогда вопрос а "Nested Sets" или элементарная генерация URI в момент добавления записи и хранения ее в базе, не эффективнее ли будет?
Nested Sets эффективнее в любом случае.
спасибо. а для yii2 можно пример добавить?
хотя как вариант просто переписать под себя уже готовый виджет Menu и все.
а вот как правильно составить правила для многоуровнего меню?
Делаю категории в action index
подскажите, пожалуйста, как мне в меню указать ссылку на категорию? ($id категории куда дописать?)
У меня есть пара вопросов.
в конфигурации приложения:
везде написано вставьте, а куда конкретно не могу нигде нарыть.
1 какой файл является конфигом приложения? config/web.php
2 это сюда или еще куда?
Дмитрий, а как реализовать такие же красивые ссылки для фильтров(типа: цена, цвет, размер,высота) на странице категории, если все параметры динамически создаются в админке?
В конечном результате хочеться увидить что-то типа
Где computers/laptop - категория и подкатегория
Apple - бренд
Spacegray - цвет
2015 - год выпуска
Через своё правило UrlRule, которое будет в нужной последовательности склеивать и парсить.
Дмитрий, вопрос немного в оффтоп. Не подскажете как реализовать вывод подкатегорий и категорий в Breadcrumbs (категории/подкатегории хранятся в одной таблице) на странице записи в Yii 2?
Огромное спасибо, Дмитрий!
Спасибо за помощь :)
Дмитрий, не подскажите пожалуйста как реализовать генерацию url через отдельный класс с помощью интерфейса UrlRuleInterface когда записи хранятся внутри вложенной категории(третий уровень вложенности) в Yii2? То есть через методы parseRequest($manager, $request) и createUrl($manager, $route, $params).
В обычном правиле rules это выглядит так:
И еще ошибка при таком правиле, что не выводятся картинки с библиотеки yii2-images, хотя путь в атрибуте src у них прописывается. Может регулярные выражения неверно прописаны? Если убрать одну вложенность, то картинки нормально отображаются.
Буду очень признателен за помощь.
Разбиваете путь на части:
и последовательно ищете по этим частям.
Дмитрий, спасибо за отличную статью. Использовал описанный в статье метод вместе с DAO и наткнулся на препятствие при решении вопроса фильтрации товаров в рубриках и дочерних рубриках по свойствам товаров. Для хранения свойств товаров использую две таблицы в одной названия свойства, во второй значения свойств, привязка к товарам и привязка к названию свойства. Не испытываю затруднений в формировании DAO запросов, а вот при передаче значений в действие контроллера возникли затруднения.
Подскажите, каким образом можно фильтровать товары по значениям свойств товаров внутри рубрик при такой организации работы UrlManager? Похожий вопрос задавал Nikolay 26.02.2016 но у меня немного другая ситуация. И если можно раскройте немного ответ который был дан Николаю.
Ответ Николаю - сделать класс, который будет парсить адрес и компоновать его обратно.
А как исключить экшн из правила, чтобы получилось что-то типа
чтобы в урл не дублировалась страница по / и 'site/index
.htaccess не в счет
Здравствуйте, Дмитрий. Подскажите, пожалуйста, как сделать ЧПУ для фильтров на сайте. Неужели нужно все варианты в rules перебрать? И что делать, если не все параметры обязательны? Например, допустим есть четыре фильтра : сортировать по языку (lang), по минимальной сумме (min), по максимальной сумме (max) и по определенной валюте (valuta).
Всё нестандартное можно сделать через класс-правило.
Благодарю за помощь и быстрый ответ )))) попробуем разобраться
Добрый день.
Использую Yii2 - хочу сделать SEO ссылки вида:
мой-сайт/название-категории/название-продукта.html
мой-сайт/nike/nike-air-90.html
Мои UrlManager правила:
Мой контроллер КАТЕГОРИЙ:
Мой контроллер ТОВАРА:
1) Вопрос
Категории у меня работают правильно - то есть открываются по ссылкам вида:
мой-сайт/название-категории
мой-сайт/nike
А вот товары не получается никак реализовать, чтобы сначала шла КАТЕГОРИЯ/ТОВАР вида:
мой-сайт/название-категории/название-продукта.html
мой-сайт/nike/nike-air-90.html
2) Вопрос
Как мне правильно формировать ссылки во view?
Сейчас у меня ссылки вида:
Ссылка на категорию:
<?= Url::to(['/nike'])?> А хочется получать ссылку автоматом.
Ссылка на товар:
мой-сайт/nike-air-90.html
А хочется получать ссылку на товар вместе с категорией вида:
мой-сайт/nike/nike-air-90.html
Разобрался сам вот решение :) Но есть 1 косяк. сначала покажу код:
Теперь ссылки приняли настоящий SEO + ЧПУ вид:
мой-сайт/nike
мой-сайт/nike/nike-air-90.html
А теперь проблема:
Когда я захожу на товар по такой ссылке:
вид такой ссылки получается: http://мой-сайт/nike/nike-air-90.html
Но, если убрать в ссылке Категории любую букву или ее изменить например так:
мой-сайт/ne/nike-air-90.html или мой-сайт/nеkkie-re-ewe/nike-air-90.html
а таких категорий в Базе Данных нет!
То почему то ссылка все равно отрабатывает и показывается! Как я понимаю Категория не проверяется из БД - что очень плохо. Как в этом случае быть подскажите пожалуйста. Ведь если неправильно написать ссылку то должен срабатывать ошибка 404 код в контроллере Категория:
а проверка не отрабатывает - почему?
Такие нестандартные вещи решаются созданием своих классов-правил.
Как можно в yii2 исправить проблему с кодированием слеша '/' в %2F при добавлении LinkPager::widget на вложенную страницу каталога.
Интернет магазин имеет вложенные категории /catalog/muzhskoe в которой много товаров. нужна пагинация. в итоге при формировании url виджетом пагинатора получается catalog%2Fmuzhskoe ?
Можно, напирмер, через свой класс-правило.
Спасибо, разрешил данным способом
Здравствуйте!
При отправке формы у меня создается примерно такой урл:
tagkls.ru/color?form[greenRound]=22&form[greenSquare]=12
могли бы подсказать как изменить имена get-параметров и убрать название формы
(например - form[greenRound] - переименовать в -- greenR), чтобы получилось вот так:
tagkls.ru/color?greenR=22&greenS=12
Спасибо.
В Yii2 в модели формы переопределите метод formName:
Оказывается всё так просто.
Большое спасибо!
Здравствуйте, как сделать так, чтобы тире между двумя параметрами не учитывалось?
Есть правило вида:
После его разбора если города идут через тире или пробел, то ничего не работает.
Если добавить в правило - ( [\w\- ] ) - то города могут некорректно читаться, учитывается следующее тире после параметра.
Ust-kaminsk-ufa - получится fromcity = Ust-kaminsk-ufa, toCity = ''
Создайте класс-правило и парсите в нём.
Подскажите, пожалуйста...
А как получить товары категории с учетом вложенных...
Например по адресу site.com/shop/computers/printers/laser
нужно получить товары, у которых категория = "laser"...
А вот на странице site.com/shop/computers
нужно получить товары из категорий: "computers", "printers", "lasers" и т.д., то есть из текущей и всего дерева вглубь...
Достать все id вложенных категорий и искать товары по всем ['category_id' => $ids].