Чекбоксы для связей «Многие ко многим» в Yii

Галочки

Разговорились сегодня насчёт вывода списка чекбоксов в админке для выбора категорий к записи, то есть для связи MANY_MANY. Предположим, что в нашем блоге есть записи и категории. Или товары в магазине и категории. При этом у каждой записи или у каждого товара можно выбрать несколько категорий. Как вывести этот список на странице редактирования статьи или товара?

Должно получиться что-то похожее на это:

Категории:

Зима
Весна
Лето
Осень

При этом уже выбранные пункты должны быть отмечены.

Для вывода списка чекбоксов класс CActiveForm содержит метод checkBoxList(), который делегирует вызов методу CHtml::activeCheckBoxList():

<?php echo $form->checkBoxList($model, <поле>, <массив элементов>); ?>

Поле модели для работы данного метода должно содержать одномерный массив выбранных категорий. Наш пример со временами года можно задать так:

<?php $model->categories = array(1, 3); ?>
 
<?php echo $form->checkBoxList($model, 'categories', array(
    1=>'Зима',
    2=>'Весна',
    3=>'Лето',
    4=>'Осень'
)); ?>

Метод построит четыре чекбокса, из которых первый и третий будет автоматически отмечен.

Для автоматической постройки массива всех элементов из массива моделей можно использовать метод CHtml::listData():

<?php echo $form->checkBoxList($model, 'categories', CHtml::listData(Category::model()->findAll(), 'id', 'name')); ?>

Поначалу можно подумать, что достаточно сделать в модели отношение $model->categories и использовать предыдущий вариант.

class Post extends CActiveRecord
{
    public function relations()
    {
        return array(
            'categories'=>array(self::MANY_MANY, 'Category', '{{post_category}}(post_id, category_id)'),
        ),
    );
}

Но это не сработает, так как в checkBoxList() нужно передавать именно поле с массивом первичных ключей, а не массивом моделей из выборки (из отношения).

Для решения проблемы можно добавить дополнительное свойство и формировать массив в геттере

class Post extends CActiveRecord()
{
    protected $categories_array;
 
    public function rules(){
        return array(
            array('categoriesArray', 'safe'),
        );
    }
 
    public function relations()
    {
        return array(
            'categories'=>array(self::MANY_MANY, 'Category', '{{post_category}}(post_id, category_id)'),
        ),
    );
 
    // ...
 
    public function getCategoriesArray()
    {
        if ($this->categories_array===null)
            $this->categories_array=CHtml::listData($this->categories, 'id', 'id');
        return $this->categories_array;
    }   
 
    public function setCategoriesArray($value)
    {
        $this->categories_array=$value;
    }   
 
    // обрабатываем новый массив $this->categories_array здесь 
    // или свойство $model->categoriesArray в контроллере
    protected function afterSave()
    {
        // ...
        parent::afterSave();
    }
}

И работать в форме с этим полем

<?php echo $form->checkBoxList($model, 'categoriesArray', CHtml::listData(Category::model()->findAll(), 'id', 'name')); ?>

Чекбоксы будут правильно выводиться, а при присваивании безопасных атрибутов $model->attributes=$_POST['Post'] переданный массив с номерами выбранных пользователем категорий будет сохраняться в защищённую переменную.

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

class Post extends CActiveRecord()
{
    // ...
 
    protected function afterSave()
    {
        $this->refreshCategories();
        parent::afterSave();
    }
 
    protected function refreshCategories()
    {
        $categories = $this->categoriesArray;
 
        PostCategory::model()->deleteAllByAttributes(array('post_id'=>$this->id));
 
        if (is_array($categories))
        {
            foreach ($categories as $id)
            {
                if (Category::model()->exists('id=:id', array(':id'=>$id)))
                {                
                    $postCat = new PostCategory();
                    $postCat->post_id = $this->id;
                    $postCat->category_id = $id;
                    $postCat->save();
                }
            }
        }
    }
}

Но есть риск через несколько веков переполнить индекс типа INT... Хотя в блоге это мало кому грозит.

Можно пойти дальше и вынести геттер и сеттер в поведение:

Код на GitHub

Теперь чтобы добавить свойство $model->categoriesArray в нашу модель просто сконфигурируем и подключим к модели это поведение:

class Post extends CActiveRecord
{
    public function relations()
    {
        return array(
            'categories'=>array(self::MANY_MANY, 'Category', '{{post_category}}(post_id, category_id)'),
        ),
    );
 
    public function behaviors()
    {
        return array(
            'DMultiplyListBehavior'=>array(
                 'class'=>'DMultiplyListBehavior',
                 'attribute'=>'categoriesArray',
                 'relation'=>'categories',
                 'relationPk'=>'id',
            ),
        );
    }
 
    protected function afterSave()
    {
        $this->refreshCategories();
        parent::afterSave();
    }
 
