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

Разговорились сегодня насчёт вывода списка чекбоксов в админке для выбора категорий к записи, то есть для связи MANY_MANY. Предположим, что в нашем блоге есть записи и категории. Или товары в магазине и категории. При этом у каждой записи или у каждого товара можно выбрать несколько категорий. Как вывести этот список на странице редактирования статьи или товара?
Должно получиться что-то похожее на это:
Категории:
Зима
Весна
Лето
Осень
При этом уже выбранные пункты должны быть отмечены.
Для вывода списка чекбоксов класс CActiveForm содержит метод checkBoxList(), который делегирует вызов методу CHtml::activeCheckBoxList():
<?php echo $form->checkBoxList($model, <поле>, <массив элементов>);
Поле модели для работы данного метода должно содержать одномерный массив выбранных категорий. Наш пример со временами года можно задать так:
<?php $model->categories = array(1, 3); 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... Хотя в блоге это мало кому грозит.
Можно пойти дальше и вынести геттер и сеттер в поведение:
Теперь чтобы добавить свойство $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().
ПазитиFF TimzСпасибо за урок. Но что-то не так. Сделал все как написано, все работает кроме сохранения. Поведение выстреливает (удаляются старые связи пост/категория), но новые не появляются. уже даже не знаю что и думать. Подскажите хоть в какую сторону копать? Структуру БД взял отсюда http://www.yiiframework.com/doc/guide/1.1/ru/database.arr остальное как у вас.
Дмитрий ЕлисеевА отдельно код
работает?
ПазитиFF Timzда.
Дмитрий ЕлисеевЯ немного изменил метод refreshCategories(). Обновите у себя.
ПазитиFF Timzв дебаге пишет
Не удалось присвоить небезопасный атрибут "categories" класса "Post"
на $model->attributes=$_POST['Post'];
Дмитрий ЕлисеевПонятно. Наверное Вы забыли сделать его безопасным и он не приходит из формы:
public function rules(){ return array( array('categoriesArray', 'safe'), ); }
ПазитиFF TimzПардон, это была моя невнимательность. По шагам пробовал пример, забыл заменить в
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] и одновременно подключено поведение, реализующее то же самое. Оставьте что-то одно.
АлександрСтранно, что пока я в контроллере я не написал
динамически созданное свойство 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 и ищу некоторые решения в интернете. Очень часто нахожу хорошие решения моих задач на вашем блоге. Видно, что ваши решения тех или иных задач построены грамотно. Спасибо за то, что очень толково пишите, понятно и подробно расписываете решения!
АлексейСпасибо за статью, очень помогло!
Но я пока не понял почему при редактировании не отображаются в чекбоксах данные из базы?
Код
сделан.
Видимо что-то с геттером 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, это и было ошибочно, ведь в геттере надо получить массив для чекбоксов, а сами чекбоксы ведь "рисует" представление. Вот с таким тупняком своим я еще на один шаг приблизился к пониманию работы фреймворка. Спасибо Вам!
АзизПривет!
А можно мини вопрос, а как теперь выводить например, через запятую, выбранные категории например в виджете. Когда простая связь была, делал по типу:
сейчас не прокатывает.
Потому что по сути нужен кажись Геттер который бы выбирал все по 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')));
Азиза можно еще вопрос? как сделать поиск , типа фильтра
и вот такое
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")))', ),
Сергей ОсиповДмитрий, добрый день.
Пытаюсь освоить работу с чекбоксами для связи многие-ко-многим (как раз по теме статьи).
Вывод чекбоксов на форму происходит, как задумано, но вот почему-то ни один из них не "чекнутый".
Вот строка:
При этом соответствующий сеттер '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:
А лучше:
protected function beforeDelete() { if (parent::beforeDelete()) { $this->deleteRelations(); return true; } return false; }
DfKВ варианте с явным геттером ошибка - возвращается массив моделей значений, а не массив первичных ключей, из-за чего при редактировании чекбоксы оказываются не отмеченными, хотя в базе сохранены.
Дмитрий ЕлисеевГде именно?
DfKpublic 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, ФИО сотрудника и его список должностей, только должности выводятся кодами, а не названиями. Как сделать так чтобы были названия? Не получается, кроме как делать запрос к справочнику должностей, для каждого сотрудника. (но так получается очень много запросов)
Дмитрий ЕлисеевДействительно, не очень удобно использовать отношения с жадной загрузкой, когда нужно связать несколько таблиц, у каждой из которых ещё есть и переводы. Но это неизбежная цена гибкой мультиязычности.
Александр – pushkin.ruМожно использовать не 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 выставить:
Страница открылась, но чекбоксы , которые должны быть выделены, почему-то не выделены
В чем может быть причина?
AndreyИ еще в форме сделал:
Но все равно, как то пусто, галочки не выводятся( Пробовал 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 (имя класса поведения) , а не класс, к которому подключено поведение, модель которого я удаляю?!
des1roerвот моя реализация
http://des1roer.blogspot.ru/2015/03/yii-yii-for-dummies-chtmlcheckboxlist.html
АлексейДобрый день! Ваш блог очень помог, так как до этого я сделал немножко некрасивое решение. Пробую на Yii2. Остановился на варианте геттер-сеттер. Прописал в rules, чтобы свойство было безопасным. Но когда форму заполняю данными модели (у меня $newsEditForm->load($news)) то свойство categoriesArray в форме не заполняется. Если из модели вызвать напрямую $news->categoriesArray - то все есть. При выводе $news->getAttributes() его тоже нету. Подскажите в каком направлении копать?
Дмитрий ЕлисеевВесьма странно. А в форме нормально поле выводится?
АлексейВ форму я передаю так
Дмитрий ЕлисеевПонятно. Из getAttributes() это поле не возвращается, поэтому в сеттер ничего не приходит.
Поэтому либо присваивайте прямо из POST-параметров:
либо как у Вас или вручную:
ДанилНа сколько я понимаю, этот геттер не даст сохранить пустой массив (удалить все категории, которые прежде были заданы). Есть идеи как это лучше обойти?
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'); ); }
НауарЗдравствуйте! Имеются таблицы "Дисциплины" и "Группы". Я создал форму, в которой checkboxlist (Группы) является зависимым от значения dropdownlist (Дисциплины). Список чекбоксов генерируется и обновляется, чекбоксов может быть от 1 до n. Но данные не заносятся в БД, даже когда выбран один чекбокс. Интересует вопрос как можно сделать вставку от выбранных чекбоксов, а именно нескольких строк в таблицу, где поля будут отличаться только ID и IDГруппы. Вставка нескольких строк за один запрос. Извините, может замудрил с вопросом. Заранее спасибо.
Дмитрий ЕлисеевОбычным циклом foreach. Похожее с сохранением есть в вебинаре по связям.