Passed
Push — master ( 56bd83...953e97 )
by Alban
02:28
created

_loadOrCreateRelationModel()   B

Complexity

Conditions 6
Paths 8

Size

Total Lines 14
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 6

Importance

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