Passed
Push — master ( 5e2fe7...e6807c )
by Alban
02:45
created

_loadOrCreateRelationModel()   B

Complexity

Conditions 7
Paths 12

Size

Total Lines 18
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 7

Importance

Changes 0
Metric Value
cc 7
eloc 10
nc 12
nop 4
dl 0
loc 18
ccs 11
cts 11
cp 1
crap 7
rs 8.8333
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\Component;
9
use yii\base\Exception;
10
use yii\base\InvalidArgumentException;
11
use yii\base\ModelEvent;
12
use yii\base\UnknownPropertyException;
13
use yii\db\ActiveQuery;
14
use yii\db\BaseActiveRecord;
15
use yii\db\Exception as DbException;
16
use yii\db\Transaction;
17
use yii\helpers\ArrayHelper;
18
use yii\helpers\Inflector;
19
use yii\helpers\VarDumper;
20
21
/**
22
 * This Active Record Behavior allows to validate and save the Model relations when the save() method is invoked.
23
 * List of handled relations should be declared using the $relations parameter via an array of relation names.
24
 * @author albanjubert
25
 */
26
class SaveRelationsBehavior extends Behavior
27
{
28
29
    public $relations = [];
30
    private $_relations = [];
31
    private $_oldRelationValue = []; // Store initial relations value
32
    private $_newRelationValue = []; // Store update relations value
33
    private $_relationsToDelete = [];
34
    private $_relationsSaveStarted = false;
35
    private $_transaction;
36
37
38
    private $_relationsScenario = [];
39
    private $_relationsExtraColumns = [];
40
    private $_relationsCascadeDelete = [];
41
42
    /**
43
     * @inheritdoc
44
     */
45 41
    public function init()
46
    {
47 41
        parent::init();
48 41
        $allowedProperties = ['scenario', 'extraColumns', 'cascadeDelete'];
49 41
        foreach ($this->relations as $key => $value) {
50 40
            if (is_int($key)) {
51 40
                $this->_relations[] = $value;
52
            } else {
53 36
                $this->_relations[] = $key;
54 36
                if (is_array($value)) {
55 36
                    foreach ($value as $propertyKey => $propertyValue) {
56 36
                        if (in_array($propertyKey, $allowedProperties)) {
57 36
                            $this->{'_relations' . ucfirst($propertyKey)}[$key] = $propertyValue;
58
                        } else {
59 36
                            throw new UnknownPropertyException('The relation property named ' . $propertyKey . ' is not supported');
60
                        }
61
                    }
62
                }
63
            }
64
        }
65 41
    }
66
67
    /**
68
     * @inheritdoc
69
     */
70 40
    public function events()
71
    {
72
        return [
73 40
            BaseActiveRecord::EVENT_BEFORE_VALIDATE => 'beforeValidate',
74 40
            BaseActiveRecord::EVENT_AFTER_INSERT    => 'afterSave',
75 40
            BaseActiveRecord::EVENT_AFTER_UPDATE    => 'afterSave',
76 40
            BaseActiveRecord::EVENT_BEFORE_DELETE   => 'beforeDelete',
77 40
            BaseActiveRecord::EVENT_AFTER_DELETE    => 'afterDelete'
78
        ];
79
    }
80
81
    /**
82
     * Check if the behavior is attached to an Active Record
83
     * @param Component $owner
84
     * @throws RuntimeException
85
     */
86 41
    public function attach($owner)
87
    {
88 41
        if (!($owner instanceof BaseActiveRecord)) {
89 1
            throw new RuntimeException('Owner must be instance of yii\db\BaseActiveRecord');
90
        }
91 40
        parent::attach($owner);
92 40
    }
93
94
    /**
95
     * Override canSetProperty method to be able to detect if a relation setter is allowed.
96
     * Setter is allowed if the relation is declared in the `relations` parameter
97
     * @param string $name
98
     * @param boolean $checkVars
99
     * @return boolean
100
     */
101 35
    public function canSetProperty($name, $checkVars = true)
102
    {
103
        /** @var BaseActiveRecord $owner */
104 35
        $owner = $this->owner;
105 35
        $relation = $owner->getRelation($name, false);
106 35
        if (in_array($name, $this->_relations) && !is_null($relation)) {
107 34
            return true;
108
        }
109 1
        return parent::canSetProperty($name, $checkVars);
110
    }
111
112
    /**
113
     * Override __set method to be able to set relations values either by providing a model instance,
114
     * a primary key value or an associative array
115
     * @param string $name
116
     * @param mixed $value
117
     * @throws \yii\base\InvalidArgumentException
118
     */
119 34
    public function __set($name, $value)
120
    {
121
        /** @var BaseActiveRecord $owner */
122 34
        $owner = $this->owner;
123 34
        if (in_array($name, $this->_relations)) {
124 34
            Yii::debug("Setting {$name} relation value", __METHOD__);
125
            /** @var ActiveQuery $relation */
126 34
            $relation = $owner->getRelation($name);
127 34
            if (!isset($this->_oldRelationValue[$name])) {
128 34
                if ($owner->isNewRecord) {
129 16
                    if ($relation->multiple === true) {
130 8
                        $this->_oldRelationValue[$name] = [];
131
                    } else {
132 14
                        $this->_oldRelationValue[$name] = null;
133
                    }
134
                } else {
135 19
                    $this->_oldRelationValue[$name] = $owner->{$name};
136
                }
137
            }
138 34
            if ($relation->multiple === true) {
139 22
                $this->setMultipleRelation($name, $value);
140
            } else {
141 21
                $this->setSingleRelation($name, $value);
142
            }
143
        }
144 34
    }
145
146
    /**
147
     * Set the named multiple relation with the given value
148
     * @param $name
149
     * @param $value
150
     * @throws \yii\base\InvalidArgumentException
151
     */
152 22
    protected function setMultipleRelation($name, $value)
153
    {
154
        /** @var BaseActiveRecord $owner */
155 22
        $owner = $this->owner;
156
        /** @var ActiveQuery $relation */
157 22
        $relation = $owner->getRelation($name);
158 22
        $newRelations = [];
159 22
        if (!is_array($value)) {
160 4
            if (!empty($value)) {
161 3
                $value = [$value];
162
            } else {
163 1
                $value = [];
164
            }
165
        }
166 22
        foreach ($value as $entry) {
167 21
            if ($entry instanceof $relation->modelClass) {
168 16
                $newRelations[] = $entry;
169
            } else {
170
                // TODO handle this with one DB request to retrieve all models
171 9
                $newRelations[] = $this->processModelAsArray($entry, $relation, $name);
172
            }
173
        }
174 22
        $this->_newRelationValue[$name] = $newRelations;
175 22
        $owner->populateRelation($name, $newRelations);
176 22
    }
177
178
    /**
179
     * Get a BaseActiveRecord model using the given $data parameter.
180
     * $data could either be a model ID or an associative array representing model attributes => values
181
     * @param mixed $data
182
     * @param \yii\db\ActiveQuery $relation
183
     * @return BaseActiveRecord
184
     */
185 14
    protected function processModelAsArray($data, $relation, $name)
186
    {
187
        /** @var BaseActiveRecord $modelClass */
188 14
        $modelClass = $relation->modelClass;
189 14
        $fks = $this->_getRelatedFks($data, $relation, $modelClass);
190 14
        return $this->_loadOrCreateRelationModel($data, $fks, $modelClass, $name);
191
    }
192
193
    /**
194
     * Get the related model foreign keys
195
     * @param $data
196
     * @param $relation
197
     * @param BaseActiveRecord $modelClass
198
     * @return array
199
     */
200 14
    private function _getRelatedFks($data, $relation, $modelClass)
201
    {
202 14
        $fks = [];
203 14
        if (is_array($data)) {
204
            // search PK
205 10
            foreach ($modelClass::primaryKey() as $modelAttribute) {
206 10
                if (array_key_exists($modelAttribute, $data) && !empty($data[$modelAttribute])) {
207 7
                    $fks[$modelAttribute] = $data[$modelAttribute];
208
                } else {
209 8
                    $fks = [];
210 8
                    break;
211
                }
212
            }
213 10
            if (empty($fks)) {
214
                // Get the right link definition
215 8
                if ($relation->via instanceof BaseActiveRecord) {
216
                    $link = $relation->via->link;
217 8
                } elseif (is_array($relation->via)) {
218 2
                    list($viaName, $viaQuery) = $relation->via;
219 2
                    $link = $viaQuery->link;
220
                } else {
221 6
                    $link = $relation->link;
222
                }
223 8
                foreach ($link as $relatedAttribute => $modelAttribute) {
224 8
                    if (array_key_exists($modelAttribute, $data) && !empty($data[$modelAttribute])) {
225 8
                        $fks[$modelAttribute] = $data[$modelAttribute];
226
                    }
227
                }
228
            }
229
        } else {
230 4
            $fks = $data;
231
        }
232 14
        return $fks;
233
    }
234
235
    /**
236
     * Load existing model or create one if no key was provided and data is not empty
237
     * @param $data
238
     * @param $fks
239
     * @param $modelClass
240
     * @param $relationName
241
     * @return BaseActiveRecord
242
     */
243 14
    private function _loadOrCreateRelationModel($data, $fks, $modelClass, $relationName)
244
    {
245
246
        /** @var BaseActiveRecord $relationModel */
247 14
        $relationModel = null;
248 14
        if (!empty($fks)) {
249 8
            $relationModel = $modelClass::findOne($fks);
250
        }
251 14
        if (!($relationModel instanceof BaseActiveRecord) && !empty($data)) {
252 9
            $relationModel = new $modelClass;
253 9
            if (array_key_exists($relationName, $this->_relationsScenario)) {
254 7
                $relationModel->setScenario($this->_relationsScenario[$relationName]);
255
            }
256
        }
257 14
        if (($relationModel instanceof BaseActiveRecord) && is_array($data)) {
258 10
            $relationModel->setAttributes($data);
259
        }
260 14
        return $relationModel;
261
    }
262
263
    /**
264
     * Set the named single relation with the given value
265
     * @param $name
266
     * @param $value
267
     * @throws \yii\base\InvalidArgumentException
268
     */
269 21
    protected function setSingleRelation($name, $value)
270
    {
271
        /** @var BaseActiveRecord $owner */
272 21
        $owner = $this->owner;
273
        /** @var ActiveQuery $relation */
274 21
        $relation = $owner->getRelation($name);
275 21
        if (!($value instanceof $relation->modelClass)) {
276 9
            $value = $this->processModelAsArray($value, $relation, $name);
277
        }
278 21
        $this->_newRelationValue[$name] = $value;
279 21
        $owner->populateRelation($name, $value);
280 21
    }
281
282
    /**
283
     * Before the owner model validation, save related models.
284
     * For `hasOne()` relations, set the according foreign keys of the owner model to be able to validate it
285
     * @param ModelEvent $event
286
     * @throws DbException
287
     * @throws \yii\base\InvalidConfigException
288
     */
289 31
    public function beforeValidate(ModelEvent $event)
290
    {
291 31
        if ($this->_relationsSaveStarted === false && !empty($this->_oldRelationValue)) {
292
            /* @var $model BaseActiveRecord */
293 31
            $model = $this->owner;
294 31
            if ($this->saveRelatedRecords($model, $event)) {
295
                // If relation is has_one, try to set related model attributes
296 31
                foreach ($this->_relations as $relationName) {
297 31
                    if (array_key_exists($relationName, $this->_oldRelationValue)) { // Relation was not set, do nothing...
298
                        /** @var ActiveQuery $relation */
299 31
                        $relation = $model->getRelation($relationName);
300 31
                        if ($relation->multiple === false && !empty($model->{$relationName})) {
301 19
                            Yii::debug("Setting foreign keys for {$relationName}", __METHOD__);
302 19
                            foreach ($relation->link as $relatedAttribute => $modelAttribute) {
303 19
                                if ($model->{$modelAttribute} !== $model->{$relationName}->{$relatedAttribute}) {
304 19
                                    $model->{$modelAttribute} = $model->{$relationName}->{$relatedAttribute};
305
                                }
306
                            }
307
                        }
308
                    }
309
                }
310
            }
311
        }
312 31
    }
313
314
    /**
315
     * For each related model, try to save it first.
316
     * If set in the owner model, operation is done in a transactional way so if one of the models should not validate
317
     * or be saved, a rollback will occur.,
318
     * This is done during the before validation process to be able to set the related foreign keys.
319
     * @param BaseActiveRecord $model
320
     * @param ModelEvent $event
321
     * @return bool
322
     * @throws DbException
323
     * @throws \yii\base\InvalidConfigException
324
     */
325 31
    protected function saveRelatedRecords(BaseActiveRecord $model, ModelEvent $event)
326
    {
327 31
        $this->startTransactionForModel($model);
328
        try {
329 31
            foreach ($this->_relations as $relationName) {
330 31
                if (array_key_exists($relationName, $this->_oldRelationValue)) { // Relation was not set, do nothing...
331
                    /** @var ActiveQuery $relation */
332 31
                    $relation = $model->getRelation($relationName);
333 31
                    if (!empty($model->{$relationName})) {
334 29
                        if ($relation->multiple === false) {
335 19
                            $this->_prepareHasOneRelation($model, $relationName, $event);
336
                        } else {
337 19
                            $this->_prepareHasManyRelation($model, $relationName);
338
                        }
339
                    }
340
                }
341
            }
342 31
            if (!$event->isValid) {
343
                throw new Exception('One of the related model could not be validated');
344
            }
345
        } catch (Exception $e) {
346
            Yii::warning(get_class($e) . ' was thrown while saving related records during beforeValidate event: ' . $e->getMessage(), __METHOD__);
347
            $this->_rollback();
348
            $model->addError($model->formName(), $e->getMessage());
349
            $event->isValid = false; // Stop saving, something went wrong
350
            return false;
351
        }
352 31
        return true;
353
    }
354
355
    /**
356
     * @param BaseActiveRecord $model
357
     */
358 31
    protected function startTransactionForModel(BaseActiveRecord $model)
359
    {
360 31
        if ($this->isModelTransactional($model) && is_null($model->getDb()->transaction)) {
361 5
            $this->_transaction = $model->getDb()->beginTransaction();
362
        }
363 31
    }
364
365
    /**
366
     * @param BaseActiveRecord $model
367
     * @return bool
368
     */
369 31
    protected function isModelTransactional(BaseActiveRecord $model)
370
    {
371 31
        if (method_exists($model, 'isTransactional')) {
372 31
            return ($model->isNewRecord && $model->isTransactional($model::OP_INSERT))
373 24
                || (!$model->isNewRecord && $model->isTransactional($model::OP_UPDATE))
374 31
                || $model->isTransactional($model::OP_ALL);
375
        }
376
        return false;
377
    }
378
379
    /**
380
     * @param BaseActiveRecord $model
381
     * @param ModelEvent $event
382
     * @param $relationName
383
     */
384 19
    private function _prepareHasOneRelation(BaseActiveRecord $model, $relationName, ModelEvent $event)
385
    {
386 19
        $relationModel = $model->{$relationName};
387 19
        $this->validateRelationModel(self::prettyRelationName($relationName), $relationName, $model->{$relationName});
388 19
        if ($relationModel->getIsNewRecord()) {
389
            // Save Has one relation new record
390 13
            if ($event->isValid && (count($model->dirtyAttributes) || $model->{$relationName}->isNewRecord)) {
391 13
                Yii::debug('Saving ' . self::prettyRelationName($relationName) . ' relation model', __METHOD__);
392 13
                $model->{$relationName}->save();
393
            }
394
        }
395 19
    }
396
397
    /**
398
     * Validate a relation model and add an error message to owner model attribute if needed
399
     * @param string $prettyRelationName
400
     * @param string $relationName
401
     * @param BaseActiveRecord $relationModel
402
     */
403 29
    protected function validateRelationModel($prettyRelationName, $relationName, BaseActiveRecord $relationModel)
404
    {
405
        /** @var BaseActiveRecord $model */
406 29
        $model = $this->owner;
407 29
        if (!is_null($relationModel) && ($relationModel->isNewRecord || count($relationModel->getDirtyAttributes()))) {
408 21
            Yii::debug("Validating {$prettyRelationName} relation model using " . $relationModel->scenario . ' scenario', __METHOD__);
409 21
            if (!$relationModel->validate()) {
410 4
                $this->_addError($relationModel, $model, $relationName, $prettyRelationName);
411
            }
412
413
        }
414 29
    }
415
416
    /**
417
     * Attach errors to owner relational attributes
418
     * @param $relationModel
419
     * @param $owner
420
     * @param $relationName
421
     * @param $prettyRelationName
422
     */
423 5
    private function _addError($relationModel, $owner, $relationName, $prettyRelationName)
424
    {
425 5
        foreach ($relationModel->errors as $attributeErrors) {
426 5
            foreach ($attributeErrors as $error) {
427 5
                $owner->addError($relationName, "{$prettyRelationName}: {$error}");
428
            }
429
        }
430 5
    }
431
432
    /**
433
     * @param $relationName
434
     * @param int|null $i
435
     * @return string
436
     */
437 29
    protected static function prettyRelationName($relationName, $i = null)
438
    {
439 29
        return Inflector::camel2words($relationName, true) . (is_null($i) ? '' : " #{$i}");
440
    }
441
442
    /**
443
     * @param BaseActiveRecord $model
444
     * @param $relationName
445
     */
446 19
    private function _prepareHasManyRelation(BaseActiveRecord $model, $relationName)
447
    {
448
        /** @var BaseActiveRecord $relationModel */
449 19
        foreach ($model->{$relationName} as $i => $relationModel) {
450 19
            $this->validateRelationModel(self::prettyRelationName($relationName, $i), $relationName, $relationModel);
451
        }
452 19
    }
453
454
    /**
455
     * Rollback transaction if any
456
     * @throws DbException
457
     */
458 2
    private function _rollback()
459
    {
460 2
        if (($this->_transaction instanceof Transaction) && $this->_transaction->isActive) {
461
            $this->_transaction->rollBack(); // If anything goes wrong, transaction will be rolled back
462
            Yii::info('Rolling back', __METHOD__);
463
        }
464 2
    }
465
466
    /**
467
     * Link the related models.
468
     * If the models have not been changed, nothing will be done.
469
     * Related records will be linked to the owner model using the BaseActiveRecord `link()` method.
470
     * @throws Exception
471
     */
472 29
    public function afterSave()
473
    {
474 29
        if ($this->_relationsSaveStarted === false) {
475
            /** @var BaseActiveRecord $owner */
476 29
            $owner = $this->owner;
477 29
            $this->_relationsSaveStarted = true;
478
            // Populate relations with updated values
479 29
            foreach ($this->_newRelationValue as $name => $value) {
480 29
                $owner->populateRelation($name, $value);
481
            }
482
            try {
483 29
                foreach ($this->_relations as $relationName) {
484 29
                    if (array_key_exists($relationName, $this->_oldRelationValue)) { // Relation was not set, do nothing...
485 29
                        Yii::debug("Linking {$relationName} relation", __METHOD__);
486
                        /** @var ActiveQuery $relation */
487 29
                        $relation = $owner->getRelation($relationName);
488 29
                        if ($relation->multiple === true) { // Has many relation
489 19
                            $this->_afterSaveHasManyRelation($relationName);
490
                        } else { // Has one relation
491 19
                            $this->_afterSaveHasOneRelation($relationName);
492
                        }
493 29
                        unset($this->_oldRelationValue[$relationName]);
494
                    }
495
                }
496 1
            } catch (Exception $e) {
497 1
                Yii::warning(get_class($e) . ' was thrown while saving related records during afterSave event: ' . $e->getMessage(), __METHOD__);
498 1
                $this->_rollback();
499
                /***
500
                 * Sadly mandatory because the error occurred during afterSave event
501
                 * and we don't want the user/developper not to be aware of the issue.
502
                 ***/
503 1
                throw $e;
504
            }
505 29
            $owner->refresh();
506 29
            $this->_relationsSaveStarted = false;
507 29
            if (($this->_transaction instanceof Transaction) && $this->_transaction->isActive) {
508 4
                $this->_transaction->commit();
509
            }
510
        }
511 29
    }
512
513
    /**
514
     * @param $relationName
515
     * @throws DbException
516
     */
517 19
    public function _afterSaveHasManyRelation($relationName)
518
    {
519
        /** @var BaseActiveRecord $owner */
520 19
        $owner = $this->owner;
521
        /** @var ActiveQuery $relation */
522 19
        $relation = $owner->getRelation($relationName);
523
524
        // Process new relations
525 19
        $existingRecords = [];
526
        /** @var ActiveQuery $relationModel */
527 19
        foreach ($owner->{$relationName} as $i => $relationModel) {
528 19
            if ($relationModel->isNewRecord) {
529 11
                if (!empty($relation->via)) {
530 9
                    if ($relationModel->validate()) {
531 9
                        $relationModel->save();
532
                    } else {
533 1
                        $this->_addError($relationModel, $owner, $relationName, self::prettyRelationName($relationName, $i));
534 1
                        throw new DbException('Related record ' . self::prettyRelationName($relationName, $i) . ' could not be saved.');
535
                    }
536
                }
537 11
                $junctionTableColumns = $this->_getJunctionTableColumns($relationName, $relationModel);
538 11
                $owner->link($relationName, $relationModel, $junctionTableColumns);
539
            } else {
540 13
                $existingRecords[] = $relationModel;
541
            }
542 19
            if (count($relationModel->dirtyAttributes)) {
543 4
                if ($relationModel->validate()) {
544 4
                    $relationModel->save();
545
                } else {
546
                    $this->_addError($relationModel, $owner, $relationName, self::prettyRelationName($relationName));
547
                    throw new DbException('Related record ' . self::prettyRelationName($relationName) . ' could not be saved.');
548
                }
549
            }
550
        }
551 18
        $junctionTablePropertiesUsed = array_key_exists($relationName, $this->_relationsExtraColumns);
552
553
        // Process existing added and deleted relations
554 18
        list($addedPks, $deletedPks) = $this->_computePkDiff(
555 18
            $this->_oldRelationValue[$relationName],
556 18
            $existingRecords,
557 18
            $junctionTablePropertiesUsed
558
        );
559
560
        // Deleted relations
561
        $initialModels = ArrayHelper::index($this->_oldRelationValue[$relationName], function (BaseActiveRecord $model) {
562 13
            return implode('-', $model->getPrimaryKey(true));
563 18
        });
564 18
        $initialRelations = $owner->{$relationName};
565 18
        foreach ($deletedPks as $key) {
566 2
            $owner->unlink($relationName, $initialModels[$key], true);
567
        }
568
569
        // Added relations
570 18
        $actualModels = ArrayHelper::index(
571 18
            $junctionTablePropertiesUsed ? $initialRelations : $owner->{$relationName},
572
            function (BaseActiveRecord $model) {
573 18
                return implode('-', $model->getPrimaryKey(true));
574 18
            }
575
        );
576 18
        foreach ($addedPks as $key) {
577 4
            $junctionTableColumns = $this->_getJunctionTableColumns($relationName, $actualModels[$key]);
578 4
            $owner->link($relationName, $actualModels[$key], $junctionTableColumns);
579
        }
580 18
    }
581
582
    /**
583
     * Return array of columns to save to the junction table for a related model having a many-to-many relation.
584
     * @param string $relationName
585
     * @param BaseActiveRecord $model
586
     * @return array
587
     * @throws \RuntimeException
588
     */
589 15
    private function _getJunctionTableColumns($relationName, $model)
590
    {
591 15
        $junctionTableColumns = [];
592 15
        if (array_key_exists($relationName, $this->_relationsExtraColumns)) {
593 1
            if (is_callable($this->_relationsExtraColumns[$relationName])) {
594 1
                $junctionTableColumns = $this->_relationsExtraColumns[$relationName]($model);
595
            } elseif (is_array($this->_relationsExtraColumns[$relationName])) {
596
                $junctionTableColumns = $this->_relationsExtraColumns[$relationName];
597
            }
598 1
            if (!is_array($junctionTableColumns)) {
599
                throw new RuntimeException(
600
                    'Junction table columns definition must return an array, got ' . gettype($junctionTableColumns)
601
                );
602
            }
603
        }
604 15
        return $junctionTableColumns;
605
    }
606
607
    /**
608
     * Compute the difference between two set of records using primary keys "tokens"
609
     * If third parameter is set to true all initial related records will be marked for removal even if their
610
     * properties did not change. This can be handy in a many-to-many relation involving a junction table.
611
     * @param BaseActiveRecord[] $initialRelations
612
     * @param BaseActiveRecord[] $updatedRelations
613
     * @param bool $forceSave
614
     * @return array
615
     */
616 18
    private function _computePkDiff($initialRelations, $updatedRelations, $forceSave = false)
617
    {
618
        // Compute differences between initial relations and the current ones
619
        $oldPks = ArrayHelper::getColumn($initialRelations, function (BaseActiveRecord $model) {
620 13
            return implode('-', $model->getPrimaryKey(true));
621 18
        });
622 18
        $newPks = ArrayHelper::getColumn($updatedRelations, function (BaseActiveRecord $model) {
623 13
            return implode('-', $model->getPrimaryKey(true));
624 18
        });
625 18
        if ($forceSave) {
626 1
            $addedPks = $newPks;
627 1
            $deletedPks = $oldPks;
628
        } else {
629 17
            $identicalPks = array_intersect($oldPks, $newPks);
630 17
            $addedPks = array_values(array_diff($newPks, $identicalPks));
631 17
            $deletedPks = array_values(array_diff($oldPks, $identicalPks));
632
        }
633 18
        return [$addedPks, $deletedPks];
634
    }
635
636
    /**
637
     * @param $relationName
638
     * @throws \yii\base\InvalidCallException
639
     */
640 19
    private function _afterSaveHasOneRelation($relationName)
641
    {
642
        /** @var BaseActiveRecord $owner */
643 19
        $owner = $this->owner;
644
645 19
        if ($this->_oldRelationValue[$relationName] !== $owner->{$relationName}) {
646 16
            if ($owner->{$relationName} instanceof BaseActiveRecord) {
647 15
                $owner->link($relationName, $owner->{$relationName});
648
            } else {
649 1
                if ($this->_oldRelationValue[$relationName] instanceof BaseActiveRecord) {
650 1
                    $owner->unlink($relationName, $this->_oldRelationValue[$relationName]);
651
                }
652
            }
653
        }
654 19
        if ($owner->{$relationName} instanceof BaseActiveRecord) {
655 17
            $owner->{$relationName}->save();
656
        }
657 19
    }
658
659
    /**
660
     * Get the list of owner model relations in order to be able to delete them after its deletion
661
     */
662 3
    public function beforeDelete()
663
    {
664
        /** @var BaseActiveRecord $owner */
665 3
        $owner = $this->owner;
666 3
        foreach ($this->_relationsCascadeDelete as $relationName => $params) {
667 3
            if ($params === true) {
668
                /** @var ActiveQuery $relation */
669 3
                $relation = $owner->getRelation($relationName);
670 3
                if (!empty($owner->{$relationName})) {
671 3
                    if ($relation->multiple === true) { // Has many relation
672 2
                        $this->_relationsToDelete = ArrayHelper::merge($this->_relationsToDelete, $owner->{$relationName});
673
                    } else {
674 1
                        $this->_relationsToDelete[] = $owner->{$relationName};
675
                    }
676
                }
677
            }
678
        }
679 3
    }
680
681
    /**
682
     * Delete related models marked as to be deleted
683
     * @throws Exception
684
     */
685 3
    public function afterDelete()
686
    {
687
        /** @var BaseActiveRecord $modelToDelete */
688 3
        foreach ($this->_relationsToDelete as $modelToDelete) {
689
            try {
690 3
                if (!$modelToDelete->delete()) {
691 1
                    throw new DbException('Could not delete the related record: ' . $modelToDelete::className() . '(' . VarDumper::dumpAsString($modelToDelete->primaryKey) . ')');
692
                }
693 1
            } catch (Exception $e) {
694 1
                Yii::warning(get_class($e) . ' was thrown while deleting related records during afterDelete event: ' . $e->getMessage(), __METHOD__);
695 1
                $this->_rollback();
696 1
                throw $e;
697
            }
698
        }
699 2
    }
700
701
    /**
702
     * Populates relations with input data
703
     * @param array $data
704
     */
705 5
    public function loadRelations($data)
706
    {
707
        /** @var BaseActiveRecord $owner */
708 5
        $owner = $this->owner;
709 5
        foreach ($this->_relations as $relationName) {
710
            /** @var ActiveQuery $relation */
711 5
            $relation = $owner->getRelation($relationName);
712 5
            $modelClass = $relation->modelClass;
713
            /** @var ActiveQuery $relationalModel */
714 5
            $relationalModel = new $modelClass;
715 5
            $formName = $relationalModel->formName();
716 5
            if (array_key_exists($formName, $data)) {
717 5
                $owner->{$relationName} = $data[$formName];
718
            }
719
        }
720 5
    }
721
722
    /**
723
     * Set the scenario for a given relation
724
     * @param $relationName
725
     * @param $scenario
726
     * @throws InvalidArgumentException
727
     */
728 2
    public function setRelationScenario($relationName, $scenario)
729
    {
730
        /** @var BaseActiveRecord $owner */
731 2
        $owner = $this->owner;
732 2
        $relation = $owner->getRelation($relationName, false);
733 2
        if (in_array($relationName, $this->_relations) && !is_null($relation)) {
734 1
            $this->_relationsScenario[$relationName] = $scenario;
735
        } else {
736 1
            throw new InvalidArgumentException('Unknown ' . $relationName . ' relation');
737
        }
738
739 1
    }
740
}
741