This behavior makes it easy to maintain many-to-many and one-to-many relations in your ActiveRecord models.
- In your model, add the behavior and configure it
- In your model, add validation rules for the attributes created by the behavior
- In your view, create form fields for the attributes
As an example, let's assume you are dealing with entities like Book, Author and Review. The Book model has the following relationships:
public function getAuthors()
{
return $this->hasMany(Author::className(), ['id' => 'author_id'])
->viaTable('book_has_author', ['book_id' => 'id']);
}
public function getReviews()
{
return $this->hasMany(Review::className(), ['id' => 'review_id']);
}In the same model, the behaviour can be configured like so:
public function behaviors()
{
return [
[
'class' => \voskobovich\behaviors\ManyToManyBehavior::className(),
'relations' => [
'author_list' => 'authors',
'review_list' => 'reviews',
],
],
];
}Relation names don't need to end in _list, and you can use any name for a relation. It is recommended to use meaningful names, though.
Attributes lik author_list and review_list in the Book model are created automatically. By default, they are configured to accept data from a standard select input (see below). However, it is possible to use custom getter and setter functions, which may be useful for interaction with more complex frontend scripts:
...
'author_list' => [
'authors',
'get' => function($value) {
return JSON::encode($value);
},
'set' => function($value) {
return JSON::decode($value);
},
]
...The setter function receives whatever data comes through the $_REQUEST and is expected to return the array of the related model IDs. The getter function receives the array of the related model IDs.
For seting additional values in junction table (apart columns required for relation), you can use viaTableValues:
...
'author_list' => [
'authors',
'viaTableValues' => [
'status' => 123,
'created_at' => function($model, $relationName, $attributeName) {
return new \yii\db\Expression('NOW()');
},
],
]
...When one-to-many relations are saved, old links are removed and new links are created. To remove an old link, the corresponding foreign-key column is set to a certain value. It is NULL by default, but can be configured differently. Note that your database must support your chosen default value, so if you are using NULL as a default value, the field must be nullable.
You can supply a constant value like so:
...
'review_list' => [
'reviews',
'default' => 17,
],
...It is also possible to assign the default value to NULL explicitly, like so: 'default' => null. Another option is to provide a function to calculate the default value:
...
'review_list' => [
'reviews',
'default' => function($model, $relationName, $attributeName) {
//default value calculation
//...
return $defaultValue;
},
],
...The function accepts 3 parameters. In our example $model is the instance of the Book class (owner of the behavior), $relationName is 'reviews' and $attributeName is 'review_list'.
If you need the db connection inside this function, it is recommended to obtain it from either the primary model (Book) or the secondary model (Review).
function($model, $relationName, $attributeName) {
//get db connection from primary model (Book)
$connection = $model::getDb();
...
//OR get db connection from secondary model (Review)
$secondaryModelClass = $model->getRelation($relationName)->modelClass;
$connection = $secondaryModelClass::getDb();
...
//further value calculation logic (db query)It is possible to use this behavior for a single relationship multiple times in a single model. For example, it is possible to have author_list for normal form input and author_list_json for JSON string input at the same time. However, one should keep in mind that parameters are processed in the order they are given in the config, so if both author_list and author_list_json contain data, items from author_list will be saved first only to be overwritten by items from author_list_json afterwards. It is advised to provide data to only one of those attributes to avoid this.
The attributes are created automatically. However, you must supply a validation rule for them (usually a safe validator):
public function rules()
{
return [
[['author_list', 'review_list'], 'safe']
];
}By default, the behavior will accept data from a multiselect field:
<?= $form->field($model, 'author_list')
->dropDownList($authorsAsArray, ['multiple' => true]) ?>
...
<?= $form->field($model, 'review_list')
->dropDownList($reviewsAsArray, ['multiple' => true]) ?>- Composite primary keys are not supported.
- Junction table for many-to-many links is updated using the connection from the primary model.
- When using a function to calculate the default value, keep in mind that this function is called once, right before the relations are saved, and then its result is used to update all relevant rows using one query.
- Relations are saved using DAO (i. e. by manipulating the tables directly).
The preferred way to install this extension is through composer.
Either run
php composer.phar require --prefer-dist voskobovich/yii2-many-many-behavior "*"
or add
"voskobovich/yii2-many-many-behavior": "*"
to the require section of your composer.json file.