Many-to-Many Conservation in Yii2 Through Behavior

If you had to work with Yii2, you probably had a situation where you needed to maintain a many-to-many relationship.

When it became clear that the network did not yet have behaviors for working with this type of connection, then the necessary code was written at the “after save” event and with the parting word “well, it works” was sent to the repository.

Personally, I was not happy with this alignment of events. I decided to write the very magical behavior that is so lacking in the official Yii2 build.

Installation


Install via Composer:
php composer require --prefer-dist voskobovich/yii2-many-many-behavior "~3.0"

Or add to composer.json your project in the “require” section:
"voskobovich/yii2-many-many-behavior": "~3.0"

We carry out:
php composer update

Sources on GitHub .

How to use?


For an example we will take the popular type of communication the Publication and Categories.

Connect the behavior to Publish.
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 accept an array of primary category keys that came from a form or by API.

Behavior can be configured to work with multiple relationships at once. For example, a Publication may have a relationship 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 indicate them in the safe group, and you're done.

Now create a field in the view for selecting categories.
field($model, 'category_ids')->dropDownList(Category::listAll(), ['multiple' => true]) ?>

I have been using the listAll () method in my projects for a long time and now I have the opportunity to share it. It is great for filling multi-selects in GridView forms and filters.

Everything, after these manipulations, Categories should easily be attached to the Publication.

What about optimization and security?


The request to create 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 go away.
All the logic for link management is wrapped in a transaction.

Further more


Quite often, the task goes beyond the standard “save / get” related models. For such tasks, advanced settings are provided in the behavior.

Custom getters and setters


Often, for various js plugins, you need to be able to send data to JSON or a string of the form “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 category_ids , category_ids_json and category_ids_string attributes . As you can see 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.
Open in the documentation .

Manage Spanning Table Field Values


Often the connection contains not only primary keys, but also additional information. For example: creation date or sort 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) {
                            // Первая в категория будет главной
                            return array_search($relatedPk, $model->category_ids) === 0;
                        },
                    ],
                ]
            ],
        ],
    ];
}

Open in the documentation .

Setting the default value for orphaned models


I understand that the title sounds strange, but it's the right thing.
The fact is that behavior can work not only with the many-to-many relationship, but also with the one-to-many relationship.
In the first case, records from the link table are simply deleted and new ones are written in their place.
In the second type of communication, it is understood that first you need to make the related models orphans (untie), and then shelter them back (tie).
As a result, some models may remain orphans and need to be placed in a kind of “Archive”. Just for setting the owner of all orphaned records, the default parameter was created . If you do not specify it, then the entries in the connecting field will remain null.
public function behaviors()
{
    return [
        [
            'class' => \voskobovich\behaviors\ManyToManyBehavior::className(),
            'relations' => [
                'category_ids' => [
                    'categories',
                    'default' => 17,
                ]
            ],
        ],
    ];
}

Open in the documentation .

Deletion Condition from Spanning Table


Often records of the same structure but of different types are stored in the link table.
For example: in the table product_has_attachment there are photos and prices of the goods. Each type of attachment has its own connection.
But what happens if we add a new price to the product? All entries 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 ... because there were not only price lists, but also photos ... hell!
To prevent this from happening, 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 adding a new price list, only the list of price lists will be affected, while the pictures will remain untouched.
Open in the 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 easier.
If so, put the stars on github and recommend it to your friends, because there are still those who have not heard of him and continue to “make crutch bikes.”

Also popular now: