Passed
Push — master ( 264731...97d88c )
by Alban
03:51
created

SaveRelationsBehavior::_computePkDiff()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 18
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 12
nc 2
nop 3
dl 0
loc 18
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
namespace lhs\Yii2SaveRelationsBehavior;
4
5
use RuntimeException;
6
use Yii;
7
use yii\base\Behavior;
8
use yii\base\Exception;
9
use yii\base\ModelEvent;
10
use yii\base\UnknownPropertyException;
11
use yii\db\BaseActiveRecord;
12
use yii\db\Exception as DbException;
13
use yii\db\Transaction;
14
use yii\helpers\ArrayHelper;
15
use yii\helpers\Inflector;
16
17
/**
18
 * This Active Record Behavior allows to validate and save the Model relations when the save() method is invoked.
19
 * List of handled relations should be declared using the $relations parameter via an array of relation names.
20
 * @author albanjubert
21
 */
22
class SaveRelationsBehavior extends Behavior
23
{
24
25
    public $relations = [];
26
    private $_relations = [];
27
    private $_oldRelationValue = []; // Store initial relations value
28
    private $_newRelationValue = []; // Store update relations value
29
    private $_relationsSaveStarted = false;
30
    private $_transaction;
31
32
33
    private $_relationsScenario = [];
34
    private $_relationsExtraColumns = [];
35
36
    //private $_relationsCascadeDelete = []; //TODO
37
38
    /**
39
     * @inheritdoc
40
     */
41
    public function init()
42
    {
43
        parent::init();
44
        $allowedProperties = ['scenario', 'extraColumns'];
45
        foreach ($this->relations as $key => $value) {
46
            if (is_int($key)) {
47
                $this->_relations[] = $value;
48
            } else {
49
                $this->_relations[] = $key;
50
                if (is_array($value)) {
51
                    foreach ($value as $propertyKey => $propertyValue) {
52
                        if (in_array($propertyKey, $allowedProperties)) {
53
                            $this->{'_relations' . ucfirst($propertyKey)}[$key] = $propertyValue;
54
                        } else {
55
                            throw new UnknownPropertyException('The relation property named ' . $propertyKey . ' is not supported');
56
                        }
57
                    }
58
                }
59
            }
60
        }
61
    }
62
63
    /**
64
     * @inheritdoc
65
     */
66
    public function events()
67
    {
68
        return [
69
            BaseActiveRecord::EVENT_BEFORE_VALIDATE => 'beforeValidate',
70
            BaseActiveRecord::EVENT_AFTER_INSERT    => 'afterSave',
71
            BaseActiveRecord::EVENT_AFTER_UPDATE    => 'afterSave',
72
        ];
73
    }
74
75
    /**
76
     * Check if the behavior is attached to an Active Record
77
     * @param BaseActiveRecord $owner
78
     * @throws RuntimeException
79
     */
80
    public function attach($owner)
81
    {
82
        if (!($owner instanceof BaseActiveRecord)) {
0 ignored issues
show
introduced by
$owner is always a sub-type of yii\db\BaseActiveRecord.
Loading history...
83
            throw new RuntimeException('Owner must be instance of yii\db\BaseActiveRecord');
84
        }
85
        parent::attach($owner);
86
    }
87
88
    /**
89
     * Override canSetProperty method to be able to detect if a relation setter is allowed.
90
     * Setter is allowed if the relation is declared in the `relations` parameter
91
     * @param string $name
92
     * @param boolean $checkVars
93
     * @return boolean
94
     */
95
    public function canSetProperty($name, $checkVars = true)
96
    {
97
        if (in_array($name, $this->_relations) && $this->owner->getRelation($name, false)) {
0 ignored issues
show
Bug introduced by
The method getRelation() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

97
        if (in_array($name, $this->_relations) && $this->owner->/** @scrutinizer ignore-call */ getRelation($name, false)) {

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
98
            return true;
99
        }
100
        return parent::canSetProperty($name, $checkVars);
101
    }
102
103
    /**
104
     * Override __set method to be able to set relations values either by providing a model instance,
105
     * a primary key value or an associative array
106
     * @param string $name
107
     * @param mixed $value
108
     */
109
    public function __set($name, $value)
110
    {
111
        if (in_array($name, $this->_relations)) {
112
            Yii::trace("Setting {$name} relation value", __METHOD__);
0 ignored issues
show
Deprecated Code introduced by
The function yii\BaseYii::trace() has been deprecated: since 2.0.14. Use [[debug()]] instead. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

112
            /** @scrutinizer ignore-deprecated */ Yii::trace("Setting {$name} relation value", __METHOD__);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
113
            if (!isset($this->_oldRelationValue[$name])) {
114
                if ($this->owner->isNewRecord) {
115
                    if ($this->owner->getRelation($name)->multiple === true) {
116
                        $this->_oldRelationValue[$name] = [];
117
                    } else {
118
                        $this->_oldRelationValue[$name] = null;
119
                    }
120
                } else {
121
                    $this->_oldRelationValue[$name] = $this->owner->{$name};
122
                }
123
            }
124
            if ($this->owner->getRelation($name)->multiple === true) {
125
                $this->setMultipleRelation($name, $value);
126
            } else {
127
                $this->setSingleRelation($name, $value);
128
            }
129
        }
130
    }
131
132
    /**
133
     * Set the named multiple relation with the given value
134
     * @param $name
135
     * @param $value
136
     */
137
    protected function setMultipleRelation($name, $value)
138
    {
139
        $relation = $this->owner->getRelation($name);
140
        $newRelations = [];
141
        if (!is_array($value)) {
142
            if (!empty($value)) {
143
                $value = [$value];
144
            } else {
145
                $value = [];
146
            }
147
        }
148
        foreach ($value as $entry) {
149
            if ($entry instanceof $relation->modelClass) {
150
                $newRelations[] = $entry;
151
            } else {
152
                // TODO handle this with one DB request to retrieve all models
153
                $newRelations[] = $this->processModelAsArray($entry, $relation);
154
            }
155
        }
156
        $this->_newRelationValue[$name] = $newRelations;
157
        $this->owner->populateRelation($name, $newRelations);
158
    }
159
160
    /**
161
     * Get a BaseActiveRecord model using the given $data parameter.
162
     * $data could either be a model ID or an associative array representing model attributes => values
163
     * @param mixed $data
164
     * @param \yii\db\ActiveQuery $relation
165
     * @return BaseActiveRecord
166
     */
167
    protected function processModelAsArray($data, $relation)
168
    {
169
        /** @var BaseActiveRecord $modelClass */
170
        $modelClass = $relation->modelClass;
171
        // Get the related model foreign keys
172
        if (is_array($data)) {
173
            $fks = [];
174
175
            // search PK
176
            foreach ($modelClass::primaryKey() as $modelAttribute) {
177
                if (array_key_exists($modelAttribute, $data) && !empty($data[$modelAttribute])) {
178
                    $fks[$modelAttribute] = $data[$modelAttribute];
179
                } else {
180
                    $fks = [];
181
                    break;
182
                }
183
            }
184
            if (empty($fks)) {
185
                // Get the right link definition
186
                if ($relation->via instanceof BaseActiveRecord) {
187
                    $viaQuery = $relation->via;
0 ignored issues
show
Documentation Bug introduced by
It seems like $relation->via of type yii\db\BaseActiveRecord is incompatible with the declared type object|array of property $via.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
188
                    $link = $viaQuery->link;
189
                } elseif (is_array($relation->via)) {
190
                    list($viaName, $viaQuery) = $relation->via;
191
                    $link = $viaQuery->link;
192
                } else {
193
                    $link = $relation->link;
194
                }
195
                foreach ($link as $relatedAttribute => $modelAttribute) {
196
                    if (array_key_exists($modelAttribute, $data) && !empty($data[$modelAttribute])) {
197
                        $fks[$modelAttribute] = $data[$modelAttribute];
198
                    }
199
                }
200
            }
201
        } else {
202
            $fks = $data;
203
        }
204
        // Load existing model or create one if no key was provided and data is not empty
205
        /** @var BaseActiveRecord $relationModel */
206
        $relationModel = null;
207
        if (!empty($fks)) {
208
            $relationModel = $modelClass::findOne($fks);
209
        }
210
        if (!($relationModel instanceof BaseActiveRecord) && !empty($data)) {
211
            $relationModel = new $modelClass;
212
        }
213
        if (($relationModel instanceof BaseActiveRecord) && is_array($data)) {
214
            $relationModel->setAttributes($data);
215
        }
216
        return $relationModel;
217
    }
218
219
    /**
220
     * Set the named single relation with the given value
221
     * @param $name
222
     * @param $value
223
     */
224
    protected function setSingleRelation($name, $value)
225
    {
226
        $relation = $this->owner->getRelation($name);
227
        if (!($value instanceof $relation->modelClass)) {
228
            $value = $this->processModelAsArray($value, $relation);
229
        }
230
        $this->_newRelationValue[$name] = $value;
231
        $this->owner->populateRelation($name, $value);
232
    }
233
234
    /**
235
     * Before the owner model validation, save related models.
236
     * For `hasOne()` relations, set the according foreign keys of the owner model to be able to validate it
237
     * @param ModelEvent $event
238
     */
239
    public function beforeValidate(ModelEvent $event)
240
    {
241
        if ($this->_relationsSaveStarted == false && !empty($this->_oldRelationValue)) {
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
242
            /* @var $model BaseActiveRecord */
243
            $model = $this->owner;
244
            if ($this->saveRelatedRecords($model, $event)) {
245
                // If relation is has_one, try to set related model attributes
246
                foreach ($this->_relations as $relationName) {
247
                    if (array_key_exists($relationName, $this->_oldRelationValue)) { // Relation was not set, do nothing...
248
                        $relation = $model->getRelation($relationName);
249
                        if ($relation->multiple === false && !empty($model->{$relationName})) {
0 ignored issues
show
Bug introduced by
Accessing multiple on the interface yii\db\ActiveQueryInterface suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
250
                            Yii::trace("Setting foreign keys for {$relationName}", __METHOD__);
0 ignored issues
show
Deprecated Code introduced by
The function yii\BaseYii::trace() has been deprecated: since 2.0.14. Use [[debug()]] instead. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

250
                            /** @scrutinizer ignore-deprecated */ Yii::trace("Setting foreign keys for {$relationName}", __METHOD__);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
251
                            foreach ($relation->link as $relatedAttribute => $modelAttribute) {
0 ignored issues
show
Bug introduced by
Accessing link on the interface yii\db\ActiveQueryInterface suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
252
                                if ($model->{$modelAttribute} !== $model->{$relationName}->{$relatedAttribute}) {
253
                                    $model->{$modelAttribute} = $model->{$relationName}->{$relatedAttribute};
254
                                }
255
                            }
256
                        }
257
                    }
258
                }
259
            }
260
        }
261
    }
262
263
    /**
264
     * For each related model, try to save it first.
265
     * If set in the owner model, operation is done in a transactional way so if one of the models should not validate
266
     * or be saved, a rollback will occur.,
267
     * This is done during the before validation process to be able to set the related foreign keys.
268
     * @param BaseActiveRecord $model
269
     * @param ModelEvent $event
270
     * @return bool
271
     */
272
    protected function saveRelatedRecords(BaseActiveRecord $model, ModelEvent $event)
273
    {
274
        if (
275
            method_exists($model, 'isTransactional')
276
            && is_null($model->getDb()->transaction)
277
            && (
278
                ($model->isNewRecord && $model->isTransactional($model::OP_INSERT))
279
                || (!$model->isNewRecord && $model->isTransactional($model::OP_UPDATE))
280
                || $model->isTransactional($model::OP_ALL)
281
            )
282
        ) {
283
            $this->_transaction = $model->getDb()->beginTransaction();
284
        }
285
        try {
286
            foreach ($this->_relations as $relationName) {
287
                if (array_key_exists($relationName, $this->_oldRelationValue)) { // Relation was not set, do nothing...
288
                    $relation = $model->getRelation($relationName);
289
                    if (!empty($model->{$relationName})) {
290
                        if ($relation->multiple === false) {
0 ignored issues
show
Bug introduced by
Accessing multiple on the interface yii\db\ActiveQueryInterface suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
291
                            $relationModel = $model->{$relationName};
292
                            $p1 = $model->isPrimaryKey(array_keys($relation->link));
0 ignored issues
show
Bug introduced by
Accessing link on the interface yii\db\ActiveQueryInterface suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
293
                            $p2 = $relationModel::isPrimaryKey(array_values($relation->link));
294
                            $pettyRelationName = Inflector::camel2words($relationName, true);
295
                            if ($relationModel->getIsNewRecord() && $p1 && !$p2) {
296
                                // Save Has one relation new record
297
                                $this->saveModelRecord($model->{$relationName}, $event, $pettyRelationName, $relationName);
298
                            } else {
299
                                $this->validateRelationModel($pettyRelationName, $relationName, $relationModel, $event);
300
                            }
301
                        } else {
302
                            // Save Has many relations new records
303
                            /** @var BaseActiveRecord $relationModel */
304
                            foreach ($model->{$relationName} as $i => $relationModel) {
305
                                $pettyRelationName = Inflector::camel2words($relationName, true) . " #{$i}";
306
                                $this->validateRelationModel($pettyRelationName, $relationName, $relationModel, $event);
307
                            }
308
                        }
309
                    }
310
                }
311
            }
312
            if (!$event->isValid) {
313
                throw new Exception("One of the related model could not be validated");
314
            }
315
        } catch (Exception $e) {
316
            Yii::warning(get_class($e) . " was thrown while saving related records during beforeValidate event: " . $e->getMessage(), __METHOD__);
317
            $this->_rollback();
318
            $model->addError($model->formName(), $e->getMessage());
319
            $event->isValid = false; // Stop saving, something went wrong
320
            return false;
321
        }
322
        return true;
323
    }
324
325
    /**
326
     * Validate and save the model if it is new or changed
327
     * @param BaseActiveRecord $model
328
     * @param ModelEvent $event
329
     * @param $pettyRelationName
330
     * @param $relationName
331
     */
332
    protected function saveModelRecord(BaseActiveRecord $model, ModelEvent $event, $pettyRelationName, $relationName)
333
    {
334
        $this->validateRelationModel($pettyRelationName, $relationName, $model, $event);
335
        if ($event->isValid && (count($model->dirtyAttributes) || $model->isNewRecord)) {
336
            Yii::trace("Saving {$pettyRelationName} relation model", __METHOD__);
0 ignored issues
show
Deprecated Code introduced by
The function yii\BaseYii::trace() has been deprecated: since 2.0.14. Use [[debug()]] instead. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

336
            /** @scrutinizer ignore-deprecated */ Yii::trace("Saving {$pettyRelationName} relation model", __METHOD__);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
337
            $model->save(false);
338
        }
339
    }
340
341
    /**
342
     * Validate a relation model and add an error message to owner model attribute if needed
343
     * @param string $pettyRelationName
344
     * @param string $relationName
345
     * @param BaseActiveRecord $relationModel
346
     * @param ModelEvent $event
347
     */
348
    protected function validateRelationModel($pettyRelationName, $relationName, BaseActiveRecord $relationModel, ModelEvent $event)
0 ignored issues
show
Unused Code introduced by
The parameter $event is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

348
    protected function validateRelationModel($pettyRelationName, $relationName, BaseActiveRecord $relationModel, /** @scrutinizer ignore-unused */ ModelEvent $event)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
349
    {
350
        /** @var BaseActiveRecord $model */
351
        $model = $this->owner;
352
        if (!is_null($relationModel) && ($relationModel->isNewRecord || count($relationModel->getDirtyAttributes()))) {
353
            if (key_exists($relationName, $this->_relationsScenario)) {
354
                $relationModel->setScenario($this->_relationsScenario[$relationName]);
355
            }
356
            Yii::trace("Validating {$pettyRelationName} relation model using " . $relationModel->scenario . " scenario", __METHOD__);
0 ignored issues
show
Deprecated Code introduced by
The function yii\BaseYii::trace() has been deprecated: since 2.0.14. Use [[debug()]] instead. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

356
            /** @scrutinizer ignore-deprecated */ Yii::trace("Validating {$pettyRelationName} relation model using " . $relationModel->scenario . " scenario", __METHOD__);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
357
            if (!$relationModel->validate()) {
358
                $this->_addError($relationModel, $model, $relationName, $pettyRelationName);
359
            }
360
        }
361
    }
362
363
    /**
364
     * Attach errors to owner relational attributes
365
     * @param $relationModel
366
     * @param $owner
367
     * @param $relationName
368
     * @param $pettyRelationName
369
     */
370
    private function _addError($relationModel, $owner, $relationName, $pettyRelationName)
371
    {
372
        foreach ($relationModel->errors as $attributeErrors) {
373
            foreach ($attributeErrors as $error) {
374
                $owner->addError($relationName, "{$pettyRelationName}: {$error}");
375
            }
376
        }
377
    }
378
379
    /**
380
     * Rollback transaction if any
381
     * @throws DbException
382
     */
383
    private function _rollback()
384
    {
385
        if (($this->_transaction instanceof Transaction) && $this->_transaction->isActive) {
386
            $this->_transaction->rollBack(); // If anything goes wrong, transaction will be rolled back
387
            Yii::info("Rolling back", __METHOD__);
388
        }
389
    }
390
391
    /**
392
     * Link the related models.
393
     * If the models have not been changed, nothing will be done.
394
     * Related records will be linked to the owner model using the BaseActiveRecord `link()` method.
395
     */
396
    public function afterSave()
397
    {
398
        if ($this->_relationsSaveStarted == false) {
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
399
            /** @var BaseActiveRecord $model */
400
            $model = $this->owner;
401
            $this->_relationsSaveStarted = true;
402
            // Populate relations with updated values
403
            foreach ($this->_newRelationValue as $name => $value) {
404
                $this->owner->populateRelation($name, $value);
405
            }
406
            try {
407
                foreach ($this->_relations as $relationName) {
408
                    if (array_key_exists($relationName, $this->_oldRelationValue)) { // Relation was not set, do nothing...
409
                        Yii::trace("Linking {$relationName} relation", __METHOD__);
0 ignored issues
show
Deprecated Code introduced by
The function yii\BaseYii::trace() has been deprecated: since 2.0.14. Use [[debug()]] instead. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

409
                        /** @scrutinizer ignore-deprecated */ Yii::trace("Linking {$relationName} relation", __METHOD__);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
410
                        $relation = $model->getRelation($relationName);
411
                        if ($relation->multiple === true) { // Has many relation
0 ignored issues
show
Bug introduced by
Accessing multiple on the interface yii\db\ActiveQueryInterface suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
412
                            // Process new relations
413
                            $existingRecords = [];
414
                            /** @var BaseActiveRecord $relationModel */
415
                            foreach ($model->{$relationName} as $i => $relationModel) {
416
                                if ($relationModel->isNewRecord) {
417
                                    if ($relation->via !== null) {
0 ignored issues
show
Bug introduced by
Accessing via on the interface yii\db\ActiveQueryInterface suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
418
                                        if ($relationModel->validate()) {
419
                                            $relationModel->save();
420
                                        } else {
421
                                            $pettyRelationName = Inflector::camel2words($relationName, true) . " #{$i}";
422
                                            $this->_addError($relationModel, $model, $relationName, $pettyRelationName);
423
                                            throw new DbException("Related record {$pettyRelationName} could not be saved.");
424
                                        }
425
                                    }
426
                                    $junctionTableColumns = $this->_getJunctionTableColumns($relationName, $relationModel);
427
                                    $model->link($relationName, $relationModel, $junctionTableColumns);
428
                                } else {
429
                                    $existingRecords[] = $relationModel;
430
                                }
431
                                if (count($relationModel->dirtyAttributes)) {
432
                                    if ($relationModel->validate()) {
433
                                        $relationModel->save();
434
                                    } else {
435
                                        $pettyRelationName = Inflector::camel2words($relationName, true);
436
                                        $this->_addError($relationModel, $model, $relationName, $pettyRelationName);
437
                                        throw new DbException("Related record {$pettyRelationName} could not be saved.");
438
                                    }
439
                                }
440
                            }
441
                            $junctionTablePropertiesUsed = array_key_exists($relationName, $this->_relationsExtraColumns);
442
                            // Process existing added and deleted relations
443
                            list($addedPks, $deletedPks) = $this->_computePkDiff(
444
                                $this->_oldRelationValue[$relationName],
445
                                $existingRecords,
446
                                $junctionTablePropertiesUsed
447
                            );
448
                            // Deleted relations
449
                            $initialModels = ArrayHelper::index($this->_oldRelationValue[$relationName], function (BaseActiveRecord $model) {
450
                                return implode("-", $model->getPrimaryKey(true));
451
                            });
452
                            $initialRelations = $model->{$relationName};
453
                            foreach ($deletedPks as $key) {
454
                                $model->unlink($relationName, $initialModels[$key], true);
455
                            }
456
                            // Added relations
457
                            $actualModels = ArrayHelper::index(
458
                                $junctionTablePropertiesUsed ? $initialRelations : $model->{$relationName},
459
                                function (BaseActiveRecord $model) {
460
                                    return implode("-", $model->getPrimaryKey(true));
461
                                }
462
                            );
463
                            foreach ($addedPks as $key) {
464
                                $junctionTableColumns = $this->_getJunctionTableColumns($relationName, $actualModels[$key]);
465
                                $model->link($relationName, $actualModels[$key], $junctionTableColumns);
466
                            }
467
                        } else { // Has one relation
468
                            if ($this->_oldRelationValue[$relationName] !== $model->{$relationName}) {
469
                                if ($model->{$relationName} instanceof BaseActiveRecord) {
470
                                    $model->link($relationName, $model->{$relationName});
471
                                } else {
472
                                    if ($this->_oldRelationValue[$relationName] instanceof BaseActiveRecord) {
473
                                        $model->unlink($relationName, $this->_oldRelationValue[$relationName]);
474
                                    }
475
                                }
476
                            }
477
                            if ($model->{$relationName} instanceof BaseActiveRecord) {
478
                                $model->{$relationName}->save();
479
                            }
480
481
                        }
482
                        unset($this->_oldRelationValue[$relationName]);
483
                    }
484
                }
485
            } catch (Exception $e) {
486
                Yii::warning(get_class($e) . " was thrown while saving related records during afterSave event: " . $e->getMessage(), __METHOD__);
487
                $this->_rollback();
488
                /***
489
                 * Sadly mandatory because the error occurred during afterSave event
490
                 * and we don't want the user/developper not to be aware of the issue.
491
                 ***/
492
                throw $e;
493
            }
494
            $model->refresh();
495
            $this->_relationsSaveStarted = false;
496
            if (($this->_transaction instanceof Transaction) && $this->_transaction->isActive) {
497
                $this->_transaction->commit();
498
            }
499
        }
500
    }
501
502
    /**
503
     * Return array of columns to save to the junction table for a related model having a many-to-many relation.
504
     * @param string $relationName
505
     * @param BaseActiveRecord $model
506
     * @return array
507
     */
508
    private function _getJunctionTableColumns($relationName, $model)
509
    {
510
        $junctionTableColumns = [];
511
        if (array_key_exists($relationName, $this->_relationsExtraColumns)) {
512
            if (is_callable($this->_relationsExtraColumns[$relationName])) {
513
                $junctionTableColumns = $this->_relationsExtraColumns[$relationName]($model);
514
            } elseif (is_array($this->_relationsExtraColumns[$relationName])) {
515
                $junctionTableColumns = $this->_relationsExtraColumns[$relationName];
516
            }
517
            if (!is_array($junctionTableColumns)) {
518
                throw new RuntimeException(
519
                    'Junction table columns definition must return an array, got ' . gettype($junctionTableColumns)
520
                );
521
            }
522
        }
523
        return $junctionTableColumns;
524
    }
525
526
    /**
527
     * Compute the difference between two set of records using primary keys "tokens"
528
     * If third parameter is set to true all initial related records will be marked for removal even if their
529
     * properties did not change. This can be handy in a many-to-many relation involving a junction table.
530
     * @param BaseActiveRecord[] $initialRelations
531
     * @param BaseActiveRecord[] $updatedRelations
532
     * @param bool $forceSave
533
     * @return array
534
     */
535
    private function _computePkDiff($initialRelations, $updatedRelations, $forceSave = false)
536
    {
537
        // Compute differences between initial relations and the current ones
538
        $oldPks = ArrayHelper::getColumn($initialRelations, function (BaseActiveRecord $model) {
539
            return implode("-", $model->getPrimaryKey(true));
540
        });
541
        $newPks = ArrayHelper::getColumn($updatedRelations, function (BaseActiveRecord $model) {
542
            return implode("-", $model->getPrimaryKey(true));
543
        });
544
        if ($forceSave) {
545
            $addedPks = $newPks;
546
            $deletedPks = $oldPks;
547
        } else {
548
            $identicalPks = array_intersect($oldPks, $newPks);
549
            $addedPks = array_values(array_diff($newPks, $identicalPks));
550
            $deletedPks = array_values(array_diff($oldPks, $identicalPks));
551
        }
552
        return [$addedPks, $deletedPks];
553
    }
554
555
    /**
556
     * Populates relations with input data
557
     * @param array $data
558
     */
559
    public function loadRelations($data)
560
    {
561
        /** @var BaseActiveRecord $model */
562
        $model = $this->owner;
563
        foreach ($this->_relations as $relationName) {
564
            $relation = $model->getRelation($relationName);
565
            $modelClass = $relation->modelClass;
0 ignored issues
show
Bug introduced by
Accessing modelClass on the interface yii\db\ActiveQueryInterface suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
566
            /** @var BaseActiveRecord $relationalModel */
567
            $relationalModel = new $modelClass;
568
            $formName = $relationalModel->formName();
569
            if (array_key_exists($formName, $data)) {
570
                $model->{$relationName} = $data[$formName];
571
            }
572
        }
573
    }
574
}
575