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

Утверждено

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

Задача генерации списков

Рассмотрим довольно частую необходимость получения ассоциативного массива для выпадающего списка категории. В начале освоения Yii многие используют непосредственную генерацию ассоциативных массивов с нужными полями из выдачи моделей с помощью метода CHtml::listData() прямо в коде представления:

class Category extends CActiveRecord
{
    // ...
}
<?php
<div class="row">
    <?php 
        $list = CHtml::listData(Category::model()->findAll(array('order'=>'title ASC')), 'id', 'title');
    ?>
    <?php echo $form->labelEx($model,'category_id'); ?><br />
    <?php echo $form->dropDownList($model,'category_id', $list); ?><br />
    <?php 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');
    }
}
<?php
<div class="row">
    <?php echo $form->labelEx($model,'category_id'); ?><br />
    <?php echo $form->dropDownList($model,'category_id', Category::model()->getAssocList()); ?><br />
    <?php 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 />
    <?php echo $form->dropDownList($model,'category_id', Category::getAssocList()); ?><br />
    <?php 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}}';
    }
}

Поведение для работы с категориями

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

Код на GitHub

Это поведение разбито на два класса: 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'),
        ));
    }   
}

Использование для выпадающего списка:

<?php
<div class="row">
    <?php echo $form->labelEx($model, 'category_id'); ?><br />
    <?php echo $form->dropDownList(
        $model,
        'category_id',
        array_merge(
            array(''=>'[Select category]'), 
            BlogCategory::model()->published()->getTabList()
        )
    ); ?><br />
    <?php echo $form->error($model, 'category_id'); ?>
</div>

Вывод меню:

<?php
<h2>Разделы блога:</h2>
<?php $this->widget('zii.widgets.CMenu', array(
    'items'=>BlogCategory::model()->cache(3600)->getMenuList(10))
); ?>
 
<h2>Подразделы текущего раздела <?php echo $category->title; ?>:</h2>
<?php $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
<?php
$this->pageTitle = 'Каталог';
$this->breadcrumbs array('Каталог');
?>
 
<h1>Каталог</h1>
 
<p>Категории:</p>
<?php $this->widget('zii.widgets.CMenu', array('items' => ShopCategory::model()->getMenuList()));?>
 
<?php echo $this->renderPartial('_loop', array('dataProvider'=>$dataProvider)); ?>

Представление shop/category.php:

<?php
<?php
$this->pageTitle = 'Каталог - ' . $category->getFullTitle();
$this->breadcrumbs = array_merge(
    array(
        'Каталог'=>$this->createUrl('shop/index'),
    ), 
    $category->getBreadcrumbs()
);
?>
 
<h1><?php echo CHtml::encode($category->title); ?></h1>
 
<p>Children categories:</p>
<?php $this->widget('zii.widgets.CMenu', array('items' => $category->getMenuList()));?>
 
<?php echo $this->renderPartial('_loop', array('dataProvider'=>$dataProvider)); ?>

Представление shop/view.php:

<?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><?php echo CHtml::encode($product->title); ?></h1>
 
<?php if ($product->category): ?>
    <p>Category: <?php echo CHtml::link($product->category->title, $product->category->url); ?></p>
<?php endif; ?>
 
<p>Price: <?php 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 на саму себя:

'parent' => array(self::BELONGS_TO, 'Category', 'parent_id'),
Ответить

 

alex

Спасибо за интересную статью и за ваш блог!
К сожалению Ваш репозиторий category-behavior на github временно недоступен.

Ответить

 

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

Да уж. Половина не открывается. GitHub сейчас глючит:

9:59 UTC: We are investigating issues with one of our fileserver pairs. 
A small number of respositories may be affected.
Ответить

 

Denis LED

Помогите довести всё на 100% до конца, заменив:

/shop/kolca/obruchalnie/s-brilliantom/115 //id=115

на

/shop/kolca/obruchalnie/s-brilliantom/kolcoAlias //alias="kolcoAlias"

