[Из песочницы] Сохранение «много ко многим» в Yii2 через поведение

bbc47933552e4c90846c80e731b06ddf.jpg Если вам приходилось работать с Yii2, наверняка возникала ситуация, когда нужно было сохранить связь «много ко многим».Когда становилось ясно, что в сети еще нет поведений для работы с этим типом связи, тогда нужный код писался на событии «after save» и с напутствием «ну работает же» отправлялся в репозиторий.

Лично меня не устраивал такой расклад событий. Я решил написать то самое волшебное поведение, которого так не хватает в официальной сборке Yii2.

УстановкаУстанавливаем через Composer: php composer.phar require --prefer-dist voskobovich/yii2-many-many-behavior »*» Или добавляем в composer.json своего проекта в раздел «require»: «voskobovich/yii2-many-many-behavior»:»*» Выполняем: # php composer.phar update Исходники: yii2-many-many-behavior.Как пользоваться? Создаем в модели новый атрибут: public $users_list = array (); Он будет хранить массив идентификаторов которые нужно привязать к нашей модели.На эти свойства вешаются поля формы: field ($model, 'users_list') →dropDownList (User: getListData (), ['multiple' => true]) ?> Далее добавляем это свойство в правила валидации: public function rules () { return [ [['users_list'], 'safe'] ]; } Это нужно, чтобы позволить заполнять его с формы через setAttributes ().Внимание!

Стоит учесть, что в массиве, который возвращает getAttributes (), не будет свойства «users_list». Для того, чтобы он там появился, нужно переопределить метод getAttributes () вашей модели вот так:

public function getAttributes ($names = null, $except = []) { $attributes = parent: getAttributes ($names = null, $except = []); return array_replace ($attributes, [ 'users_list' => $this→users_list ]); } Дальше нужно подключить поведение в модель:

public function behaviors () { return [ [ 'class' => \voskobovich\behaviors\MtMBehavior: className (), 'relations' => [ 'users' => 'users_list', 'tasks' => [ 'tasks_list', function ($tasksList) { return array_rand ($tasksList, 2); } ] ], ], ]; } В этом примере описано две связи: «users» и «tasks».В первую связь будет сохранен массив, который придет в атрибут «users_list» с формы, а во вторую связь будет сохранено только два случайных идентификатора из массива «tasks_list».Надеюсь, понятно.Как работает? Опытные разработчики могут дальше не читать, а для начинающих — рассказываю.Что такое поведение? Вот определение из официальной документации:

Поведения (behaviors) — это экземпляры класса [[yii\base\Behavior]] или класса, унаследованного от него. Поведения, также известные как примеси, позволяют расширять функциональность существующих [[yii\base\Component|компонентов]] без необходимости изменения дерева наследования. После прикрепления поведения к компоненту, его методы и свойства «внедряются» в компонент, и становятся доступными так же, как если бы они были объявлены в самом классе компонента. Кроме того, поведение может реагировать на события, создаваемые компонентом, что позволяет тонко настраивать или модифицировать обычное выполнение кода компонента.

Наше поведение должно реагировать на два события: После создания модели (EVENT_AFTER_INSERT) После изменения модели (EVENT_AFTER_UPDATE) На оба события один и тот же обработчик, так как логика одинаковая.Объявляем события и обработчик в нашем поведении.Метод events () вызывается фреймворком и заставляет поведение «работать».

/** * Events list * @return array */ public function events () { return [ ActiveRecord: EVENT_AFTER_INSERT => 'saveRelations', ActiveRecord: EVENT_AFTER_UPDATE => 'saveRelations', ]; }

