DCategoryBehavior: Работа с категориями и списками в Yii

В процессе работы с фреймворком часто приходится запрашивать у моделей всевозможные массивы (для генерации меню, для выпадающих списков в формах поиска или редактирования сущностей), а также находить нужную категорию по составному пути.
Задача генерации списков
Рассмотрим довольно частую необходимость получения ассоциативного массива для выпадающего списка категории. В начале освоения Yii многие используют непосредственную генерацию ассоциативных массивов с нужными полями из выдачи моделей с помощью метода CHtml::listData() прямо в коде представления:
class Category extends CActiveRecord { // ... }
<div class="row"> <?php $list = CHtml::listData(Category::model()->findAll(array('order'=>'title ASC')), 'id', 'title'); echo $form->labelEx($model,'category_id'); <br /> echo $form->dropDownList($model,'category_id', $list); <br /> echo $form->error($model,'category_id'); </div>
Копировать такой код не очень удобно, поэтому логично спрятать эту генерацию в саму модель:
class Category extends CActiveRecord { public function getAssocList() { $models = $this->findAll(array('order'=>'title ASC')); return CHtml::listData(models, 'id', 'title'); } }
<div class="row"> <?php echo $form->labelEx($model,'category_id'); <br /> echo $form->dropDownList($model,'category_id', Category::model()->getAssocList()); <br /> echo $form->error($model,'category_id'); </div>
Этот код уже лучше (не будем здесь обращать внимания на желательный для облегчения метода переход к DAO). Теперь достаточно все модели, которые где-либо фигурируют в выпадающих списках, оснастить методом getAssocList().
Некоторые советуют делать метод getAssocList() статическим, чтобы не создавать лишний экземпляр вызовом Category::model():
class Category extends CActiveRecord { public static function getAssocList() { $models = self::model()->findAll(array('order'=>'title ASC')); return CHtml::listData(models, 'id', 'title'); } } <div class="row"> <?php echo $form->labelEx($model,'category_id'); <br /> echo $form->dropDownList($model,'category_id', Category::getAssocList()); <br /> echo $form->error($model,'category_id'); </div>
Но этот путь намного хуже, так как не поддерживает наследование и не даёт возможности использовать именованные группы условий вида Category::model()->published()->getAssocList().
Это один из часто используемых методов. Рассмотрим теперь другие.
Вложенные категории и ЧПУ
При введении ЧПУ на сайт, то есть при переходе с численных адресов
http://site.com/page/17 - страница http://site.com/shop/category/9 - категория 9 http://site.com/shop/category/15 - категория 15 http://site.com/shop/product/115 - товар
на человекопонятные адреса
http://site.com/page/payment http://site.com/shop/sergi/zoloto/krasnoe-zoloto http://site.com/shop/kolca/obruchalnie/zoloto/beloe-zoloto/s-brilliantom http://site.com/shop/kolca/obruchalnie/zoloto/beloe-zoloto/s-brilliantom/115
в моделях нужно хранить псевдонимы (добавить поле alias), а в контроллерах вместо поиска по идентификатору Category::model()->findByPk($id) нужно придумать и использовать методы поиска по псевдониму findByAlias($alias) или по пути findByPath($path). Метод findByPath() должен разбивать путь kolca/obruchalnie/zoloto/beloe-zoloto/s-brilliantom на элементы и находить нужную модель.
Варианты организации категорий
В определённый момент число моделей категорий начнёт расти. Появятся категории блога, категории магазина, категории портфолио и т.д. с методом getMenuList() для генерации пунктов меню. Для вложенных категорий и вложенных статических страниц нам уже потребовалось ввести свои методы findByPath(). Было бы неплохо сделать несколько удобных методов для построения различных списков. Разброс наборов одинаковых методов по разным моделям засоряет код, поэтому целесообразнее сделать универсальными и собрать все в одном месте. Рассмотрим два варианта.
1. Выделение общих методов в базовый класс
Для объединения общего кода можно выделить базовый абстрактный или конкретный класс Category, от которого наследовать все модели категорий.
abstract class Category extends CActiveRecord { public method findByAlias($alias) { return $this->findByAttributes(array('alias'=>$alias)); } } class ShopCategory extends Category { public static function model($className=__CLASS__) { return parent::model($className); } public function tableName() { return '{{shop_category}}'; } }
В этом случае можно использовать метод ShopCategory::model()->findByAlias() и остальные, реализованные в базовом классе. Но может возникнуть классическая проблема невозможности множественного наследования реализации, если вы вдруг захотите наследовать этот класс одновременно с каким-либо другим.
2. Выделение общих методов в поведение
Идея поведений очень хороша сама по себе, так как не сковывая разработчика никакими ограничениями поведения могут свободно «прилипать» к любой компоненту. Достаточно лишь выделить все необходимые нам методы в класс поведения и при необходимости легко подключать к любой модели. При этом можно также наследовать категории от базового класса:
class Page extends CActiveRecord { public function behaviors() { return array( 'SomeBehavior'=>array( 'class'=>'SomeBehavior', 'titleAttribute'=>'title', 'aliasAttribute'=>'alias' ), ); } } abstract class Category extends CActiveRecord { public function behaviors() { return array( 'SomeBehavior'=>array( 'class'=>'SomeBehavior', 'titleAttribute'=>'title', 'aliasAttribute'=>'alias' ), ); } public function rules(){ // ... } public function attributeLabels(){ // ... } // ... } class BlogCategory extends Category { public static function model($className=__CLASS__){ return parent::model($className); } public function tableName(){ return '{{blog_category}}'; } } class ShopCategory extends Category { public static function model($className=__CLASS__){ return parent::model($className); } public function tableName(){ return '{{shop_category}}'; } }
Поведение для работы с категориями
Предлагаю свою коллекцию часто используемых методов для работы со списками, одноуровневыми и иерархическими категориями. Методы собраны в два поведения: базовое (для простых одноуровневых моделей) и расширенное (для иерархических). Расширенное поведение наследуется от базового, поэтому сразу оба поведения к одной и той же модели подключать не надо.
Это поведение разбито на два класса: DCategoryBehavior для простых категорий и DCategoryTreeBehavior (наследуется от первого) для иерархических. Иерархические категории должны иметь внешний ключ и отношение relation, ссылающиеся на родительскую категорию (то есть быть устроены по принципу Adjacency List). Так как DCategoryTreeBehavior является наследником DCategoryBehavior, для иерархических моделей также доступны и все методы, работающие для простых списков.
Рассмотрим параметры подключения поведения и методы, которые можно использовать в модели:
Поведение DCategoryBehavior
Параметры:
| Атрибут | Описание | По умолчанию |
|---|---|---|
| titleAttribute | Атрибут модели, содержащий название. | title |
| aliasAttribute | Атрибут модели, содержащий псевдоним для составления URL. | alias |
| urlAttribute | Атрибут, содержащий URL адрес категории или страницы. Вы можете либо хранить адрес в поле 'url' модели, либо определить в модели метод `getUrl()`, конструирующий адрес. Поведение будет запрашивать URL модели для генерации массива для меню методом `getMenuList()`. | url |
| linkActiveAttribute | Свойство должно возвращать true если ссылка в меню должна быть активна. При желании Вы моджете переопределить в модели публичный метод `getLinkActive()` или задать $_GET-параметр `requestPathAttribute`, по которому сравнение путей будет производиться автоматически самим поведением. | linkActive |
| requestPathAttribute | $_GET-параметр, по которому встроенный метод `getLinkActive()` определяет активность ссылки в меню при вызове `getMenuList()`. | path |
| defaultCriteria | Параметры выборки, которые будут применяться во всех методах. Используйте, например, `array('order'=>'title')` для сортировки по алфавиту всех выборок. | array() |
Методы:
| Метод | Описание |
|---|---|
| findByAlias($alias) | Замена `findByPk($id)` для поиска модели по псевдониму. |
| getArray() | Возвращает массив идентификаторов всех категорий. |
| getAssocList() | Возвращает ассоциативный массив вида ($id=>$title, $id=>$title, ...) для выпадающих списков. |
| getAliasList() | Возвращает ассоциативный массив вида ($alias=>$title, $alias=>$title, ...). Можно использовать для списков в формах поиска. |
| getUrlList() | Возвращает ассоциативный массив вида ($url=>$title, $url=>$title, ...). Полезен для автоподстановки ссылок в редакторе меню. |
| getMenuList() | Возвращает массив для использования в виджете zii.widgets.CMenu. |
| getLinkActive() | Используется методом `getMenuList()` для апределения активности ссылки в меню. Использует сравнение $_GET-параметра `requestPathAttribute` с текущим псевдонимом. Вы можете легко переопределить этот метод в своей модели. |
Поведение DCategoryTreeBehavior
Этот класс является наследником DCategoryBehavior, поэтому содержит все его методы и свои:
Параметры:
| Атрибут | Описание | По умолчанию |
|---|---|---|
| parentAttribute | Атрибут модели, содержащий идентификатор дочерней категории. | parent_id |
| parentRelation | Отношение, ссылающееся на родительскую категорию. | parent |
Методы:
| Метод | Описание |
|---|---|
| findByPath($path) | Замена `findByPk($id)` для поиска модели по пути. |
| isChildOf($parent)* | Проверка на принадлежность родительской модели. |
| getChildsArray($parent=0)* | Возвращает массив идентификаторов дочерних элементов. |
| getAssocList($parent=0)* | Возвращает массив с полными именами ($id=>$fullTitle, $id=>$fullTitle, ...). |
| getAliasList($parent=0)* | Возвращает массив с полными именами и псевдонимами вместо идентификаторов ($alias=>$fullTitle, $alias=>$fullTitle, ...). |
| getTabList($parent=0)* | Возвращает массив, оформленный с отступами у дочерних категорий ($id=>$title, $id=>$title, ...). |
| getUrlList($parent=0))* | Возвращает ассоциативный массив вида ($url=>$title, $url=>$title, ...). Полезен для автоподстановки ссылок в редакторе меню. |
| getMenuList($sub=0, $parent=0)* | Возвращает массив для виджета zii.widgets.CMenu. Можно указать число вложенных уровней. |
| getPath($separator='/') | Собирает полный путь из псевдонимов. |
| getBreadcrumbs($lastLink=false) | Возвращает массив для виджета for zii.widgets.CBreadcrumbs. Вызовите `getBreadcrumbs(true)` если необходимо добавить ссылку в последний элемент. |
| getFullTitle($inverse=false, $separator=' - ') | Собирает полный заголовок. |
* В качестве аргумента $parent можно использовать идентификатор, их массив или объект модели. Примеры:
- `Model::model()->getChildsArray()`;
- `Model::model()->getChildsArray(5)`;
- `Model::model()->getChildsArray(array(1, 3, 5))`;
- `Model::model()->getChildsArray($model)` равнозначно `$model->getChildsArray()`.
Для работы с этим поведением достаточно скопировать оба класса в любую директорию проекта (например, protected/components) и подключить нужное к любой модели.
class Tag extends CActiveRecord { // ... public function behaviors() { return array( 'CategoryBehavior'=>array( 'class'=>'DCategoryBehavior', 'titleAttribute'=>'title', 'defaultCriteria'=>array( 'order'=>'t.title ASC' ), ), ); } private $_url; // Генрирует URL. Используйте $model->url вместо Yii::app()->createUrl(...); public function getUrl() { if ($this->_url === null) $this->_url = Yii::app()->createUtl('blog/tag', array('tag'=>$this->title); return $this->_url; } // ... } // Модель статической страницы class Page extends CActiveRecord { // ... public function behaviors() { return array( 'CategoryBehavior'=>array( 'class'=>'DCategoryBehavior', 'titleAttribute'=>'title', 'aliasAttribute'=>'alias', 'urlAttribute'=>'url', 'requestPathAttribute'=>'alias', 'defaultCriteria'=>array( 'order'=>'t.title ASC' ), ), ); } private $_url; // Генерирует URL данной страницы. Используйте $model->url вместо Yii::app()->createUrl(...); public function getUrl() { if ($this->_url === null) $this->_url = Yii::app()->request->baseUrl . '/page/' . $this->cache(3600)->getPath() . Yii::app()->urlManager->urlSuffix; return $this->_url; } // ... } // Базовый класс для всех категорий на сайте abstract class Category extends CActiveRecord { // Переопределяется в дочерних классах protected $urlPrefix=''; // ... public function behaviors() { return array( 'CategoryTreeBehavior'=>array( 'class'=>'DCategoryTreeBehavior', 'titleAttribute'=>'title', 'aliasAttribute'=>'alias', 'urlAttribute'=>'url', 'requestPathAttribute'=>'path', 'parentAttribute'=>'parent_id', 'parentRelation'=>'parent', 'defaultCriteria'=>array( 'order'=>'t.title ASC' ), ), ); } public function rules(){ // ... } public function attributeLabels(){ // ... } public function scopes() { return array( 'published'=>array( 'condition'=>'t.public=1', ), ); } private $_url; // Генерирует URL просмотра данной категории. Используйте $model->url вместо Yii::app()->createUrl(...); public function getUrl() { if ($this->_url === null) $this->_url = Yii::app()->request->baseUrl . '/' . $this->urlPrefix . $this->cache(3600)->getPath() . Yii::app()->urlManager->urlSuffix; return $this->_url; } // ... } /* * Перезаписав значение поля urlPrefix в дочернем классе мы избавляемся от * необходимости переписывать метод getUrl() в каждой дочерней модели */ class BlogCategory extends Category { protected $urlPrefix='blog/'; public static function model($className=__CLASS__) { return parent::model($className); } public function tableName() { return '{{blog_category}}'; } public function relations() { return array_merge(parent::relations(), array( 'parent' => array(self::BELONGS_TO, 'BlogCategory', 'parent_id'), )); } }
Использование для выпадающего списка:
<div class="row"> <?php echo $form->labelEx($model, 'category_id'); <br /> echo $form->dropDownList( $model, 'category_id', array_merge( array(''=>'[Select category]'), BlogCategory::model()->published()->getTabList() ) ); <br /> echo $form->error($model, 'category_id'); </div>
Вывод меню:
<h2>Разделы блога:</h2> <?php $this->widget('zii.widgets.CMenu', array( 'items'=>BlogCategory::model()->cache(3600)->getMenuList(10)) ); <h2>Подразделы текущего раздела echo $category->title; :</h2> $this->widget('zii.widgets.CMenu', array( 'items'=>$category->cache(3600)->getMenuList()) );
Рассмотрим более сложный комплексный пример применения некоторых из методов в прототипе интернет-магазина.
Пример использования для каталога товаров
В этих примерах мы будем создавать URL-адреса категории и товаров простой конкатенацией строк. Этот вариант примитивен и не гибок. Кроме того,
ClinkPagerбудет генерировать немного некорректные ссылки (будет кодировать слэши в категориях на спецсимволы). Для более корректной работы с адресами необходимо немного изменитьCUrlManagerи переписать методыgetUrlна использование методаcreateUrlсогласно данной инструкции.
Прописываем маршруты в конфигурационном файле config/main.php:
return array( 'components'=>array( 'urlManager'=>array( 'urlFormat'=>'path', 'showScriptName'=>false, 'rules'=>array( // ... 'shop/<action:cart|order>'=>'shop/<action>', // site.com/shop/printers/home/laser/15 'shop/<path:.+>/<id:\d+>'=>'shop/view', // site.com/shop/printers/home/laser 'shop/<path:.+>'=>'shop/category', 'shop'=>'shop/index', // ... ), ), ), )
Базовый класс и модель категории каталога:
abstract class Category extends CActiveRecord { protected $urlPrefix = ''; // ... public function behaviors() { return array( 'CategoryTreeBehavior'=>array( 'class'=>'DCategoryTreeBehavior', 'titleAttribute'=>'title', 'aliasAttribute'=>'alias', 'urlAttribute'=>'url', 'requestPathAttribute'=>'path', 'parentAttribute'=>'parent_id', 'parentRelation'=>'parent', 'defaultCriteria'=>array( 'order'=>'t.title ASC' ), ), ); } public function rules() { return array( array('title, alias', 'required'), array('title, alias', 'length', 'max'=>255), array('parent_id', 'numerical', 'integerOnly'=>true), ); } public function attributeLabels(){ // ... } public function scopes() { return array( 'published'=>array( 'condition'=>'t.public=1', ), ); } private $_url; // Генерирует URL. Используйте `$model->url` вместо вызова `Yii::app()->createUrl(...)`; public function getUrl() { if ($this->_url === null) $this->_url = Yii::app()->request->baseUrl . '/' . $this->urlPrefix . $this->cache(3600)->getPath() . Yii::app()->urlManager->urlSuffix; return $this->_url; } // ... }
class ShopCategory extends Category { protected $urlPrefix = 'shop/'; public static function model($className=__CLASS__) { return parent::model($className); } public function tableName() { return '{{blog_category}}'; } public function relations() { return array_merge(parent::relations(), array( 'parent' => array(self::BELONGS_TO, 'ShopCategory', 'parent_id'), )); } }
Модель продукта:
class ShopProduct extends CActiveRecord { // ... public function relations() { return array( 'category' => array(self::BELONGS_TO, 'ShopCategory', 'category_id'), ); } private $_url; /* * Генерирует URL страницы просмотра продукта. * Используйте повсеместно запись $model->url вместо вызова Yii::app()->createUrl() */ public function getUrl(){ if ($this->_url === null) $this->_url = Yii::app()->request->baseUrl . '/shop/' . $this->category->path . '/' . $this->id; return $this->_url; } }
Контроллер каталога:
class ShopController extends Controller { public function actionIndex() { $criteria = new CDbCriteria; $criteria->order = 't.id DESC'; $dataProvider = new CActiveDataProvider( ShopProduct::model()->cache(300), array( 'criteria'=>$criteria, 'pagination'=>array( 'pageSize'=>20, 'pageVar'=>'page', ) ) ); $this->render('index', array( 'dataProvider'=>$dataProvider, )); } public function actionCategory($path) { $category = ShopCategory::model()->findByPath($path); if (!$category) throw new CHttpException(404, 'Category not found'); $criteria = new CDbCriteria; $criteria->order = 't.id DESC'; $criteria->addInCondition('t.category_id', array_merge( array($category->id), $category->getChildsArray() )); $dataProvider = new CActiveDataProvider( ShopProduct::model()->cache(300), array( 'criteria'=>$criteria, 'pagination'=>array( 'pageSize'=>20, 'pageVar'=>'page', ) ) ); $this->render('category', array( 'dataProvider'=>$dataProvider, 'category' => $category, )); } public function actionView($id) { $product = ShopProduct::model()->with('category')->findByPk($id); // Защита от зеркал страниц if (Yii::app()->request->requestUri != $product->url) $this->redirect($product->url); if (!$product) throw new CHttpException(404, 'Not found'); $this->render('view', array( 'product'=>$product, )); } }
Представление shop/index.php:
<?php $this->pageTitle = 'Каталог'; $this->breadcrumbs array('Каталог'); <h1>Каталог</h1> <p>Категории:</p> $this->widget('zii.widgets.CMenu', array('items' => ShopCategory::model()->getMenuList())); echo $this->renderPartial('_loop', array('dataProvider'=>$dataProvider));
Представление shop/category.php:
<?php $this->pageTitle = 'Каталог - ' . $category->getFullTitle(); $this->breadcrumbs = array_merge( array( 'Каталог'=>$this->createUrl('shop/index'), ), $category->getBreadcrumbs() ); <h1> echo CHtml::encode($category->title); </h1> <p>Children categories:</p> $this->widget('zii.widgets.CMenu', array('items' => $category->getMenuList())); echo $this->renderPartial('_loop', array('dataProvider'=>$dataProvider));
Представление shop/view.php:
<?php $this->pageTitle = $product->title; $this->breadcrumbs=array( 'Каталог'=>$this->createUrl('shop/index'), ); if ($product->category) $this->breadcrumbs = array_merge( $this->breadcrumbs, $product->category->getBreadcrumbs(true) ); $this->breadcrumbs[]= $product->title; <h1> echo CHtml::encode($product->title); </h1> if ($product->category): <p>Category: echo CHtml::link($product->category->title, $product->category->url); </p> endif; <p>Price: echo number_format($product->price, 0, '.', ' '); </p>
Alisherбольшое спасибо
один вопрос
$criteria->addInCondition('t.category_id', array_merge( array($category->id), $category->getChildsArray() ));Эта чтобы выбрать всех товаров всех подкатегорий данной категории?
Дмитрий ЕлисеевДа, для этого. Ещё не упомянул один полезный трюк:
В форме редактирования древовидных категорий в админке можно воспользоваться разностью массивов array_diff_key:
<div class="row"> <?php echo $form->labelEx($model, 'parent_id'); ?><br /> <?php echo $form->dropDownList($model,'parent_id', array(0=>'') + array_diff_key(BlogCategory::model()->getTabList(), $model->getAssocList())); ?><br /> <?php echo $form->error($model, 'parent_id'); ?> </div>Это исключит из выпадающего списка всех детей текущей категории и её саму, что не даст их зациклить (не получится случайно сделать своим родителем самого себя или своего потомка).
Alisherспасибо
этого не знал
utophyСпасибо! Наконец-то найден нормальный блог по yii
Дмитрий ЕлисеевСпасибо! Я тоже ищу порой что-то серьёзное.
Денис НаталевичРазъясните пожалуйста один момент.
Почему если в DropDownList масив передать:
array('key1'=>'value1','key2'=>'value2')то значения option value задаются такие, как мы указали.
Но если передать, как в вашем примере:
array_merge(array(''=>'[Без категории]'), Category::model()->published()->getTabList())то передаваемые значения option value уже не учитываются и отстёт начинается с нуля
Скриншот: http://clip2net.com/s/4KeMY4
Дмитрий ЕлисеевМожно делать так:
array(''=>'[Без категории]') + Category::model()->published()->getTabList()а можно и так:
$form->dropDownList($model, 'category_id', Category::model()->published()->getTabList(), array('empty'=>'[Без категории]'))
Денис НаталевичСпасибо! Так заработало как надо
Денис НаталевичgetTabList() работает отлично. Понадобился обычный список getMenuList() и возникла проблема.
Ошибка: В классе Group и его поведениях не найден метод или замыкание с именем "getPath".
Посмотрел в файл DCategoryBehavior.php и в нём, в отличие от DCategoryTreeBehavior.php нет метода getPath. Хотя в вашем примере он вызывается
Как быть?
Дмитрий ЕлисеевВ обычном списке в методе getUrl() вашей модели нужно вместо $model->getPath() прямо использовать $model->alias.
Евгений ШвейнДобрый вечер, Дмитрий
Подскажите в чем может быть ошибка, сделал вроде все по вашей инструкции (пример для исполльзования с каталогом товаров), но у меня не получается построить меню из метода "getMenuList", он возвращает пустой массив, так же как и метод "getUrlList".
Модель у меня содержит атрибуты: id, parent_id, title, slug
А так же отношение: parent
Метод getUrl так же добавлен в модель.
Дмитрий ЕлисеевА другие методы работают?
Евгений ШвейнДа, getAssocList, getAliasList, getTabList
getTabList у меня отлично работает в форме добавления/редактирования в drop down поле
Евгений ШвейнНаправите в нужном направлении, в чем может быть проблема?
Дмитрий ЕлисеевА print_r($items) даёт вообще array()? Метод getUrl() в модели публичный?
Евгений ШвейнДолго ковырялся в коде вашего поведения, но увы знаний не хватает для решения проблемы.
Опытным путем в обнимку с CVarDumper выяснилось, что проблема кроется в методе "_getMenuListRecursive".
Метод выдает просто "array()" если так вызывать "getMenuList()", если же вызвать "getMenuList(0,1)", то метод возвращает ветку с родителем "id=1".
Когда в методе "_getMenuListRecursive" вставляю "CVarDumper::dump($items,2,true);", получаю:
array ( '' => array ( 0 => CatalogCategory(...) 1 => CatalogCategory(...) 2 => CatalogCategory(...) ) 1 => array ( 0 => CatalogCategory(...) ) 3 => array ( 0 => CatalogCategory(...) ) )Как я понял, что ошибка в том, что первый элемент массива с пустым ключем, хотя он должен возвращать "0".
Сможете подсказать, в чем же все же дело?
Вот так выглядит моя таблица: http://s018.radikal.ru/i500/1304/6d/c4c4f0978d76.png
Евгений ШвейнХотя зря наверное грешу на "_getMenuListRecursive", так как в методе "getMenuList" CVarDumper::dump($categories,2,true); тоже возвращает:
array ( '' => array ( 0 => CatalogCategory(...) 1 => CatalogCategory(...) 2 => CatalogCategory(...) ) 1 => array ( 0 => CatalogCategory(...) ) 3 => array ( 0 => CatalogCategory(...) ) )
Евгений ШвейнКажется мне, что все это из-за того что у меня атрибут parent_id возвращает null в этом куске кода:
$categories = array(); foreach ($items as $item){ $categories[$item->{$this->parentAttribute}][] = $item; }в методах getUrlList и getMenuList
Дмитрий ЕлисеевА если привести тип [(int)$item->{$this->parentAttribute}], то будет работать?
Дмитрий ЕлисеевОбновил компонент. Добавил во все методы приведение типов для parent_id. Теперь не должно быть проблем с NULL.
Евгений ШвейнДмитрий, огромное вам Спасибо!
Все заработало! Ваши статьи и наработки по Yii это просто чудо!
seedспасибо за полезную инфу.
непонятно по adjacency list;
какого типа должен быть parentRelation? belongs_to, has_one?
и с какой моделью связь? на себя?
Дмитрий ЕлисеевВ приведённых здесь листингах это BELONGS_TO на саму себя:
alexСпасибо за интересную статью и за ваш блог!
К сожалению Ваш репозиторий category-behavior на github временно недоступен.
Дмитрий ЕлисеевДа уж. Половина не открывается. GitHub сейчас глючит:
Denis LEDПомогите довести всё на 100% до конца, заменив:
на
Ведь если в urlManager`е изменить «shop/path:.+/id:\d+» на «shop/path:.+/alias:\w+», то это равносильно «shop/path:.+», и будет вызываться actionCategory и придёться сначало делать проверку ни продукт ли последний элемент $path
Подскажите как это правильнее сделать?
Дмитрий ЕлисеевПроще не убирать «id:\d+» товара, а просто добавить «alias:[\w-]+» после него, то есть отображать товар в виде «shop/path/id/alias», то есть:
Тогда путанницы не будет.
Denis LEDИзменил правило на: 'shop/path:.+/id:\d+/alias:[\w-]+'=>'shop/view',
Ввожу в адресную строку: /shop/teh/phone/21/nokia-c2-03
Получаю тамже: /shop/teh/phone/21 И "error 404 Category not found"
Почему urlmanager обрезает alias?
Дмитрий ЕлисеевА это правило стоит выше shop/path?
Denis LEDДа:
Denis LEDВот ещё примеры:
Дмитрий ЕлисеевДобавьте два правила для 'shop/view':
и в методе ShopProduct::getUrl() допишите прибавление $this->alias.
Иначе сейчас при заходе на
срабатывает ваше правило, но условие
if (Yii::app()->request->requestUri != $product->url) $this->redirect($product->url);перебрасывает редиректом на $product->url, который у Вас до сих пор равен
Denis LEDЗдравствуйте, я вам наверно уже надоел, но у меня опять к вам вопрос :)
Сделал по аналогии ContentCategory и ShopCategory
ShopCategory работает отлично, но ContentCategory перестаёт работать в контроллере на строках:
Если ещё точнее то в DCategoryTreeBehavior.php на строке:
Таблицы для ContentCategory и ShopCategory идентичны
Проверил cached и $criteria и они идентичны, с разницей только имён классов.
Я уже голову поломал, не знаете в чём может быть проблема?
ContentCategory - пусто
ShopCategory - всё как надо
t0os – scriptidy.comВидимо, ContentCategory наследуется не от Category.
Denis LED
t0os – scriptidy.comВ коде выше написано
cinjectВыводит и id родителя.
Смысл их сливать?
$criteria->addInCondition('t.category_id', array_merge( array($category->id), $category->getChildsArray() ));
Дмитрий ЕлисеевИсправил в коде поведения. Теперь не выводит. Спасибо.
cinjectНе за что :)
А почему именно AL? Чем NestedSet не устроил?
Дмитрий ЕлисеевГод назад я особо не вдавался в особенности Nested Sets и нигде не использовал. При желании можно и для этого класс-наследник сочинить, например какой-нибудь
class DCategoryNSBehavior extends DCategoryBehavior {...}записав там публичные методы аналогично методам DCategoryTreeBehavior.
ВиталийА вы можете выложить в конце статьи демо сайт с реализацией того, что вы описывали в этой статье? Я был бы вам безмерно благодарен, и, наверное, не только я.
Дмитрий ЕлисеевМожет быть рассмотрю когда-нибудь пример создания сайта на Yii или Yii2. Но это по обстоятельствам.
НастяА что будет в _loop?
Дмитрий ЕлисеевВывод элементов
<?php $this->widget('zii.widgets.CListView', array( 'dataProvider'=>$dataProvider, 'itemView'=>'_view', )); ?>
Мимо проходилЗдравствуйте!
Я ток начал изучать yii, пытаюсь колдовать с тестовым блогом, хочу прикрутить категории, но не выходит. Данная статья с поведением помогла, даже собрал свой компонент выводящий простой список категорий, но вот прикрутить имеющийся из репозитория в этой статье не выходит, что не делаю, выдаёт ошибку - "В классе BlogCategory и его поведениях не найден метод или замыкание с именем "published". Пробовал и так и так, копировал готовый код, пытался найти проблему, ноль эффекту, ошибка и всё, хотя в базе поле published есть, и так вроде всё норм...
Не могли бы вы выложить архив примера, не весь демо сайт, а чисто файлы по категориям приложения, и дамп базы, что бы можно было по коду глянуть что и где я не так делаю, а то непонятно, то ли в базе какие неточности, то ли я не туда код засунул...
Дмитрий ЕлисеевЭто именованная группа условий. Используется для удобства и записывается как
public function scopes() { return array( 'published'=>array( 'condition'=>'public=1', ), ); }в модели и позволяет пользоваться собой как методом для подмешивания условий в запрос на выборку:
Использовать её необязательно.
Мимо проходилах да.., убрать из запроса "published()", я даже и не подумал. :)
Буду теперь знать для чего, пригодится.
Мимо проходилСпасибо, помогло. Я эту функцию в модели Category вписал, как и в статье, и хоть модель BlogCategory и наследуется от Category, она не работает, по всей видимости потому что модели лежат в модулях(blog и category) а не в обшей категории моделей.
Кинул функцию прямо в BlogCategory и всё стало на свои места...
Спасибо за статью и Behavior.
ДанилЗдравствуйте, спасибо огромное за расширение. Все работает отлично, но так как у меня мало опыта, не могу разобраться, как сделать так, что бы корневая категория не отображалась в path модели, то есть не фигурировала нигде вообще? Спасибо.
ДанилP.S. меню вывожу через getMenuList(), на данный момент ссылки категорий имеют вид http://yiitest/blog/CATEGORIES/Dizayn-v-promyshlennosti
хотелось бы избавиться от CATEGORIES
ДанилПардон, разобрался, просто надо было внимательней читать мануал, в параметрах getmenulist просто задать parent=1
ДанилВсе таки не понятно, может подскажете, как избавиться от корневой категории в пути url? Я так понимаю в методе getpath нужно что-то изменить, но не хватает опыта. подскажите пожалуйста. Тут в цикле как раз я так понял и происходит формирование пути:
while ($i-- && $this->cached($category)->{$this->parentRelation}){ $uri[] = $category->{$this->parentRelation}->{$this->aliasAttribute}; $category = $category->{$this->parentRelation}; }все что мне приходит на ум, перебрать повторно $uri и вырезать root, но может есть другой, более красивый способ?
Дмитрий ЕлисеевА можно убраь категорию CATEGORIES вообще из базы? Или она обязательно нужна?
ДанилЕсли я убираю из базы, возникает циклическая переадресация...
Дмитрий ЕлисеевНо при этом надо parend_id у остальных обнулить.
ДанилСпасибо, все отлично заработало, а я уже в дебри полез.
Сергей – realbsb.ruЭто всё аццке глубоко. Подскажите, от чего оттолкнуться? Знаю, что есть Nested Set, изучаю Yii, но не хотелось бы изобретать велосипед. Если можно, на русском. Спасибо!
Дмитрий ЕлисеевДаже не знаю, что посоветовать. Nested Set, всё-таки, лучше для большого количества категорий.
LiGeRДоброе время суток! Хотел узнать а нельзя ли во view использовать alias вместо id?
Дмитрий ЕлисеевМожно. Только напишите URL-правило так, чтобы оно не пересекалось с правилами категорий.
LiGeRЯ в Yii новичок так что не судите строго. Правило написать в URLManager?
Дмитрий ЕлисеевДа.
Алексей ПарниковЗдравствуйте Дмитрий. Не могу разобраться с getMenuList(). Прочитав все комменты, ни чего для себя не нашел.
А интересуют следующие моменты:
есть поля (исходя из исходников) в бд id, parent_id, title, url, alias ; для чего используется поле url, для описания полного пути?
Пример:
Родитель
id=1 | parent_id=0 | title='Родитель' | url='Мне не понятно что здесь(я поставил roditel)' | alias='roditel'
Вложенный элемент
id=2 | parent_id=1 | title='Вложенный элемент' | url='Мне не понятно что здесь(я поставил vlizhenniy-element)' | alias='vlizhenniy-element'
Если для Родителя url = 'roditel' , а для Вложенного элемента url = 'rodite/vlizhenniy-element' . То по-моему это не хорошо.
При использовании переноса раздела в другой раздел придется менять не только parent_id, но и url.
Возможно я ошибаюсь, что так должно все выглядеть, вот если как многие другие писали посмотреть дамп бд и работу скриптов другое дело.
Вот у меня выбирается $category = Categories::model()->findByPath($path=roditel);
далее я загоняю в Cmenu вот так
$this->widget('zii.widgets.CMenu', array( 'items'=>$category->getMenuList()) );выводит
а по идее должен
Если меняю _getMenuListRecursive() - то все ломается в других местах. Пробовал 'url'=>$item->{$this->urlAttribute} 247 строка , поменять на 'url'=>$item->getPath()
Дмитрий ЕлисеевПоле url в базе хранить не надо. Должен быть метод-геттер getUrl() вроде приведённого:
public function getUrl() { if ($this->_url === null) $this->_url = Yii::app()->request->baseUrl . '/' . $this->urlPrefix . $this->getPath() . Yii::app()->urlManager->urlSuffix; return $this->_url; }Именно он срабатывает при вызове $model->url.
Алексей ПарниковВсе заработало когда я внес корректировки в Ваш код, возможно вы как то по-другому использовали это.
в общем вот
class FjCategotyTreeBehavior extends DCategoryTreeBehavior{ public function getBreadcrumbs($lastLink=false) { if ($lastLink){ $breadcrumbs = array($this->getOwner()->{$this->titleAttribute} => $this->getOwner()->{$this->urlAttribute}); } else { $breadcrumbs = array($this->getOwner()->{$this->titleAttribute}); } $page = $this->getOwner(); $i = 50; while ($i-- && $this->cached($page)->{$this->parentRelation}){ $breadcrumbs[$page->{$this->parentRelation}->{$this->titleAttribute}] = Yii::app()->request->baseUrl."/".$page->{$this->parentRelation}->getPath(); $page = $page->{$this->parentRelation}; } return array_reverse($breadcrumbs); } protected function _getMenuListRecursive($items, $parent, $sub){ $parent = (int)$parent; $resultArray = array(); if (isset($items[$parent]) && $items[$parent]){ foreach ($items[$parent] as $item){ //Fj::d($item->getPath()); $active = $item->{$this->linkActiveAttribute}; $resultArray[$item->getPrimaryKey()] = array( 'id'=>$item->getPrimaryKey(), 'label'=>$item->{$this->titleAttribute}, 'url'=>array($item->getPath()), 'icon'=>$this->iconAttribute !== null ? $item->{$this->iconAttribute} : '', 'active'=>$active, 'itemOptions'=>array('class'=>'item_' . $item->getPrimaryKey()), 'linkOptions'=>$active ? array('rel'=>'nofollow') : array(), ) + ($sub ? array('items'=>$this->_getMenuListRecursive($items, $item->getPrimaryKey(), $sub - 1)) : array()); } } return $resultArray; } }и уже использую это поведение
Виктор КомягинКак-то странно работает. Вроде таблица создана по правилам, а метод getFullTitle возращае одному итему такое:
Аренда автобусов от компании - Аренда автобусов - Транспорт - Транспорт - Транспорт...
Тоже самое касается и вывода пути. Если итем привязан к самой верхней категории, то в урле будет так
/category/category/item_url вместо /category/item_url где category одно и тоже (два раза выводит поле alias из таблицы)
Дмитрий ЕлисеевА чему равны id и parent_id у категории «Транспорт»?
Виктор КомягинТут спасибо за ответ, я немного переделал код, уже не помню что допиливал, но уже работает все как надо. Спасибо!
Виктор КомягинЕсть еще один момент.
Код
protected function _getMenuListRecursive($items, $parent, $sub) { $parent = (int)$parent; $resultArray = array(); if (isset($items[$parent]) && $items[$parent]){ foreach ($items[$parent] as $item){ $active = $item->{$this->linkActiveAttribute}; $resultArray[$item->getPrimaryKey()] = array( 'id'=>$item->getPrimaryKey(), 'label'=>$item->{$this->titleAttribute}, 'url'=>$item->{$this->urlAttribute}, 'icon'=>$this->iconAttribute !== null ? $item->{$this->iconAttribute} : '', 'active'=>$active, 'itemOptions'=>array('class'=>'item_' . $item->getPrimaryKey()), ) + ($sub ? array('items'=>$this->_getMenuListRecursive($items, $item->getPrimaryKey(), $sub - 1)) : array()); } } return $resultArray; }В ключе active всегда будет адрес *URL раздела, а виджет CMenu воспринимает active как true или false поэтому все элементы меню всегда будут активны в данном случае.
Дмитрий ЕлисеевТам linkActiveAttribute, а не linkAttribute.
Виктор Комягинвсмысле? виджет CMenu читает массив, видит, что у итема в active есть что-то и считает это true, делает каждый элемент активным.
Дмитрий ЕлисеевВ linkActiveAttribute содержится имя свойства:
То есть вызов
'active' => $item->{$this->linkActiveAttribute},равносилен
Но так как это свойство, а не поле, то магическим методом вызывается геттер getLinkActive(), который возвращает true или false.
Вы путаете getUrl() и getLinkActive().
Сергей КеримовСделал все, как в приведенных примерах, за исключением имен таблиц.
При переходе по ссылке shop/index получаю ошибку:
Fatal error: Cannot instantiate abstract class CActiveRecord in Z:\home\localhost\www\yii\framework\db\ar\CActiveRecord.php on line 395 Call Stack # Time Memory Function Location 1 0.0000 131776 {main}( ) ..\index.php:0 ... 10 0.0400 2160360 CActiveRecord::model( ) ..\ShopController.php:16В строке 16 ShopController.php
ShopProduct::model()->cache(300),
Дмитрий ЕлисеевА в ShopProduct есть метод model()?
public static function model($className=__CLASS__) { return parent::model($className); }
Максим$this->cache(3600)->getPath()
по факту ничего не кэширует. использую CFileCache и он включён.
и почему-то дебаггер 2 раза заходит в метод getPath в бихевере.
пока не отловил почему.
howПод yii2 будет?
Дмитрий ЕлисеевВозможно.
ev22boxНам очень надо!!! Помочь?
ВикторА что должно хранится в базе в полях url и alias?
В alias - типа category1 или category2 а в url - category1/category2 так?
Дмитрий ЕлисеевДа, в alias хранится именно category1 или category2.
Поля url в таблице быть не должно. Его роль в модели должен выполнять геттер getUrl().
АлександрА есть ли аналогичное расширение на yii2? Было бы очень круто!
КамильДмитрий, есть вопрос.
Вы говорите, что поле URL в БД не должно быть, что формирование должно быть у геттера getUrl().
У меня оно не работает, а вот если я в таблицу добавляют поле URL и там прописываю ( catalog/catalog2 ) то ссылка отлично срабатывает, т.е я прописываю родителя.
Как исправить это. Возможно геттер у меня не срабатывает.
И еще как вывести полные хлебные крошки? Спасибо
КамильНашел такой выход, вроде работает :)
public function getParents($id) { $links = array(); $criteria = $this->getOwnerCriteria(); $criteria->mergeWith(array( 'condition'=>'t.id=:category', 'params'=>array(':category'=>$id) )); $model = $this->cached($this->getOwner())->find($criteria); $links[] = $model->alias; if($model->parent_id !== null){ $links[] = $this->getParents($model->parent_id); } return implode(array_reverse($links), '/'); } public function getPath($separator='/') { $category = $this->getOwner(); $p_key = $this->getOwner()->getPrimaryKey(); $uri[] = $this->getParents($p_key); $i = 10; while ($i-- && $this->cached($category)->{$this->parentRelation}){ $uri[] = $category->{$this->parentRelation}->{$this->aliasAttribute}; $category = $category->{$this->parentRelation}; } return implode(array_reverse($uri), $separator); }
ВикторА как получить список\массив всех родительских элементов?
Дмитрий ЕлисеевОтнаследоваться и дописать метод getParentsList().