Ми}{@лbI4

Блог хеллоуворлдщика

Работа с несколькими моделями и отображение их в CGridView

18.03.2014 yii, cgridview, active record

Иногда, или даже, чаще всего возникает потребность использовать несколько моделей в CGridView. В сети есть не мало решений связанные с использованием нескольких моделей в CGridView. Есть хорошие и есть плохие. Я решил разобраться в этом вопросе. Мы рассмотрим чужие костыли, решение через behavior и мой костыль.

Например, метод 1

Фрагмент класса модели:

class Sample extends CActiveRecord {
  
        ...
  
        public $product_name;//for filtering
  
        ...
  
        public function rules() {
                return array(
                        ...
  
                        array('id, product_name, ...', 'safe', 'on'=>'search'),
                );
        }
  
        public function relations() {
                return array(
                        ...
  
                        'product' => array(self::BELONGS_TO, 'Products', 'productid'),
                );
        }
  
        ...
  
        public function search() {
                $criteria=new CDbCriteria;
  
                $criteria->compare('t.id', $this->id);
                $criteria->compare('t.productid', $this->product_name);
  
                ...
  
                $criteria->with = array('product');
  
                return new CActiveDataProvider(get_class($this), array(
                        'criteria'=>$criteria,
                        'sort'=>array(
                                'attributes'=>array(
                                        'id',
                                        'product_name'=>array(
                                                'asc'=>'lower(product.name)',
                                                'desc'=>'lower(product.name) DESC',
                                        ),
                                        ...
                                ),
                                'defaultOrder'=>array('id'=>true),
                        ),
                ));
        }
  
...
}

Фрагмент view:

$this->widget('zii.widgets.grid.CGridView', array(
                'dataProvider'=>$model->search(),
                'filter'=>$model,
                'columns'=>array(
                        array (
                                'name'=>'id',
                                'filter'=>CHtml::activeTextField($model, 'id', array('size'=>5)),
                        ),
                        array(
                                'header'=>'Product',
                                'name'=>'product_name',
                                'value'=>'$data->product->name',
                                'type'=>'raw',
                                'filter'=>CHtml::listData(Products::model()->findAll(), 'id', 'name'),
                        ),
  
                        ...
                ),
        ));

Метод 2:

http://hashcode.ru/questions/200684/php-yii-как-передать-данные-нескольких-моделей-в-cgridview

И посмотрев на метод 2, я решил копнуть глубже и посмотреть, как это всё работает.

Мой труЪ-мазафака-метод:

Часть из модели:

/**
 * @var array - Array virtual properties where the key is name column in  * table and the value is name relation with this field in class.
 */
private $_virtualProperties = array(
    'first_name' => 'profile',
    'second_name' => 'profile',
);
  
/**
 * @var array - The value each from properties certain in {@link $_virtualProperties}.
 */
private $_dataVirtualProperties = array();
  
/**
 * PHP getter magic method.
 * This method is overridden property which contained in other tables,
 * but can call using virtual property list which define
 * in {@link $_virtualProperties} and get their value.
 * @param string $name - property name
 * @return mixed - property value
 */
public function __get($name)
{
    if (!isset($this->_virtualProperties[$name]))
    {
        return parent::__get($name);
    }      
     
    $table = $this->_virtualProperties[$name];
    if (!$this->_dataVirtualProperties[$name] && $this->$table)
    { 
        $this->_dataVirtualProperties[$name] = $this->$table->$name;
    }
  
    return $this->_dataVirtualProperties[$name];       
}
  
/**
 * PHP magic method.
 * Calling this method allows determine availability the
 * properties in array {@link $_virtualProperties} and call getter for get value property.
 *
 * @param string $name - property name
 * @return mixed - property value
 */
public function __isset($name)
{
    if (!isset($this->_virtualProperties[$name]))
    {
        return parent::__isset($name);
    }      
  
    return $this->$name;
}
  
/**
 * PHP magic method.
 * Setter a values the property which define in {@link $_virtualProperties} if this property there in array.
 * @param string $name - property name
 * @param mixed $value - propery value
 * @return void
 */
public function __set($name, $value)
{
    if (!isset($this->_virtualProperties[$name]))
    {
        parent::__set($name, $value);
         
        return;
    }
  
    $this->_dataVirtualProperties[$name] = $value;
}
  
  
  
/**
 * @return array validation rules for model attributes.
 */
public function rules()
{
    return array(
         
        array('first_name,second_name', 'safe', ),
         
    );
}
  
/**
 * @return array relational rules.
 */
public function relations()
{
    return array(
        'profile' => array(self::HAS_ONE, 'Profile', 'user_id'), -------> допустим что есть такая связь
    );
}
  
/**
 * Retrieves a list of models based on the current search/filter conditions.
 *
 * Typical usecase:
 * - Initialize the model fields with values from filter form.
 * - Execute this method to get CActiveDataProvider instance which will filter
 * models according to data in model fields.
 * - Pass data provider to CGridView, CListView or any similar widget.
 *
 * @return CActiveDataProvider the data provider that can return the models
 * based on the search/filter conditions.
 */
public function search()
{
    $criteria = new CDbCriteria();
     
    $criteria->with = array('profile');
  
     
    $criteria->compare('profile.first_name', $this->first_name, true);    ->>>> здесь название колонки я указал через точку <имя_связи_для_таблицы>.<имя_колонки>, для того чтоб не произошло конфликтов имён
    $criteria->compare('profile.second_name', $this->second_name, true);
     
  
    $sort = new CSort();
    $sort->attributes = array(
         
        'first_name' => array(
            'asc' => 'profile.first_name',
            'desc' => 'profile.first_name DESC',
        ),
        'second_name' => array(
            'asc' => 'profile.second_name',
            'desc' => 'profile.second_name DESC',
        ),
         
        '*',
    );
     
    return new CActiveDataProvider($this, array('criteria' => $criteria, 'sort' => $sort));
}

Часть из view:

$this->widget(
    'zii.widgets.grid.CGridView',
    array(
        'id' => 'users-grid',
        'dataProvider' => $model->search(),
        'filter' => $model,
        'columns' => array(
            ,
            'first_name',       --->>>> здесь пишем название колонок как будто они существуют в родительской модели
            'second_name',
             
    ),
));

И вроде бы всё хорошо, можно работать, но не тут то было! Если нам понадобиться отображать поля из нескольких моделей, где поля имеют одинаковое название, то данное решение работать из коробки не будет. Поэтому мы сделаем так:

Часть модели:

   /**
     * @var array - Array virtual properties where the key is name column in      * table and the value is name relation with this field in class.
     */
    private $_virtualProperties = array(//данне введены для примера
        'author' => 'user',
        'category' => 'categories',
    );
     
    /**
     * @var array - The value each from properties certain in {@link $_virtualProperties}.
     */
    private $_dataVirtualProperties = array();
     
    /**
     * @var array Aliases for use with multiple tables where the fields are the same.
     * Where key - alias of field in {@link $_virtualProperties}, Where value - real name of field.
     */
    private $_aliasForProperties = array(//данне введены для примера
        'author' => 'username',
        'category' => 'title',
    );
     
    /**
     * PHP getter magic method.
     * This method is overridden property which contained in other tables,
     * but can call using virtual property list which define
     * in {@link $_virtualProperties} and get their value.
     * @param string $name - property name
     * @return mixed - property value
     */
    public function __get($name)
    {
        if (!isset($this->_virtualProperties[$name]))
        {
            return parent::__get($name);
        }      
         
        $table = $this->_virtualProperties[$name];
        if (!$this->_dataVirtualProperties[$name] && $this->$table)
        { 
            if (isset($this->_aliasForProperties[$name]))   //added alias for field
            {
                $nameInTable = $this->_aliasForProperties[$name];
            }
            $this->_dataVirtualProperties[$name] = $this->$table->$nameInTable;
        }
  
        return $this->_dataVirtualProperties[$name];       
    }
     
    /**
     * PHP magic method.
     * Calling this method allows determine availability the
     * properties in array {@link $_virtualProperties} and call getter for get value property.
     *
     * @param string $name - property name
     * @return mixed - property value
     */
    public function __isset($name)
    {
        if (!isset($this->_virtualProperties[$name]))
        {
            return parent::__isset($name);
        }  
         
        return $this->$name;
    }
     
    /**
     * PHP magic method.
     * Setter a values the property which define in {@link $_virtualProperties} if this property there in array.
     * @param string $name - property name
     * @param mixed $value - propery value
     * @return void
     */
    public function __set($name, $value)
    {
        if (!isset($this->_virtualProperties[$name]))
        {
            parent::__set($name, $value);
             
            return;
        }
  
        $this->_dataVirtualProperties[$name] = $value;
    }
  
 
  
    /**
     * Retrieves a list of models based on the current search/filter conditions.
     *
     * @return CActiveDataProvider the data provider that can return the models
     * based on the search/filter conditions.
     */
    public function search()
    {
        $criteria = new CDbCriteria;
  
        $criteria->with = array('user', 'categories');
         
        $criteria->compare('title', $this->title, true);
        $criteria->compare('categories.title', $this->category, true);
        $criteria->compare('user.username', $this->author, true);
  
        $sort = new CSort();
        $sort->attributes = array(
            'category' => array(
                'asc' => 'categories.title',
                'desc' => 'categories.title DESC',
            ),
            'author' => array(
                'asc' => 'user.username',
                'desc' => 'user.username DESC',
            ),
            '*',
        );
         
        return new CActiveDataProvider($this, array('criteria' => $criteria, 'sort' => $sort));
    }

В приватном свойстве $_virtualProperties указывается массив знчений, где ключ это имя поля ПРИДУМАННОЕ для существуещего поля в таблице имя связи которой указана в значении. В приватном свойстве $_aliasForProperties указывается массив значений, где ключ это ключь из $_aliasForProperties, а значение это РЕАЛЬНОЕ имя этого поля в таблице связь которой указанна в значении приватного свойства $_virtualProperties.

Теперь поясню что я имел ввиду. Допустим, у нас есть две таблицы которые мы связали, и поля там имеют одинаковое имя. Естественно для одной из таблиц мы оставим имена полей как есть, (логично оставить рельные имена в для таблицы, которую описывает класс из которого происходят все действия). Для второй таблицы мы придумываем псевдонимы. Т.е. есть поле title в таблице A и title в таблице B, мы берём и обзываем title из таблицы B как header, и записываем это всё в $_virtualProperties

'header' => 'B'

а в свойство $_aliasForProperties напишем

'B' => 'title'

Если приглядеться, то получается такое соотношение 'header' => 'title' => 'B', которое можно прочитать как "header это title который нужно искать в B".

Идём дальше.

В методе search() мы написали $criteria->compare('categories.title', $this->category, true); В первый аргумент передаёте строку вида "имяСвязиДляТаблицы.реальноИмяПоля". Во второй аргумент передаёте как-будто существующее публичное свойство, где после $this-> указан ПРИДУМАННОЕ(псевдоним) поле которое позже будет ссылаться на существующее указанное в первом аргмуенте. Дальше я думаю что должно понятно что к чему.

Для отображения используем тот же принцип, что и в предыдущем посте. Вот и всё.

Заключение

В начале статьи я обещал познакомить Вас с behavior решающий данную проблему. Вот он!(с) http://www.yiiframework.com/extension/relatedsearchbehavior/

Выбирайте решение, которое Вам больше нравится. Все из них имеют свои плюсы и минусы.