/** * Save relations value in data base * @param $event * @throws ErrorException * @throws \yii\db\Exception */ public function saveRelations ($event) { $component = $event→sender; $safeAttributes = $component→safeAttributes ();

foreach ($this→relations as $relationName => $source) { if (array_search ($relationName, $safeAttributes) === NULL) throw new ErrorException («Relation \»{$relationName}\» must be safe attributes»);

if (is_array ($component→getPrimaryKey ())) throw new ErrorException («This behavior not supported composite primary key»);

$relation = $component→getRelation ($relationName);

if (empty ($relation→via)) throw new ErrorException («Attribute \»{$relationName}\» is not relation»);

list ($junctionTable) = array_values ($relation→via→from); list ($relatedColumn) = array_values ($relation→link); list ($junctionColumn) = array_keys ($relation→via→link);

// Get relation keys of attribute name if (is_string ($source) && isset ($component→{$source})) $relatedPkCollection = $component→{$source}; elseif (is_array ($source)) { list ($attributeName, $callback) = $source;

if (isset ($component→{$attributeName})) { $relatedPkCollection = (array)call_user_func ($callback, $component→{$attributeName}); $component→{$attributeName} = $relatedPkCollection; } }

// Save relations data if (! empty ($relatedPkCollection)) { $transaction = Yii::$app→db→beginTransaction (); try { $connection = Yii::$app→db; $componentPk = $component→getPrimaryKey ();

// Remove relations $connection→createCommand () →delete ($junctionTable,»{$junctionColumn} = : id», [': id' => $componentPk]) →execute ();

// Write new relations $junctionRows = array (); foreach ($relatedPkCollection as $relatedPk) array_push ($junctionRows, [$componentPk, $relatedPk]);

$connection→createCommand () →batchInsert ($junctionTable, [$junctionColumn, $relatedColumn], $junctionRows) →execute ();

$transaction→commit (); } catch (\yii\db\Exception $ex) { $transaction→rollback (); } } } } В обработчике событий saveRelations () скрипт проходит по всем описаным связям, делает ряд важных проверок и далее в два шага сохраняет связь: Удаляет старые связи в которых есть иденификатор нашей модели Записывает новые Для удаления старых связей используется один запрос в БД и, благодаря batchInsert (), для записи новых используется тоже один запрос.Обработка каждой связи обернута в транзакцию для безопасного сохранения данных.При исключении связь просто не сохранится, пользователь не увидит ошибки.С обновлением и сохранением разобрались, но как заполнить наш атрибуты «users_list» при выборке модели из базы в следующий раз? Здесь нам поможет событие «После выборки» (EVENT_AFTER_FIND), которое будет после выборки модели из базы подтягивать все перечисленные связи и по ним заполнять наши атрибуты.

Добавляем еще одно событие в events ():

public function events () { return [ ActiveRecord: EVENT_AFTER_INSERT => 'saveRelations', ActiveRecord: EVENT_AFTER_UPDATE => 'saveRelations', ActiveRecord: EVENT_AFTER_FIND => 'loadRelations' ]; } Пишем обработчик loadRelations (): public function loadRelations ($event) { $component = $event→sender; list ($primaryKey) = $component: primaryKey ();

foreach ($this→relations as $relationName => $source) { if (is_array ($source)) list ($attributeName) = $source; else $attributeName = $source;

$relation = $component→getRelation ($relationName);

if (! is_null ($relation)) { $relatedModels = $relation→indexBy ($primaryKey)→all (); $component→{$attributeName} = array_keys ($relatedModels); } } } Снова же проходимся по массиву объявленных связей, выгружаем по ним модели. При помощи indexBy () формируем выборку так, чтобы primary key моделей были в ключах коллекции. Далее, используя array_keys (), получаем ключи коллекции и присваиваем их нашим созданным свойствам. Таким образом мы восстанавливаем значения свойств модели и получаем на форме правильно выделенные пункты в multi select.Стоит учесть, что при выгрузке нашей модели выбираются и записи по связям. Так что рекомендую использовать жадную загрузку для уменьшения количества запросов к базе.

На этом у меня все.

Спасибо за внимание!

© Habrahabr.ru