    protected function refreshCategories()
    {
        $categories = $this->categoriesArray;
 
        PostCategory::model()->deleteAllByAttributes(array('post_id'=>$this->id));
 
        if (is_array($categories))
        {
            foreach ($categories as $id)
            {
                if (Category::model()->exists('id=:id', array(':id'=>$id)))
                {                
                    $postCat = new PostCategory();
                    $postCat->post_id = $this->id;
                    $postCat->category_id = $id;
                    $postCat->save();
                }
            }
        }
    }
}

Теперь свойство $model->categoriesArray вернёт нам массив первичных ключей категорий, то есть выбранные нами Array(1, 3).

Мы можем считывать этот массив для генерации списков и присваивать введённые пользователем значения:

<?php echo $form->checkBoxList($model, 'categoriesArray', CHtml::listData(Category::model()->findAll(), 'id', 'name')); ?>

Если боитесь подключать кучу поведений сразу внутри самой модели, то можете подключать это поведение динамически в контроллере используя метод attachBehavior().

Другие статьи

На многих новых сайтах всё чаще встречается вывод списка новостей или других сущностей в виде бесконечно подгружающейся ленты. На некоторых сайтах подгрузка выполняется автоматически (на twitter.com или vk.com), на других – вручную, то есть в конце списка вместо стандартного переключателя страниц имеется кнопка «Показать ещё». Освежим в памяти работу с ClistView и попробуем реализовать подобный функционал на своём сайте.

Одна из вечных тем, то и дело всплывающих в сети и касаемых Yii Framework – это спор относительно использования в своих проектах прямых SQL запросов посредством DAO с одной стороны против использования ActiveRecord с другой. Ведь при разрастании объёмов данных и связей между ними в высоконагруженных проектах многие разработчики переходят от удобной объектной модели ActiveRecord к низкоуровневой работе с прямыми SQL запросами и с простыми асcоциативными массивами. Посмотрим, как в некоторых случаях можно разогнать выборки ActiveRecord почти до скорости DAO.

Велика вероятность, что в новом или старом проекте на Yii появится необходимость изготовления меню с иконками. Это может понадобиться и при вёрстке уже готового шаблона. Рассмотрим несколько решений и сравним их между собой с точки зрения семантики и архитектурной чистоты.

Любой программист с опытом осознаёт, что в неудачно спроектированном тяжёлом проекте изначально или со временем накапливается много неуправляемого и ненужного мусора. Это, например, повторяющийся код. В описании контроллера официального руководства Yii Framework указано, что Yii поддерживает вынос действий в отдельные классы и описывается процедура подключения этих действий к контроллерам. Но мало кто пользуется этим способом, так как не находит действительно тяжёлых повторяющихся экшенов.

Комментарии

 

ПазитиFF Timz

Спасибо за урок. Но что-то не так. Сделал все как написано, все работает кроме сохранения. Поведение выстреливает (удаляются старые связи пост/категория), но новые не появляются. уже даже не знаю что и думать. Подскажите хоть в какую сторону копать? Структуру БД взял отсюда http://www.yiiframework.com/doc/guide/1.1/ru/database.arr остальное как у вас.

Ответить

 

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

А отдельно код

$postCat = new PostCategory();
$postCat->post_id = ...;
$postCat->category_id = ...;
$postCat->save();

работает?

Ответить

 

ПазитиFF Timz

да.

Ответить

 

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

Я немного изменил метод refreshCategories(). Обновите у себя.

Ответить

 

ПазитиFF Timz

в дебаге пишет
Не удалось присвоить небезопасный атрибут "categories" класса "Post"
на $model->attributes=$_POST['Post'];

Ответить

 

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

Понятно. Наверное Вы забыли сделать его безопасным и он не приходит из формы:

public function rules(){
    return array(
       array('categoriesArray', 'safe'),
   );
}
Ответить

 

ПазитиFF Timz

Пардон, это была моя невнимательность. По шагам пробовал пример, забыл заменить в

<?php echo $form->checkBoxList($model, 'categories', CHtml::listData(Category::model()->findAll(), 'id', 'name')); ?>

categories на categoriesArray
Спасибо за пример!

Ответить

 

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

Обновил поведение. Теперь array('categoriesArray', 'safe') добавлять не надо.

Ответить

 

Денис Наталевич

