Закрыть ... [X]

Мотивом к написанию данного материала послужила статья на Хабре Сохранение «много ко многим» в Yii2 через поведение.

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

Итак, что нам дано. Есть три таблицы: post, tag и post_has_tag, для них сгенерируем три модели Post, Tag и PostHasTag, таблицы например такие:

CREATE TABLE IF NOT EXISTS `post` ( `id` int(11) NOT NULL AUTO_INCREMENT, `title` varchar(255) DEFAULT NULL, `alias` varchar(255) NOT NULL, `text` text, `date_create` int(11) DEFAULT NULL, `status` tinyint(1) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE IF NOT EXISTS `tag` ( `id` int(11) NOT NULL AUTO_INCREMENT, `title` varchar(255) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE IF NOT EXISTS `post_has_tag` ( `post_id` int(11) NOT NULL, `tag_id` int(11) NOT NULL, PRIMARY KEY (`post_id`,`tag_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;

Код моделей приводить не буду, думаю, что генерация моделей с помощью Gii не предсталяет сложности. Последняя таблица представляет собой таблицу связи между постами и тегами, и у нее есть всего два поля: post_id и  tag_id, исходя из названий полей понятно что там должно храниться. Вот с этой таблицей мы и будем работать, поведение должно уметь отдавать нужные данные из этой таблицы, а также сохранять и при необходимости удалять их. Да, и в поведение в качестве параметров нужно передавать имя связи и имя атрибута модели.

Что бы хотелось: в первую очередь это простота использования, ну и чтобы все сохранялось, обновлялось, удалялось, транзакции чтобы были, вот собственно и все. Да, и речь здесь пойдет именно о типе связи «many to many» через таблицу связи, в которой только два столбца - значения первичных ключей связанных таблиц. Поведения для работы со связями один к одному и один ко многим мы напишем в следующий раз.

Первое что нужно сделать, это в модели Post обозначить связь многие ко многим через таблицу связи post_has_tag, согласно документации Yii2 это делается следующим образом:

public function getTags() { return $this->hasMany(Tag::className(), ['id' => 'tag_id']) ->viaTable('post_has_tag', ['post_id' => 'id']); }

Далее необходимо в той же модели Post объявить поведение, где tags - это имя вышеобозначенной связи, а tag_list - имя несуществующего пока атрибута модели, как он в модели потом появится будет рассказано чуть ниже, подключение очень простое:

public function behaviors() { return [ [ 'class' => \app\components\behaviors\ManyHasManyBehavior::className(), 'relations' => [ 'tags' => 'tag_list', ], ], ] }

Ну и чтобы закончить с моделью, в правилах валидации добавим safe - атрибут tag_list:

public function rules() { return [ [['tag_list'], 'safe'] ] }

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

Где:

  • tag_list - имя атрибута модели
  • items - массив тегов, отсортированых по имени, и с помощью метода хелпера ArrayHelper::map приведеный к виду ключ (id) - значение (title):
Chosen::widget([ 'model' => $model, 'attribute' => 'tag_list', 'items' => ArrayHelper::map( Tag::find()->select('id, title')->orderBy('title')->asArray()->all(), 'id', 'title' ), 'multiple' => true, ]);

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

// Массив связей, который передается в объявлении поведения в модели public $relations = []; // Массив значений атрибутов модели, в нашем случае это tag_list private $_values = [];

Далее создадим метод events() и назначим в нем обработчики событий. В нашем случае обработчик событий будет один - changeRelations.

public function events() { return [ ActiveRecord::EVENT_AFTER_INSERT => 'changeRelations', ActiveRecord::EVENT_AFTER_UPDATE => 'changeRelations', ActiveRecord::EVENT_BEFORE_DELETE => 'changeRelations', ]; }

Магический метод __get() будет возвращать массив айдишников тегов, принадлежащих соответствующему посту:

public function __get($name) { if (isset($this->_values[$name])) { return $this->_values[$name]; } else { $relation = $this->owner->getRelation(array_search($name, $this->relations)); $relationModel = new $relation->modelClass(); return $relation->select($relationModel->getPrimaryKey())->column(); } }

Магический метод __set() устанавливает приватное свойство $this->_values:

public function __set($name, $value) { $this->_values[$name] = $value; }

Далее перегрузим два метода из yii\base\Object canGetProperty() и canSetProperty() которые как раз разрешают читать из и писать в несуществующее свойство модели:

public function canGetProperty($name, $checkVars = true) { return in_array($name, $this->relations) ? true : parent::canGetProperty($name, $checkVars); } public function canSetProperty($name, $checkVars = true, $checkBehaviors = true) { return in_array($name, $this->relations) ? true : parent::canSetProperty($name, $checkVars, $checkBehaviors); }

Ну и последний метод в нашем поведении, который сохраняет и удаляет данные из связанной модели. Метод документирован.

public function changeRelations($event) { if (is_array($ownerPk = $this->owner->getPrimaryKey())) { throw new ErrorException("Составные первичные ключи не поддерживаются"); } // Сохраняем данные в таблицу связи foreach ($this->relations as $relationName => $attributeName) { $relation = $this->owner->getRelation($relationName); $relationModel = new $relation->modelClass(); // Если связь типа "многие ко многим" if (!empty($relation->via) && $relation->multiple) { // Имя таблицы связи list($junctionTable) = array_values($relation->via->from); // Имя первичного ключа владельца связи list($ownerColumn) = array_keys($relation->via->link); // Имя первичного ключа связанной таблицы list($relationPrimaryColumn) = array_keys($relation->link); // Имя поля таблицы связи для записи значений связанной таблицы list($junctionColumn) = array_values($relation->link); // Стартуем транзакцию $transaction = Yii::$app->db->beginTransaction(); try { // Если есть значения для записи if (!empty($this->_values[$attributeName])) { // Значения из базы до сохранения $oldValues = ArrayHelper::getColumn(ArrayHelper::toArray($this->owner->$relationName), $relationPrimaryColumn); // Новые значения $newValues = $this->_values[$attributeName]; // Массив новых значений для множественной вставки $insert = []; foreach ($newValues as $newValue) { if (!in_array($newValue, $oldValues)) { $insert[] = [$newValue, $ownerPk]; } } // Если есть что вставлять if (count($insert)) { Yii::$app->db->createCommand() ->batchInsert($junctionTable, [$junctionColumn, $ownerColumn], $insert) ->execute(); } // Удаляем лишнее Yii::$app->db->createCommand() ->delete($junctionTable, $ownerColumn.'="'.$ownerPk.'" AND '.$junctionColumn.' NOT IN ('.implode(',',$newValues).')') ->execute(); } else { // Удаляем все связанные данные Yii::$app->db->createCommand() ->delete($junctionTable, $ownerColumn.'='.$ownerPk) ->execute(); } $transaction->commit(); } catch (\yii\db\Exception $ex) { $transaction->rollback(); throw $ex; } } else { throw new ErrorException('Relationship type not supported.'); } } }

В завершении полный код нашего поведения, который для примера можно поместить в папку components\behaviors текущего приложения:

namespace app\components\behaviors; use Yii; use yii\db\ActiveRecord; use yii\base\ErrorException; use yii\helpers\ArrayHelper; / Class ManyHasManyBehavior @package common\components\behaviors Usage: 1. In your model, add the behavior and configure it: public function behaviors() { return [ [ 'class' =--> \common\components\behaviors\ManyHasManyBehavior::className(), 'relations' => [ 'tags' => 'tag_items', ], ], ]; } where 'tags' - name of relation, for example: public function getTags() { return $this->hasMany(Tag::className(), ['id' => 'tag_id'])->viaTable('post_has_tag', ['post_id' => 'id']); } 'tag_list' - name of attribute (the attributes are created automatically in your model) 2. In your model, add validation rules for the attributes created by the behavior, for example: public function rules() { return [ [['tag_list'], 'safe'] ]; } 3. In your view, create form fields for the attributes / class ManyHasManyBehavior extends \yii\base\Behavior { / List of relations. @var array / public $relations = []; / List values of relation attributes. @var array / private $_values = []; / Events list @return array / public function events() { return [ ActiveRecord::EVENT_AFTER_INSERT => 'changeRelations', ActiveRecord::EVENT_AFTER_UPDATE => 'changeRelations', ActiveRecord::EVENT_BEFORE_DELETE => 'changeRelations', ]; } / Save all dirty (changed) relation values ($this->_values) to the database @param $event @throws ErrorException @throws \yii\db\Exception / public function changeRelations($event) { if (is_array($ownerPk = $this->owner->getPrimaryKey())) { throw new ErrorException("This behavior does not support composite primary keys"); } // Save relations data foreach ($this->relations as $relationName => $attributeName) { $relation = $this->owner->getRelation($relationName); $relationModel = new $relation->modelClass(); // If the relation is many-to-many if (!empty($relation->via) && $relation->multiple) { // Table of junction list($junctionTable) = array_values($relation->via->from); // Column of owner table list($ownerColumn) = array_keys($relation->via->link); // Column of relation table list($relationPrimaryColumn) = array_keys($relation->link); // Column of junction table list($junctionColumn) = array_values($relation->link); $transaction = Yii::$app->db->beginTransaction(); try { if (!empty($this->_values[$attributeName])) { $oldValues = ArrayHelper::getColumn(ArrayHelper::toArray($this->owner->$relationName), $relationPrimaryColumn); $newValues = $this->_values[$attributeName]; $insert = []; foreach ($newValues as $newValue) { if (!in_array($newValue, $oldValues)) { $insert[] = [$newValue, $ownerPk]; } } if (count($insert)) { Yii::$app->db->createCommand() ->batchInsert($junctionTable, [$junctionColumn, $ownerColumn], $insert) ->execute(); } Yii::$app->db->createCommand() ->delete($junctionTable, $ownerColumn.'="'.$ownerPk.'" AND '.$junctionColumn.' NOT IN ('.implode(',',$newValues).')') ->execute(); } else { Yii::$app->db->createCommand() ->delete($junctionTable, $ownerColumn.'='.$ownerPk) ->execute(); } $transaction->commit(); } catch (\yii\db\Exception $ex) { $transaction->rollback(); throw $ex; } } else { throw new ErrorException('Relationship type not supported.'); } } } / Returns a value indicating whether a property can be read. We return true if it is one of our properties and pass the params on to the parent class otherwise. TODO: Make it honor $checkVars ?? @param string $name the property name @param boolean $checkVars whether to treat member variables as properties @return boolean whether the property can be read @see canSetProperty() / public function canGetProperty($name, $checkVars = true) { return in_array($name, $this->relations) ? true : parent::canGetProperty($name, $checkVars); } / Returns a value indicating whether a property can be set. We return true if it is one of our properties and pass the params on to the parent class otherwise. TODO: Make it honor $checkVars and $checkBehaviors ?? @param string $name the property name @param boolean $checkVars whether to treat member variables as properties @param boolean $checkBehaviors whether to treat behaviors' properties as properties of this component @return boolean whether the property can be written @see canGetProperty() / public function canSetProperty($name, $checkVars = true, $checkBehaviors = true) { return in_array($name, $this->relations) ? true : parent::canSetProperty($name, $checkVars, $checkBehaviors); } / Returns the value of an object property. Get it from our local temporary variable if we have it, get if from DB otherwise. @param string $name the property name @return mixed the property value @see __set() / public function __get($name) { if (isset($this->_values[$name])) { return $this->_values[$name]; } else { $relation = $this->owner->getRelation(array_search($name, $this->relations)); $relationModel = new $relation->modelClass(); return $relation->select($relationModel->getPrimaryKey())->column(); } } / Sets the value of a component property. The data is passed @param string $name the property name or the event name @param mixed $value the property value @see __get() / public function __set($name, $value) { $this->_values[$name] = $value; } }

Подводя итог вышенаписанному можно сазать, что мы получили поведение согласно нашим требованиям. Простое в использовании и выполняющее все, что было задумано :).

 


Источник: http://fancode.ru/post/yii2-behaviors-many-to-many



Связанные данные - Yii Framework Вязание спицами следки тапочки с описанием видео


Связанные данные yii2 Связанные данные yii2 Связанные данные yii2 Связанные данные yii2 Связанные данные yii2 Связанные данные yii2

СЕЙЧАС ЧИТАЮТ