
If you had to work with Yii2, there must have been a situation when it was necessary to keep the “many-to-many” relationship.
When it became clear that there were no behaviors on the network to work with this type of connection, then the necessary code was written on the “after save” event and with the “well works” message was sent to the repository.
Personally, I did not like this alignment of events. I decided to write the same magical behavior, which is so lacking in the official assembly of Yii2.
Installation
Install via Composer:
php composer require --prefer-dist voskobovich/yii2-many-many-behavior "~3.0"
Or add to your project’s
composer.json in the “require” section:
"voskobovich/yii2-many-many-behavior": "~3.0"
We carry out:
php composer update
Sources on
github .
')
How to use?
For example, take the popular form of communication Publication and Categories.
We connect behavior to the Publication.
class Post extends ActiveRecord { ... public function rules() { return [ [['category_ids'], 'each', 'rule' => ['integer']], ... ]; } public function behaviors() { return [ [ 'class' => \voskobovich\behaviors\ManyToManyBehavior::className(), 'relations' => [ 'category_ids' => 'categories', ], ], ]; } public function getCategories() { return $this->hasMany(Category::className(), ['id' => 'category_id']) ->viaTable('{{%post_has_category}}', ['post_id' => 'id']); } public static function listAll($keyField = 'id', $valueField = 'name', $asArray = true) { $query = static::find(); if ($asArray) { $query->select([$keyField, $valueField])->asArray(); } return ArrayHelper::map($query->all(), $keyField, $valueField); } ... }
The behavior will create a new
category_ids attribute in the model. It will take an array of primary key categories from the form or by API.
Behavior can be configured to work with several links at once. For example, a Publication may be associated with Categories, Tags, Users, Images, etc.
'relations' => [ 'category_ids' => 'categories', 'user_ids' => 'users', 'tag_ids' => 'tags', ... ]
All attributes created by the behavior
must be mentioned in the validation rules. Try to write meaningful rules, and not specify them in the group “safe” and ready.
Now we will create in the view a field for selecting categories.
<?= $form->field($model, 'category_ids')->dropDownList(Category::listAll(), ['multiple' => true]) ?>
I have been using the
listAll () method for a long time in my projects and now I have the opportunity to share it. It is great for filling out multi-selections in GridView forms and filters.
Everything after these manipulations Categories should be attached to Publications without any problems.
What about optimization and security?
The request for the formation of a list of primary keys occurs
only at the moment of reading the property, and not when selecting a model. Until you contact him - the request will not leave.
The entire logic of link management behavior is wrapped in a transaction.
Further more
Quite often, the task goes beyond the standard “save / get” related models. For such tasks, the behavior provides advanced settings.
Custom getters and setters
Often, for different js plugins, you need to be able to give data in JSON or a string like "1,2,3,4". Customize the behavior:
public function behaviors() { return [ [ 'class' => \voskobovich\behaviors\ManyToManyBehavior::className(), 'relations' => [ 'category_ids' => [ 'categories', 'fields' => [ 'json' => [ 'get' => function($value) { return JSON::encode($value); }, 'set' => function($value) { return JSON::decode($value); }, ], 'string' => [ 'get' => function($value) { return implode(',', $value); }, 'set' => function($value) { return explode(',', $value); }, ], ], ] ], ], ]; }
With this configuration, the model will have 3 new attributes
category_ids ,
category_ids_json and
category_ids_string . As can be seen from the configuration, you can not only change the format of outgoing data, but also process the data included in the attribute. For example, parse a string or JSON into an array of primary keys.
View in documentation .
Managing field values of the link table
Often the link contains not only primary keys, but also additional information. For example: creation date or sorting order. In this case, the behavior can also be configured:
public function behaviors() { return [ [ 'class' => \voskobovich\behaviors\ManyToManyBehavior::className(), 'relations' => [ 'category_ids' => [ 'categories', 'viaTableValues' => [ 'status_key' => PostHasCategory::STATUS_ACTIVE, 'created_at' => function() { return new \yii\db\Expression('NOW()'); }, 'is_main' => function($model, $relationName, $attributeName, $relatedPk) {
View in documentation .
Setting default values for orphaned models
I understand, the headline sounds weird, but it's the right thing.
The fact is that behavior can work not only with a many-to-many connection, but also with one-to-many.
In the first case, the records from the link table are simply deleted and new ones are written in their place.
In the second type of connection, it is implied that you first need to make the connected models orphaned (untie), and then shelter them back (tie).
As a result, some models can remain orphans and need to be placed in a certain “Archive”. Just to configure the owner of all orphaned records and created the
default parameter. If you do not specify it, then the entries in the binding field will be null.
public function behaviors() { return [ [ 'class' => \voskobovich\behaviors\ManyToManyBehavior::className(), 'relations' => [ 'category_ids' => [ 'categories', 'default' => 17, ] ], ], ]; }
View in documentation .
The removal condition from the link table
Often in the connecting table are stored records of the same structure but of different types.
For example: in the
product_has_attachment table there are photos and prices of the goods. For each type of attachment has its own connection.
But what will happen if we add a new price to the product? All records from the
product_has_attachment table associated with this product will be
destroyed and old prices + new will be written in their place.
But ... but ... after all, there were not only prices, but also photos ... hell!
To avoid this, you need to configure the behavior:
class Product extends ActiveRecord { ... public function behaviors() { return [ [ 'class' => \voskobovich\behaviors\ManyToManyBehavior::className(), 'relations' => [ 'image_ids' => [ 'images', 'viaTableValues' => [ 'type_key' => ProductHasAttachment::TYPE_IMAGE, ], 'customDeleteCondition' => [ 'type_key' => ProductHasAttachment::TYPE_IMAGE, ], ], 'priceList_ids' => [ 'priceLists', 'viaTableValues' => [ 'type_key' => ProductHasAttachment::TYPE_PRICE_LIST, ], 'customDeleteCondition' => [ 'type_key' => ProductHasAttachment::TYPE_PRICE_LIST, ], ] ], ], ]; } public function getImages() { return $this->hasMany(Attachment::className(), ['id' => 'attachment_id']) ->viaTable('{{%product_has_attachment}}', ['product_id' => 'id'], function ($query) { $query->andWhere([ 'type_key' => ProductHasAttachment::TYPE_IMAGE, ]); return $query; }); } public function getPriceLists() { return $this->hasMany(Attachment::className(), ['id' => 'attachment_id']) ->viaTable('{{%product_has_attachment}}', ['product_id' => 'id'], function ($query) { $query->andWhere([ 'type_key' => ProductHasAttachment::TYPE_PRICE_LIST, ]); return $query; }); } ... }
Thus, when a new price list is added, only the list of price lists will be affected, the pictures will remain untouched.
View in documentation .
This article reflects only part of the functional behavior.
For more accurate information, I recommend looking at the
documentation .
Moreover, I update the article less often than the README repository.
I sincerely hope that my behavior makes working with connections easier and simpler.
If this is the case, put the stars on github and recommend it to your friends, because there are still those who have not heard of it and continue to “rebuild crutch bikes”.