Ведь если в urlManager`е изменить «shop/path:.+/id:\d+» на «shop/path:.+/alias:\w+», то это равносильно «shop/path:.+», и будет вызываться actionCategory и придёться сначало делать проверку ни продукт ли последний элемент $path

Подскажите как это правильнее сделать?

Ответить

 

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

Проще не убирать «id:\d+» товара, а просто добавить «alias:[\w-]+» после него, то есть отображать товар в виде «shop/path/id/alias», то есть:

/shop/kolca/obruchalnie/s-brilliantom/115/kolcoAlias

Тогда путанницы не будет.

Ответить

 

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

Да:

'shop/<path:.+>/<id:\d+>/<alias:[\w-]+>'=>'shop/view',
'shop/<path:[\w_\/-]+>'=>'shop/category',
'shop'=>'shop/index',
Ответить

 

Denis LED

Вот ещё примеры:

url: /shop/teh/mob/24/samsungs3650corby
Правило: 'shop/<path:.+>/<id:\d+>/<alias:[\w-]+>'=>'shop/view',
Результат: /shop/teh/mob/24
url: /shop/teh/mob/24-samsungs3650corby
Правило: 'shop/<path:.+>/<id:\d+>-<alias:[\w-]+>'=>'shop/view',
Результат: /shop/teh/mob/24
url: /shop/teh/mob/24_samsungs3650corby
Правило: 'shop/<path:.+>/<id:\d+>_<alias:[\w-]+>'=>'shop/view',
Результат: /shop/teh/mob/24
url: /shop/teh/mob/24samsungs3650corby
Правило: 'shop/<path:.+>/<id:\d+><alias:[\w-]+>'=>'shop/view',
Результат: /shop/teh/mob/2
Ответить

 

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

Добавьте два правила для 'shop/view':

'shop/<path:.+>/<id:\d+>/<alias:[\w-]+>'=>'shop/view',
'shop/<path:.+>/<id:\d+>'=>'shop/view',
'shop/<path:[\w_\/-]+>'=>'shop/category',
'shop'=>'shop/index',

и в методе ShopProduct::getUrl() допишите прибавление $this->alias.

Иначе сейчас при заходе на

/shop/teh/mob/24/samsungs3650corby

срабатывает ваше правило, но условие

if (Yii::app()->request->requestUri != $product->url) 
    $this->redirect($product->url);

перебрасывает редиректом на $product->url, который у Вас до сих пор равен

/shop/teh/mob/24
Ответить

 

Denis LED

Здравствуйте, я вам наверно уже надоел, но у меня опять к вам вопрос :)

Сделал по аналогии ContentCategory и ShopCategory
ShopCategory работает отлично, но ContentCategory перестаёт работать в контроллере на строках:

$category = ContentCategory::model()->findByPath(path);
if (!$category) throw new CHttpException(404, 'ContentCategory not found');

Если ещё точнее то в DCategoryTreeBehavior.php на строке:

$model = $this->cached($this->getOwner())->find($criteria);

Таблицы для ContentCategory и ShopCategory идентичны
Проверил cached и $criteria и они идентичны, с разницей только имён классов.

Я уже голову поломал, не знаете в чём может быть проблема?

$model = $this->cached($this->getOwner())->find($criteria); 
throw new CHttpException(404, print_r($model)); 

ContentCategory - пусто
ShopCategory - всё как надо

Ответить

 

t0os – scriptidy.com

Видимо, ContentCategory наследуется не от Category.

Ответить

 

Denis LED
class ContentCategory extends CActiveRecord
class ShopCategory extends CActiveRecord
Ответить

 

t0os – scriptidy.com

В коде выше написано

class ShopCategory extends Category
Ответить

 

cinject
getChildsArray()

Выводит и 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',
        ),
    );
}

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

BlogCategory::model()->published()->findAll();

Использовать её необязательно.

Ответить

 

Мимо проходил

ах да.., убрать из запроса "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())
);

выводит

<ul id="yw0">
<li class="item_5"><a href="/vlizhenniy-elementl">Вложенный элемент</a></li>
</ul>

а по идее должен

<ul id="yw0">
<li class="item_5"><a href="/roditel/vlizhenniy-elementl">Вложенный элемент</a></li>
</ul>

Если меняю _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 содержится имя свойства:

public $linkAttribute = 'url';
public $linkActiveAttribute = 'linkActive';

То есть вызов

'active' => $item->{$this->linkActiveAttribute},

равносилен

'active' => $item->linkActive,

Но так как это свойство, а не поле, то магическим методом вызывается геттер 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().

Ответить

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

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


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





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