Passed
Push — master ( 7c65f5...b2dab0 )
by Alban
20:35
created

SaveRelationsBehavior::beforeDelete()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 11
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 5

Importance

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

669
                /** @scrutinizer ignore-call */ 
670
                $relation = $model->getRelation($relationName);

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...
670 3
                if (!empty($model->{$relationName})) {
671 3
                    if ($relation->multiple === true) { // Has many relation
672 2
                        $this->_relationsToDelete = ArrayHelper::merge($this->_relationsToDelete, $model->{$relationName});
673
                    } else {
674 1
                        $this->_relationsToDelete[] = $model->{$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 $model */
708 5
        $model = $this->owner;
709 5
        foreach ($this->_relations as $relationName) {
710
            /** @var ActiveQuery $relation */
711 5
            $relation = $model->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
                $model->{$relationName} = $data[$formName];
718
            }
719
        }
720 5
    }
721
}
722