У меня в admin.php в Grid`е выводятся неверные id.
Подскажите в чём может быть ошибка

В документации нашёл "5. Устранение конфликта имён столбцов ":
http://www.yiiframework.com/doc/guide/1.1/ru/database.arr#sec-6

В этом ли дело и если да, то как применить к вашему примеру?

Ответить

 

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

Чтобы корректно работали фильтры нужно в search() к именам колонок всех строк

$criteria->compare('id', $this->id);

дописать алиас t

$criteria->compare('t.id', $this->id);

И вообще, использовать «t.» полезно всегда.

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

'comments' => array(self::HAS_MANY, 'Comment', 'post_id',
    'condition'=>'comments.public = 1',
    'order'=>'comments.id ASC'
),

Теперь никакой путаницы с полями id и public комментария и поста не будет.

Ответить

 

Денис Наталевич

Всё равно не понимаю(
Добавление/изменение работают идеально.
Но вот просмотр admin.php путаются id.
Посмотрите пожалуйста где я напутал.

class Delivery extends CActiveRecord
{
    protected $payment_array;
    
    public static function model($className=__CLASS__)
    {
        return parent::model($className);
    }

    public function tableName()
    {
        return '{{delivery}}';
    }

    public $id = array();

    public function afterFind()
    {
        if (!empty($this->paymentMethods))
        {
            foreach ($this->paymentMethods as $n => $paymentMethods)
                $this->id = $paymentMethods->id;
        }

        parent::afterFind();
    }

    public function rules()
    {
        return array(
            array('title, description, price', 'required'),
            array('published, sortOrder, separate_payment', 'numerical', 'integerOnly'=>true),
            array('free_from, price', 'numerical'),
            array('title', 'length', 'max'=>255),
            array('paymentArray', 'safe'),
            array('id, title, description, free_from, price, published, sortOrder, separate_payment', 'safe', 'on'=>'search'),
        );
    }

    public function relations()
    {
        return array(
            'paymentMethods' => array(
                self::MANY_MANY, 
                'PaymentMethods', 
                '{{delivery}}_payment(id_delivery, id_payment_method)',
                'order'=>'paymentMethods.id ASC'
            ),
        );
    }
    
    public function behaviors()
    {
        return array(
            'DMultiplyListBehavior'=>array(
                 'class'=>'DMultiplyListBehavior',
                 'attribute'=>'paymentArray',
                 'relation'=>'paymentMethods',
                 'relationPk'=>'id',
            ),
        );
    }
    
    public function getPaymentArray()
    {
        if ($this->payment_array===null)
            $this->payment_array=CHtml::listData($this->paymentMethods, 'id', 'id');
        return $this->payment_array;
    }   
 
    public function setPaymentArray($value)
    {
        $this->payment_array=$value;
    }   
 
    protected function afterSave()
    {
        $this->refreshPayment();
        parent::afterSave();
    }
 
    protected function refreshPayment()
    {
        DeliveryPayment::model()->deleteAllByAttributes(array('id_delivery'=>$this->id));
 
        if (is_array($this->paymentArray))
        {
            foreach ($this->paymentArray as $id)
            {
                if (PaymentMethods::model()->exists('id=:id', array(':id'=>$id)))
                {                
                    $postDelPay = new DeliveryPayment();
                    $postDelPay->id_delivery = $this->id;
                    $postDelPay->id_payment_method = $id;
                    $postDelPay->save();
                }
            }
        }
    }

    public function attributeLabels()
    {
        return array(
            'id' => 'ID',
            'title' => 'Заголовок',
            'description' => 'Описание',
            'free_from' => 'Бесплатно от',
            'price' => 'Стоимость',
            'published' => 'Публикация',
            'sortOrder' => 'Порядок',
            'separate_payment' => 'Оплата отдельно',
            'paymentMethods' => 'Способы оплаты',
        );
    }

    public function search()
    {
        $criteria=new CDbCriteria;
        $criteria->compare('t.id',$this->id);
        $criteria->compare('t.title',$this->title,true);
        $criteria->compare('t.description',$this->description,true);
        $criteria->compare('t.free_from',$this->free_from);
        $criteria->compare('t.price',$this->price);
        $criteria->compare('t.published',$this->published);
        $criteria->compare('t.sortOrder',$this->sortOrder);
        $criteria->compare('t.separate_payment',$this->separate_payment);
        $criteria->order = 't.sortOrder ASC';

        return new CActiveDataProvider($this, array(
            'criteria'=>$criteria,
        ));
    }
}
Ответить

 

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

Вы сами путаете свой id кодом:

public $id = array();

public function afterFind()
{
    if (!empty($this->paymentMethods))
    {
        foreach ($this->paymentMethods as $n => $paymentMethods)
           $this->id = $paymentMethods->id;
    }
    parent::afterFind();
}

Удалите всё это.

Также правильнее переписать метод refreshPayment так:

protected function refreshPayment()
{
    $payments = $this->paymentArray;

    DeliveryPayment::model()->deleteAllByAttributes(array('id_delivery'=>$this->id));

    if (is_array($payments))
    {
        foreach ($payments as $id)
        {
            if (PaymentMethods::model()->exists('id=:id', array(':id'=>$id)))
            {                
                $postDelPay = new DeliveryPayment();
                $postDelPay->id_delivery = $this->id;
                $postDelPay->id_payment_method = $id;
                $postDelPay->save();
            }
        }
    }
}
Ответить

 

Денис Наталевич

Спасибо за помощь!

Ответить

 

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

Ещё заметил, что у Вас используется связка [getPaymentArray() + setPaymentArray() + payment_array] и одновременно подключено поведение, реализующее то же самое. Оставьте что-то одно.

Ответить

 

Александр

Странно, что пока я в контроллере я не написал

$model->typeSizeList=$postItemType['typeSizeList']

динамически созданное свойство typeSizeList (в примере это "categoriesArray") было либо пустым, либо содержало значения согласно данным в таблице

Ответить

 

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

Чтобы свойство проходило присвоение атрибутов, его необходимо упомянуть в rules():

array('typeSizeList', 'safe'),

Это справедливо для всех свойств.

Ответить

 

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

Обновил поведение. Теперь array('categoriesArray', 'safe') добавлять не надо.

Ответить

 

ПазитиFF Timz

А как настроить behavior если у меня несколько связей многие-ко-многим?

Ответить

 

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

Перечислить через запятую:

public function behaviors()
{
    return array(
        'CategoriesBehavior'=>array(
             'class'=>'DMultiplyListBehavior',
             'attribute'=>'categoriesArray',
             'relation'=>'categories',
             'relationPk'=>'id',
        ),
        'ColorsBehavior'=>array(
             'class'=>'DMultiplyListBehavior',
             'attribute'=>'colorsArray',
             'relation'=>'colors',
             'relationPk'=>'id',
        ),
        'SizesBehavior'=>array(
             'class'=>'DMultiplyListBehavior',
             'attribute'=>'sizesArray',
             'relation'=>'sizes',
             'relationPk'=>'id',
        ),
    );
}
Ответить

 

Шевченко Евгений

Дмитрий, спасибо вам. Я являюсь новичком в Yii и ищу некоторые решения в интернете. Очень часто нахожу хорошие решения моих задач на вашем блоге. Видно, что ваши решения тех или иных задач построены грамотно. Спасибо за то, что очень толково пишите, понятно и подробно расписываете решения!

Ответить

 

Алексей

Спасибо за статью, очень помогло!
Но я пока не понял почему при редактировании не отображаются в чекбоксах данные из базы?
Код

<?php echo $form->checkBoxList($model, 'categoriesArray', CHtml::listData(Category::model()->findAll(), 'id', 'name')); ?>

сделан.
Видимо что-то с геттером categoriesArray? Не могу понять пока как отловить ошибку.

Ответить

 

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

А отношения правильно сделаны?

Ответить

 

Алексей

Дмитрий, я вроде разобрался почему так получилось. Но не соображу как исправить. Отношения вроде правильные, но дело в том, что в геттере я сделал так:

if ($this->RefillsT_array===null)
    $this->RefillsT_array=CHtml::listData(Vcartriges::model()->findAll(),'id', 'name');
return $this->RefillsT_array;

Vcartriges это VIEW. Это мне понадобилось для вывода в Label чекбокса дополнительной информации из другой таблицы.

Поэтому в контроллере на экшн Update я сейчас пытаюсь в функции loadModel установить массиву $RefillsT_array (это Ваш protected $categories_array;) нужные значения из базы. Я в правильном направлении?

Ответить

 

Алексей

Вообще для установки галок смысл какой? Должен быть массив array(1, 3); как у Вас в начале статьи?
А то я формирую массив

array(2) {
  [2]=>
  string(1) "2"
  [3]=>
  string(1) "3"
}

но всё равно не отображается

Ответить

 

Алексей

Написал var_dump в сеттере и получил массив:

array(2) {
  [0]=>
  string(1) "7"
  [1]=>
  string(1) "8"
}

Получается формат массивов одинаковый, только вот индексы у них разные.
Не понятно :\

Ответить

 

Алексей

Дмитрий, я разобрался. Прошу прощения за кол-во сообщений, но мне это очень сильно помогло. Спасибо Вам большое.

Всё дело было в том, что в геттере я неверно сделал. Надо было как у Вас, а я делал listData из VIEW, это и было ошибочно, ведь в геттере надо получить массив для чекбоксов, а сами чекбоксы ведь "рисует" представление. Вот с таким тупняком своим я еще на один шаг приблизился к пониманию работы фреймворка. Спасибо Вам!

Ответить

 

Азиз

Привет!

А можно мини вопрос, а как теперь выводить например, через запятую, выбранные категории например в виджете. Когда простая связь была, делал по типу:

CHtml::encode($WNews->categories->name)

сейчас не прокатывает.

Потому что по сути нужен кажись Геттер который бы выбирал все по id, а потом этот массив через запятую.

Ответить

 

Азиз

Напимер вот так:

News::allCatForNews($WNews->id);

соответсвенно в модели,

public static function allCatForNews($id)
{
    $models = Category::model()->findAll('new_id=:id', array('id'=>$id));
    $array = array(); 
    foreach($models as $element) {
        $array[$element->id] = $element->title;
    }
    $Str = self::array2string($array); 
    return $Str;
}

public static function array2string($Checeked)
{
    return implode(', ',$Checeked);
}

Можешь подсказать это правильно, и имеет право на жизнь? Или как то можно сделать еще лучше?

Ответить

 

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

Можно так:

CHtml::encode(implode(', ', CHtml::listData($new->categories, 'id', 'name')));
Ответить

 

Азиз

а можно еще вопрос? как сделать поиск , типа фильтра

<?php echo $form->checkBoxList($model, 'categoriesArray', CHtml::listData(Category::model()->findAll(), 'id', 'name')); ?>
<?php echo $form->error($model,'category'); ?>

и вот такое

public function search()
{
    $criteria=new CDbCriteria;
    ...
    $criteria->compare('t.categories',$this->categories);
    $criteria->order = 'if(t.featured=1,0,1)';

    return new CActiveDataProvider($this, array(
        'criteria'=>$criteria,
    ));
}
Ответить

 

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

Добавить переменную для фильтра:

public $search_category = '';

public function rules()
{
    return array(
        ...
        array('search_category', 'safe', 'on'=>search');
    ),
}

Потом либо перестроить метод поиска на работу с отношением categories:

$criteria->compare('categories.id',$this->search_category);
$criteria->with = array('categories');

Либо добавить новое отношение news_categories и модель NewCategory для промежуточной таблицы:

return array(
    'categories'=>array(self::MANY_MANY, 'Category', '{{new_category}}(new_id, category_id)'),
    'news_categories'=>array(self::HAS_MANY, 'NewCategory', 'new_id'),
),

И использовать уже его:

public function search()
{
    $criteria=new CDbCriteria;
    ....
    criteria->compare('news_categories.category_id',$this->search_category);
    $criteria->order = 'if(t.featured=1,0,1)';

    $criteria->with = array('news_categories');

    return new CActiveDataProvider($this, array(
        'criteria'=>$criteria,
    ));
}

Во втором случае SQL запрос будет проще.

И добавить фильтр к ячейке CGridView:

array(
    'name'=>'search_category',
    'header'=>'Категория',
    'filter'=>CHtml::listData(Category::model()->findAll(), 'id', 'name'),
    'value'=>'CHtml::encode(implode(", ", CHtml::listData($data->categories, "id", "name")))',
),
Ответить

 

Сергей Осипов

Дмитрий, добрый день.

Пытаюсь освоить работу с чекбоксами для связи многие-ко-многим (как раз по теме статьи).

Вывод чекбоксов на форму происходит, как задумано, но вот почему-то ни один из них не "чекнутый".

Вот строка:

<?php echo $form->checkBoxList($model, 'linkedStyle2sArray', CHtml::listData(Style2::model()->findAll(), 'style2_id', 'style2')); ?>

При этом соответствующий сеттер 'getLinkedStyle2sArray' для 'linkedStyle2sArray' нормально отдает массив:

Array
(
    [3] => Готический
    [4] => Барокко
)

Не могли бы Вы подсказать в чем может быть дело?

Ответить

 

Сергей Осипов

Внимательнее прочитал начало статьи и нашел ошибку.

В методе

  public function getLinkedStyle2sArray()
  {
    if ($this->linked_style2s_array === null)
      $this->linked_style2s_array = CHtml::listData($this->style2s, 'style2_id', 'style2');
    return $this->linked_style2s_array;
  }  


я получал массив вида [индекс стиля] => Имя стиля, а надо ведь [индекс стиля] => Индекс стиля:

  public function getLinkedStyle2sArray()
  {
    if ($this->linked_style2s_array === null)
      $this->linked_style2s_array = CHtml::listData($this->style2s, 'style2_id', 'style2_id');
    return $this->linked_style2s_array;
  }  

Спасибо за статью! )

Ответить

 

SnowB Alex

Спасибо огромное за статью.
Убил целый день на MANY_MANY, а после прочтения написал за полчаса.

Ответить

 

Алексей

Подскажите пожалуйста как и куда подключить DMultiplyListBehavior.php?

Ответить

 

Алексей

Разобрался.
Работает почти как мне надо. =) Буду дальше разбираться.

Ответить

 

Алексей

Спасибо за статью, разобрался, сделал как мне было нужно! =)

Ответить

 

Oleg Kuzmenko

Дмитрий, здравствуйте.
Спасибо за проделанную работу, ваше поведение очень помогло реализовать нужную мне задачу.

Но есть одно "но", а именно: не могу удалить сущность, имеющую связь MANY_MANY.
У меня есть модель PlaceType, у которой связь MANY_MANY с моделью Features. Так вот, когда я из CGridView или из экшна View пытаюсь удалить запись PlaceType, то удаляются только ее Features, а сам PlaceType остается. Если закомментировать метод:

protected function beforeDelete()
{
    $this->deleteRelations();
    parent::beforeDelete();
}

То все отрабатывает нормально, в ином случае процедура удаления не заходит дальше метода deleteRelations().

Сам пока не понимаю, почему так.
У всех нормально?

Ответить

 

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

Забыли return:

return parent::beforeDelete();

А лучше:

protected function beforeDelete()
{
    if (parent::beforeDelete()) {
        $this->deleteRelations();
        return true;
    }
    return false;
}
Ответить

 

DfK

В варианте с явным геттером ошибка - возвращается массив моделей значений, а не массив первичных ключей, из-за чего при редактировании чекбоксы оказываются не отмеченными, хотя в базе сохранены.

Ответить

 

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

Где именно?

Ответить

 

DfK
public function getCategoriesArray()
{
    if ($this->categories_array===null)
        $this->categories_array=CHtml::listData($this->categories, 'id', 'id');
    return $this->categories_array;
}

но, checkBoxList в categoriesArray ждет массив ID, которые нужно выделить...

Ответить

 

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

Этим и занимается метод CHtml::listData.

Ответить

 

Дмитрий

Спасибо за статью. Подскажите как сделать чтобы id у таблицы post_category не менялся, т.к. эту связь использую в другой таблице и там id постоянен?

Ответить

 

Дмитрий

Еще уточняю, имеется ввиду не удалять старые связи а обновить их. Как это сделать?

Ответить

 

Дмитрий

Сделал так

// ВМЕСТО post_category -> category_type (в моем проекте так)

protected function refreshTypes()
    {
        $types = $this->typesArray;
         
        // выбираем все категории с типами
        $categoryTypes = CategoryType::model()->findAllByAttributes(array('category_id'=>$this->id));
 
        if (is_array($types))
        {
            // обойдем все связи категории с типами 
            foreach($categoryTypes as $c) {
                //проверим существовал ли такой тип с формы в связи
                $key = array_search($c['type_id'], $types);
                if(!$key) {
                    // если с формы галочка не стояла, удаляем связь
                    CategoryType::model()->deleteByPk(array($c['id']));
                }
                else {
                    // если галочка стояла и такая связь есть, то удаляем значение по ключу связи из массива формы типа категории
                    unset($types[$key]);
                }
            }
            
// все остальное тоже самое
            foreach ($types as $id)
            {
                if (Type::model()->exists('id = :id', array(':id' => $id)))
                {                                    
                    $catType = new CategoryType();
                    $catType->category_id = $this->id;
                    $catType->type_id = $id;
                    $catType->save();
                }
            }
        }
    }

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

Ответить

 

Роберт

Здравствуйте, такой вопрос.
Сделал с помощью Вашего класса DMultiplyListBehavior.
Все хорошо работает, только с должностями, т.е. у каждого человека есть одна или более должностей.
Теперь я вывожу в CGridView, ФИО сотрудника и его список должностей, только должности выводятся кодами, а не названиями. Как сделать так чтобы были названия? Не получается, кроме как делать запрос к справочнику должностей, для каждого сотрудника. (но так получается очень много запросов)

Ответить

 

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

Действительно, не очень удобно использовать отношения с жадной загрузкой, когда нужно связать несколько таблиц, у каждой из которых ещё есть и переводы. Но это неизбежная цена гибкой мультиязычности.

Ответить

 

Александр

Можно использовать не checkbox, а расширение Chosen.
Тогда все будет работать как написано в вашем первоначальном варианте без необходимости создавать дополнительных переменных.

<?php echo Chosen::activeDropDownList(
    $model,'categories', 
    CHtml::listData(Category::model()->findAll(), 'id', 'name'),
    array('class'=>'span5', 'data-placeholder' => 'Выберите категорию', 'multiple'=>'multiple')
); ?>

Кстати очень полезное расширение для большого количества checkbox.

Ответить

 

zavulo

Полезная статья.
Думаю воспользоваться.
Но у меня категории имеют подкатегории.

Вопрос:
Как вывести иерархическое дерево категорий с чекбоксами,
через $forn->checkBoxList($model, 'categoriesArray',...); ?

Ответить

 

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

Я в последнее время предпочитаю Nested Set для иерархических вещей. А так можно в getCategoriesArray() рекурсией построить массив и сделать отступы у названий категорий дефисами.

Ответить

 

Andrey

Спасибо!

А если несколько таких много ко многим чекбоксов, можно ли как то прицепить к одному поведению или нужно создавать несколько разных поведений и геттеров,, сеттеров?

Ответить

 

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

Несколько поведений:

public function behaviors()
{
   return array(
        'categoriesBehavior'=>array(
             'class'=>'DMultiplyListBehavior',
             'attribute'=>'categoriesArray',
             'relation'=>'categories',
             'relationPk'=>'id',
        ),
        'brandsBehavior'=>array(
             'class'=>'DMultiplyListBehavior',
             'attribute'=>'brandsArray',
             'relation'=>'brands',
             'relationPk'=>'id',
        ),
    );
}
Ответить

 

Andrey

Спасибо большое, а геттеры и сеттеры тоже дублировать надо для брэндов и ложить информацию о них в rules?

Ответить

 

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

Да. Так Вы геттеры используете или поведение?

Ответить

 

Andrey

Спасибо, это я невнимателен, не понял сразу что у вас 2 подхода к реализации)

Ответить

 

Andrey

Меня интересует этот участок

// обрабатываем новый массив $this->categories_array здесь
// или свойство $model->categoriesArray в контроллере


И работать в форме с этим полем

<?php echo $form->checkBoxList($model, 'categoriesArray', CHtml::listData(Category::model()->findAll(), 'id', 'name')); ?>

1. Если используем поведение - то действуем по такомуже алгоитму ^?
2. categoriesArray вытаскиваем как findAll или пробегаемся в цикле по связям данного продукта и формируем массив?
3. поведение, которое описано в модели Post, его нужно создавать отдельным файлом, если да, то где найти код поведения, если я правильно понял, то должен быть класс типа

class ImageBehavior extends CActiveRecordBehavior ....

Ответить

 

Andrey

Насчет пункта 3 - нашел на github , а вот первые 2 до сих пор не понял.

Ответить

 

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

1. Да, обрабатываем categoriesArray как в последнем коде модели Post.
2. Свойство categoriesArray формирует само поведение на основе связи. Самим ничего делать не надо.

Ответить

 

a.k.

Добрый день, а есть ли реализация подобного для yii2?

Ответить

 

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

Я точно не делал. Может кто-то и переписал.

Ответить

 

a.k.

Кстати, Дмитрий, почему не использовался составной ключ в данном случае? Мне кажется что так было бы проще

Ответить

 

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

Можно и с составным. Разницы особо нет.

Ответить

 

Andrey

Здравствуйте!

Поробовал использовать несколько поведения для нескольких "много ко многим"

public function behaviors() {
        return 
            'DMultiplyListBehavior'=>array(
                'class'=>'ext.DMultiplyListBehavior',
                'attribute'=>'paramsArray',
                'relation'=>'params',
                'relationPk'=>'CODE',
            ),
            'DMultiplyListBehavior'=>array(
                'class'=>'ext.DMultiplyListBehavior',
                'attribute'=>'milledArray',
                'relation'=>'milled',
                'relationPk'=>'CODE',
            ),
            'DMultiplyListBehavior'=>array(
                'class'=>'ext.DMultiplyListBehavior',
                'attribute'=>'beansArray',
                'relation'=>'beans',
                'relationPk'=>'CODE',
            ),
        );
    } 

Связи от товара выставлены:

return array(
            'beans'=>array(self::MANY_MANY, 'Beans',
                'l_product_22_beans(CODE_1, CODE_2)'),
            'milled'=>array(self::MANY_MANY, 'Milled',
                'l_product_22_milled(CODE_1, CODE_2)'),
            'params'=>array(self::MANY_MANY, 'Params',
);

В refresh продублировал 3 раза кусок с удалением и заполнением в связочные:

protected function refreshCategories()
    {
        $beans = $this->beansArray;
        LProduct22Beans::model()->deleteAllByAttributes(array('CODE_1'=>$this->CODE));
        if (is_array($beans))
        {
            foreach ($beans as $item_code)
            {
                if (Beans::model()->exists('CODE=:CODE', array(':CODE'=>$this->CODE)))
                {
                    $postCat = new LProduct22Beans();
                    $postCat->CODE_1 = $this->CODE;
                    $postCat->CODE_2 = $item_code;
                    $postCat->save();
                }
            }
        }

        $milled = $this->milledArray;
        LProduct22Milled::model()->deleteAllByAttributes(array('CODE_1'=>$this->CODE));
        if (is_array($milled))
        {
            foreach ($milled as $item_code)
            {
                if (Milled::model()->exists('CODE=:CODE', array(':CODE'=>$this->CODE)))
                {
                    $postCat = new LProduct22Milled();
                    $postCat->CODE_1 = $this->CODE;
                    $postCat->CODE_2 = $item_code;
                    $postCat->save();
                }
            }
        }

        $params = $this->paramsArray;
        LProduct22Params::model()->deleteAllByAttributes(array('CODE_1'=>$this->CODE));
        if (is_array($params))
        {
            foreach ($params as $item_code)
            {
                if (Milled::model()->exists('CODE=:CODE', array(':CODE'=>$this->CODE)))
                {
                    $postCat = new LProduct22Params();
                    $postCat->CODE_1 = $this->CODE;
                    $postCat->CODE_2 = $item_code;
                    $postCat->save();
                }
            }
        }
    }


Но когда открываю страницу в админке, где эти чекбоксы быть должны, вылетает ошибка:

Не определено свойство "Product.milledArray". (аналог Вашего CategoriesArray)

Попробовал в классе Product выставить:

public $paramsArray;
public $milledArray;
public $beansArray;

Страница открылась, но чекбоксы , которые должны быть выделены, почему-то не выделены

В чем может быть причина?

Ответить

 

Andrey

И еще в форме сделал:

<?php echo $form->checkBoxList($model, 'beansArray', CHtml::listData(Beans::model()->findAll(), 'CODE', 'NAME')); ?>
<?php echo $form->checkBoxList($model, 'milledArray', CHtml::listData(Milled::model()->findAll(), 'CODE', 'NAME')); ?>
<?php echo $form->checkBoxList($model, 'paramsArray', CHtml::listData(Params::model()->findAll(), 'CODE', 'NAME')); ?>

Но все равно, как то пусто, галочки не выводятся( Пробовал php быдлоспособом с циклом и условием на наличие связи, там все ок, связи правильно в таблицах хранятся. Где же я ошибся?

Ответить

 

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

Вы ключ 'DMultiplyListBehavior' в behaviors три раза повторили.

Ответить

 

Andrey

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

Ответить

 

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

Да, в нём.

Ответить

 

Andrey

Добрый день, немного расширил поведение под свои нужды, добавил несколько методов. Вот к примеру beforeDelete:

public function beforeDelete($event) {
        echo  '<hr/>| '. get_class($this->owner) . ' |<hr/><br/><br/><br/>';
        die();
        $this->deleteDoc();
}

Почему get_class($this->owner) выводит DMultiplyListBehavior (имя класса поведения) , а не класс, к которому подключено поведение, модель которого я удаляю?!

Ответить

 

Алексей

Добрый день! Ваш блог очень помог, так как до этого я сделал немножко некрасивое решение. Пробую на Yii2. Остановился на варианте геттер-сеттер. Прописал в rules, чтобы свойство было безопасным. Но когда форму заполняю данными модели (у меня $newsEditForm->load($news)) то свойство categoriesArray в форме не заполняется. Если из модели вызвать напрямую $news->categoriesArray - то все есть. При выводе $news->getAttributes() его тоже нету. Подскажите в каком направлении копать?

Ответить

 

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

Весьма странно. А в форме нормально поле выводится?

Ответить

 

Алексей

В форму я передаю так

$formNewsEdit->load(array_merge($news->getAttributes(), ['categoriesArray' => $news->categoriesArray]), '');
Ответить

 

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

Понятно. Из getAttributes() это поле не возвращается, поэтому в сеттер ничего не приходит.

Поэтому либо присваивайте прямо из POST-параметров:

$newsEditForm->load(Yii::app$->request->post)

либо как у Вас или вручную:

$formNewsEdit->attributes = $news->attributes;
$formNewsEdit->categoriesArray => $news->categoriesArray;
Ответить

 

Данил

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

public function getCategoriesArray()
    {
        if ($this->categories_array===null)
            $this->categories_array=CHtml::listData($this->categories, 'id', 'id');
        return $this->categories_array;
    }
Ответить

 

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

Почему не даст?

Ответить

 

Данил

Как я понимаю если в этот сеттер придет пустой массив (все чекеты сняты):

public function setCategoriesArray($value)
{
    $this->categories_array=$value;
}

то в геттере выполнится условие if ($this->categories_array===null)

public function getCategoriesArray()
{
    if ($this->categories_array===null)
        $this->categories_array=CHtml::listData($this->categories, 'id', 'id');
        return $this->categories_array;
    }

соответственно в методе обновления категорий в качестве сохраняемого массива $categories мы получим данные из базы, а не из формы:

protected function refreshCategories()
{
    $categories = $this->categoriesArray;
Ответить

 

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

Yii добавляет пустое hidden-поле перед чекбоксами, так что из формы присвоится пустая строка, а не null.

Ответить

 

Путин В.В

Здравствуйте, Дмитрий.

Что Вы имеете ввиду - 'Но есть риск через несколько веков переполнить индекс типа INT... Хотя в блоге это мало кому грозит' ?

Ответить

 

Путин В.В

Вы ошибаетесь. Спасибо и удачи.

Ответить

 

Роман Девилс

у Вас ошибочка, точка с запятой в массиве:

 public function rules(){
        return array(
            array('categoriesArray', 'safe');
        );
    }
Ответить

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

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


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



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