Passed
Push — master ( 7970a5...86f293 )
by Alban
02:33
created

src/SaveRelationsBehavior.php (1 issue)

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

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
793
        }
794 7
        return $keyName;
795
    }
796
}
797