Чекбоксы для связей «Многие ко многим» в 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()
.
Спасибо за урок. Но что-то не так. Сделал все как написано, все работает кроме сохранения. Поведение выстреливает (удаляются старые связи пост/категория), но новые не появляются. уже даже не знаю что и думать. Подскажите хоть в какую сторону копать? Структуру БД взял отсюда http://www.yiiframework.com/doc/guide/1.1/ru/database.arr остальное как у вас.
А отдельно код
работает?
да.
Я немного изменил метод refreshCategories(). Обновите у себя.
в дебаге пишет
Не удалось присвоить небезопасный атрибут "categories" класса "Post"
на $model->attributes=$_POST['Post'];
Понятно. Наверное Вы забыли сделать его безопасным и он не приходит из формы:
Пардон, это была моя невнимательность. По шагам пробовал пример, забыл заменить в
categories на categoriesArray
Спасибо за пример!
Обновил поведение. Теперь array('categoriesArray', 'safe') добавлять не надо.
У меня в admin.php в Grid`е выводятся неверные id.
Подскажите в чём может быть ошибка
В документации нашёл "5. Устранение конфликта имён столбцов ":
http://www.yiiframework.com/doc/guide/1.1/ru/database.arr#sec-6
В этом ли дело и если да, то как применить к вашему примеру?
Чтобы корректно работали фильтры нужно в search() к именам колонок всех строк
дописать алиас t
И вообще, использовать «t.» полезно всегда.
Также во всех отношениях нужно указывать для полей псевдоним по имени отношения:
Теперь никакой путаницы с полями id и public комментария и поста не будет.
Всё равно не понимаю(
Добавление/изменение работают идеально.
Но вот просмотр admin.php путаются id.
Посмотрите пожалуйста где я напутал.
Вы сами путаете свой id кодом:
Удалите всё это.
Также правильнее переписать метод refreshPayment так:
Спасибо за помощь!
Ещё заметил, что у Вас используется связка [getPaymentArray() + setPaymentArray() + payment_array] и одновременно подключено поведение, реализующее то же самое. Оставьте что-то одно.
Странно, что пока я в контроллере я не написал
динамически созданное свойство typeSizeList (в примере это "categoriesArray") было либо пустым, либо содержало значения согласно данным в таблице
Чтобы свойство проходило присвоение атрибутов, его необходимо упомянуть в rules():
Это справедливо для всех свойств.
Обновил поведение. Теперь array('categoriesArray', 'safe') добавлять не надо.
А как настроить behavior если у меня несколько связей многие-ко-многим?
Перечислить через запятую:
Дмитрий, спасибо вам. Я являюсь новичком в Yii и ищу некоторые решения в интернете. Очень часто нахожу хорошие решения моих задач на вашем блоге. Видно, что ваши решения тех или иных задач построены грамотно. Спасибо за то, что очень толково пишите, понятно и подробно расписываете решения!
Спасибо за статью, очень помогло!
Но я пока не понял почему при редактировании не отображаются в чекбоксах данные из базы?
Код
сделан.
Видимо что-то с геттером categoriesArray? Не могу понять пока как отловить ошибку.
А отношения правильно сделаны?
Дмитрий, я вроде разобрался почему так получилось. Но не соображу как исправить. Отношения вроде правильные, но дело в том, что в геттере я сделал так:
Vcartriges это VIEW. Это мне понадобилось для вывода в Label чекбокса дополнительной информации из другой таблицы.
Поэтому в контроллере на экшн Update я сейчас пытаюсь в функции loadModel установить массиву $RefillsT_array (это Ваш protected $categories_array;) нужные значения из базы. Я в правильном направлении?
Вообще для установки галок смысл какой? Должен быть массив array(1, 3); как у Вас в начале статьи?
А то я формирую массив
но всё равно не отображается
Написал var_dump в сеттере и получил массив:
Получается формат массивов одинаковый, только вот индексы у них разные.
Не понятно :\
Дмитрий, я разобрался. Прошу прощения за кол-во сообщений, но мне это очень сильно помогло. Спасибо Вам большое.
Всё дело было в том, что в геттере я неверно сделал. Надо было как у Вас, а я делал listData из VIEW, это и было ошибочно, ведь в геттере надо получить массив для чекбоксов, а сами чекбоксы ведь "рисует" представление. Вот с таким тупняком своим я еще на один шаг приблизился к пониманию работы фреймворка. Спасибо Вам!
Привет!
А можно мини вопрос, а как теперь выводить например, через запятую, выбранные категории например в виджете. Когда простая связь была, делал по типу:
сейчас не прокатывает.
Потому что по сути нужен кажись Геттер который бы выбирал все по id, а потом этот массив через запятую.
Напимер вот так:
соответсвенно в модели,
Можешь подсказать это правильно, и имеет право на жизнь? Или как то можно сделать еще лучше?
Можно так:
а можно еще вопрос? как сделать поиск , типа фильтра
и вот такое
Добавить переменную для фильтра:
Потом либо перестроить метод поиска на работу с отношением categories:
Либо добавить новое отношение news_categories и модель NewCategory для промежуточной таблицы:
И использовать уже его:
Во втором случае SQL запрос будет проще.
И добавить фильтр к ячейке CGridView:
Дмитрий, добрый день.
Пытаюсь освоить работу с чекбоксами для связи многие-ко-многим (как раз по теме статьи).
Вывод чекбоксов на форму происходит, как задумано, но вот почему-то ни один из них не "чекнутый".
Вот строка:
При этом соответствующий сеттер 'getLinkedStyle2sArray' для 'linkedStyle2sArray' нормально отдает массив:
Не могли бы Вы подсказать в чем может быть дело?
Внимательнее прочитал начало статьи и нашел ошибку.
В методе
я получал массив вида [индекс стиля] => Имя стиля, а надо ведь [индекс стиля] => Индекс стиля:
Спасибо за статью! )
Спасибо огромное за статью.
Убил целый день на MANY_MANY, а после прочтения написал за полчаса.
Подскажите пожалуйста как и куда подключить DMultiplyListBehavior.php?
Разобрался.
Работает почти как мне надо. =) Буду дальше разбираться.
Спасибо за статью, разобрался, сделал как мне было нужно! =)
Дмитрий, здравствуйте.
Спасибо за проделанную работу, ваше поведение очень помогло реализовать нужную мне задачу.
Но есть одно "но", а именно: не могу удалить сущность, имеющую связь MANY_MANY.
У меня есть модель PlaceType, у которой связь MANY_MANY с моделью Features. Так вот, когда я из CGridView или из экшна View пытаюсь удалить запись PlaceType, то удаляются только ее Features, а сам PlaceType остается. Если закомментировать метод:
То все отрабатывает нормально, в ином случае процедура удаления не заходит дальше метода deleteRelations().
Сам пока не понимаю, почему так.
У всех нормально?
Забыли return:
А лучше:
В варианте с явным геттером ошибка - возвращается массив моделей значений, а не массив первичных ключей, из-за чего при редактировании чекбоксы оказываются не отмеченными, хотя в базе сохранены.
Где именно?
но, checkBoxList в categoriesArray ждет массив ID, которые нужно выделить...
Этим и занимается метод CHtml::listData.
Спасибо за статью. Подскажите как сделать чтобы id у таблицы post_category не менялся, т.к. эту связь использую в другой таблице и там id постоянен?
Еще уточняю, имеется ввиду не удалять старые связи а обновить их. Как это сделать?
Сделал так
Все работает. но может еще есть варианты, буду признателен вам если вы из предложите.
Здравствуйте, такой вопрос.
Сделал с помощью Вашего класса DMultiplyListBehavior.
Все хорошо работает, только с должностями, т.е. у каждого человека есть одна или более должностей.
Теперь я вывожу в CGridView, ФИО сотрудника и его список должностей, только должности выводятся кодами, а не названиями. Как сделать так чтобы были названия? Не получается, кроме как делать запрос к справочнику должностей, для каждого сотрудника. (но так получается очень много запросов)
Действительно, не очень удобно использовать отношения с жадной загрузкой, когда нужно связать несколько таблиц, у каждой из которых ещё есть и переводы. Но это неизбежная цена гибкой мультиязычности.
Можно использовать не checkbox, а расширение Chosen.
Тогда все будет работать как написано в вашем первоначальном варианте без необходимости создавать дополнительных переменных.
Кстати очень полезное расширение для большого количества checkbox.
Полезная статья.
Думаю воспользоваться.
Но у меня категории имеют подкатегории.
Вопрос:
Как вывести иерархическое дерево категорий с чекбоксами,
через $forn->checkBoxList($model, 'categoriesArray',...); ?
Я в последнее время предпочитаю Nested Set для иерархических вещей. А так можно в getCategoriesArray() рекурсией построить массив и сделать отступы у названий категорий дефисами.
Спасибо!
А если несколько таких много ко многим чекбоксов, можно ли как то прицепить к одному поведению или нужно создавать несколько разных поведений и геттеров,, сеттеров?
Несколько поведений:
Спасибо большое, а геттеры и сеттеры тоже дублировать надо для брэндов и ложить информацию о них в rules?
Да. Так Вы геттеры используете или поведение?
Спасибо, это я невнимателен, не понял сразу что у вас 2 подхода к реализации)
Меня интересует этот участок
// обрабатываем новый массив $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 ....
Насчет пункта 3 - нашел на github , а вот первые 2 до сих пор не понял.
1. Да, обрабатываем categoriesArray как в последнем коде модели Post.
2. Свойство categoriesArray формирует само поведение на основе связи. Самим ничего делать не надо.
Добрый день, а есть ли реализация подобного для yii2?
Я точно не делал. Может кто-то и переписал.
Кстати, Дмитрий, почему не использовался составной ключ в данном случае? Мне кажется что так было бы проще
Можно и с составным. Разницы особо нет.
Здравствуйте!
Поробовал использовать несколько поведения для нескольких "много ко многим"
Связи от товара выставлены:
В refresh продублировал 3 раза кусок с удалением и заполнением в связочные:
Но когда открываю страницу в админке, где эти чекбоксы быть должны, вылетает ошибка:
Не определено свойство "Product.milledArray". (аналог Вашего CategoriesArray)
Попробовал в классе Product выставить:
Страница открылась, но чекбоксы , которые должны быть выделены, почему-то не выделены
В чем может быть причина?
И еще в форме сделал:
Но все равно, как то пусто, галочки не выводятся( Пробовал php быдлоспособом с циклом и условием на наличие связи, там все ок, связи правильно в таблицах хранятся. Где же я ошибся?
Вы ключ 'DMultiplyListBehavior' в behaviors три раза повторили.
Все разобрался, вы маг и волшебник, а я не внимательный ученик чародея. Еще раз спасибо за отличные статьи, с большим удовольствием изучаю их! Один вопрос. Эти массивы (выделено - не выделено), они же формируются в поведении?
Да, в нём.
Добрый день, немного расширил поведение под свои нужды, добавил несколько методов. Вот к примеру beforeDelete:
Почему get_class($this->owner) выводит DMultiplyListBehavior (имя класса поведения) , а не класс, к которому подключено поведение, модель которого я удаляю?!
вот моя реализация
http://des1roer.blogspot.ru/2015/03/yii-yii-for-dummies-chtmlcheckboxlist.html
Добрый день! Ваш блог очень помог, так как до этого я сделал немножко некрасивое решение. Пробую на Yii2. Остановился на варианте геттер-сеттер. Прописал в rules, чтобы свойство было безопасным. Но когда форму заполняю данными модели (у меня $newsEditForm->load($news)) то свойство categoriesArray в форме не заполняется. Если из модели вызвать напрямую $news->categoriesArray - то все есть. При выводе $news->getAttributes() его тоже нету. Подскажите в каком направлении копать?
Весьма странно. А в форме нормально поле выводится?
В форму я передаю так
Понятно. Из getAttributes() это поле не возвращается, поэтому в сеттер ничего не приходит.
Поэтому либо присваивайте прямо из POST-параметров:
либо как у Вас или вручную:
На сколько я понимаю, этот геттер не даст сохранить пустой массив (удалить все категории, которые прежде были заданы). Есть идеи как это лучше обойти?
Почему не даст?
Как я понимаю если в этот сеттер придет пустой массив (все чекеты сняты):
то в геттере выполнится условие if ($this->categories_array===null)
соответственно в методе обновления категорий в качестве сохраняемого массива $categories мы получим данные из базы, а не из формы:
Yii добавляет пустое hidden-поле перед чекбоксами, так что из формы присвоится пустая строка, а не null.
Здравствуйте, Дмитрий.
Что Вы имеете ввиду - 'Но есть риск через несколько веков переполнить индекс типа INT... Хотя в блоге это мало кому грозит' ?
Вы ошибаетесь. Спасибо и удачи.
у Вас ошибочка, точка с запятой в массиве:
Здравствуйте! Имеются таблицы "Дисциплины" и "Группы". Я создал форму, в которой checkboxlist (Группы) является зависимым от значения dropdownlist (Дисциплины). Список чекбоксов генерируется и обновляется, чекбоксов может быть от 1 до n. Но данные не заносятся в БД, даже когда выбран один чекбокс. Интересует вопрос как можно сделать вставку от выбранных чекбоксов, а именно нескольких строк в таблицу, где поля будут отличаться только ID и IDГруппы. Вставка нескольких строк за один запрос. Извините, может замудрил с вопросом. Заранее спасибо.
Обычным циклом foreach. Похожее с сохранением есть в вебинаре по связям.