Completed
Branch master (c2bcce)
by
unknown
55:38
created

ActiveRecord::saveRelation()   F

Complexity

Conditions 93
Paths > 20000

Size

Total Lines 238
Code Lines 128

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 238
rs 2
cc 93
eloc 128
nc 4294967295
nop 3

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * This file is part of the fangface/yii2-concord package
4
 *
5
 * For the full copyright and license information, please view
6
 * the file LICENSE.md that was distributed with this source code.
7
 *
8
 * @package fangface/yii2-concord
9
 * @author Fangface <[email protected]>
10
 * @copyright Copyright (c) 2014 Fangface <[email protected]>
11
 * @license https://github.com/fangface/yii2-concord/blob/master/LICENSE.md MIT License
12
 *
13
 */
14
15
namespace fangface\db;
16
17
use Yii;
18
use fangface\Tools;
19
use fangface\base\traits\ActionErrors;
20
use fangface\base\traits\AttributeSupport;
21
use fangface\behaviors\AutoSavedBy;
22
use fangface\behaviors\AutoDatestamp;
23
use fangface\db\ActiveAttributeRecord;
24
use fangface\db\ActiveRecord;
25
use fangface\db\ActiveRecordArray;
26
use fangface\db\ActiveRecordParentalInterface;
27
use fangface\db\ActiveRecordParentalTrait;
28
use fangface\db\ActiveRecordReadOnlyInterface;
29
use fangface\db\ActiveRecordReadOnlyTrait;
30
use fangface\db\ActiveRecordSaveAllInterface;
31
use fangface\db\ConnectionManager;
32
use fangface\db\Exception;
33
use yii\base\InvalidParamException;
34
use yii\base\ModelEvent;
35
use yii\base\UnknownMethodException;
36
use yii\db\ActiveRecord as YiiActiveRecord;
37
use yii\db\ActiveQuery;
38
use yii\db\ActiveQueryInterface;
39
use yii\db\Connection;
40
use yii\helpers\ArrayHelper;
41
42
/**
43
 * Concord Active Record
44
 *
45
 * @method ActiveRecord findOne($condition = null) static
46
 * @method ActiveRecord[] findAll($condition = null) static
47
 * @method ActiveRecord[] findByCondition($condition, $one) static
48
 */
49
class ActiveRecord extends YiiActiveRecord implements ActiveRecordParentalInterface, ActiveRecordReadOnlyInterface, ActiveRecordSaveAllInterface
50
{
51
52
    use ActionErrors;
53
    use ActiveRecordParentalTrait;
54
    use ActiveRecordReadOnlyTrait;
55
    use AttributeSupport;
56
57
    protected static $dbResourceName    = false;
58
    protected static $isClientResource  = false;
59
    protected static $dbTableName       = false;
60
    protected static $dbTableNameMethod = false; // yii, camel, default
61
62
    private $modelRelationMap           = [];
63
64
    protected $disableAutoBehaviors     = false;
65
    protected $createdAtAttr            = 'createdAt';
66
    protected $createdByAttr            = 'createdBy';
67
    protected $modifiedAtAttr           = 'modifiedAt';
68
    protected $modifiedByAttr           = 'modifiedBy';
69
70
    protected $applyDefaults            = true;
71
    protected $defaultsApplied          = false;
72
    protected $defaultsAppliedText      = false;
73
74
    private $savedNewChildRelations     = [];
75
76
77
    const SAVE_NO_ACTION                = 1;
78
    const SAVE_CASCADE                  = 2;
79
80
    const DELETE_NO_ACTION              = 4;
81
    const DELETE_CASCADE                = 8;
82
83
    const LINK_NONE                     = 16;
84
    const LINK_ONLY                     = 32;
85
    const LINK_FROM_PARENT              = 64;
86
    const LINK_FROM_CHILD               = 128;
87
    const LINK_BI_DIRECT                = 256;
88
    const LINK_FROM_PARENT_MAINT        = 512;
89
    const LINK_FROM_CHILD_MAINT         = 1024;
90
    const LINK_BI_DIRECT_MAINT          = 2048;
91
    const LINK_BI_DIRECT_MAINT_FROM_PARENT = 4096;
92
    const LINK_BI_DIRECT_MAINT_FROM_CHILD  = 8192;
93
94
    /**
95
     * @event ModelEvent an event that is triggered before saveAll()
96
     * You may set [[ModelEvent::isValid]] to be false to stop the update.
97
     */
98
    const EVENT_BEFORE_SAVE_ALL = 'beforeSaveAll';
99
100
    /**
101
     * @event Event an event that is triggered after saveAll() has completed
102
     */
103
    const EVENT_AFTER_SAVE_ALL = 'afterSaveAll';
104
105
    /**
106
     * @event Event an event that is triggered after saveAll() has failed
107
     */
108
    const EVENT_AFTER_SAVE_ALL_FAILED = 'afterSaveAllFailed';
109
110
    /**
111
     * @event ModelEvent an event that is triggered before saveAll()
112
     * You may set [[ModelEvent::isValid]] to be false to stop the update.
113
     */
114
    const EVENT_BEFORE_DELETE_FULL = 'beforeDeleteFull';
115
116
    /**
117
     * @event Event an event that is triggered after saveAll() has completed
118
     */
119
    const EVENT_AFTER_DELETE_FULL = 'afterDeleteFull';
120
121
    /**
122
     * @event Event an event that is triggered after saveAll() has failed
123
     */
124
    const EVENT_AFTER_DELETE_FULL_FAILED = 'afterDeleteFullFailed';
125
126
127
    /**
128
     * Get the name of the table associated with this ActiveRecord class.
129
     *
130
     * @param boolean $includeDbName
131
     * @return string
132
     */
133
    public static function tableName($includeDbName = true)
134
    {
135
        /** @var Connection $connection */
136
        $connection = static::getDb();
137
        $calledClass = get_called_class();
138
        $tablePrefix = $connection->tablePrefix;
139
        if (isset($calledClass::$dbTableName) && !is_null($calledClass::$dbTableName) && $calledClass::$dbTableName) {
140
            $tableName = $tablePrefix . $calledClass::$dbTableName;
141
        } else {
142
            $tableName = $tablePrefix . Tools::getDefaultTableNameFromClass($calledClass, (isset($calledClass::$tableNameMethod) && !is_null($calledClass::$tableNameMethod) && $calledClass::$tableNameMethod ? $calledClass::$tableNameMethod : 'default'));
143
        }
144
        if ($includeDbName) {
145
            preg_match("/dbname=([^;]+)/i", $connection->dsn, $matches);
146
            return $matches[1] . '.' . $tableName;
147
        }
148
        return $tableName;
149
    }
150
151
    /**
152
     * Returns the database connection used by this AR class.
153
     * By default, the "db" application component is used as the database connection.
154
     * You may override this method if you want to use a different database connection.
155
     * @throws Exception if no connection can be found
156
     * @return Connection|false The database connection used by this AR class.
157
     */
158
    public static function getDb()
159
    {
160
        $dbResourceName = 'db';
161
        $isClientResource = false;
162
163
        $calledClass = get_called_class();
164
        if (isset($calledClass::$dbResourceName) && !is_null($calledClass::$dbResourceName) && $calledClass::$dbResourceName) {
165
            $dbResourceName = $calledClass::$dbResourceName;
166
            $isClientResource = (isset($calledClass::$isClientResource) ? $calledClass::$isClientResource : false);
167
        }
168
169
        if (Yii::$app->has('dbFactory')) {
170
            /** @var ConnectionManager $dbFactory */
171
            $dbFactory = Yii::$app->get('dbFactory');
172
            return $dbFactory->getConnection($dbResourceName, true, true, $isClientResource);
173
        } elseif (Yii::$app->has($dbResourceName)) {
174
            return Yii::$app->get($dbResourceName);
175
        }
176
177
        throw new Exception('Database resource \'' . $dbResourceName . '\' not found');
178
    }
179
180
    /**
181
     * Reset the static dbResourceName (will impact all uses of the called class
182
     * until changed again)
183
     *
184
     * @param string $name
185
     */
186
    public function setDbResourceName($name)
187
    {
188
        if ($name) {
189
            $calledClass = get_called_class();
190
            $calledClass::$dbResourceName = $name;
191
        }
192
193
    }
194
195
    /**
196
     * Return the static dbResourceName
197
     */
198
    public function getDbResourceName()
199
    {
200
        $calledClass = get_called_class();
201
        if (isset($calledClass::$dbResourceName) && !is_null($calledClass::$dbResourceName) && $calledClass::$dbResourceName) {
202
            return $calledClass::$dbResourceName;
203
        }
204
    }
205
206
    /**
207
     * Returns a value indicating whether the model has an attribute with the specified name.
208
     * @param string $name the name of the attribute
209
     * @return boolean whether the model has an attribute with the specified name.
210
     */
211
    public function hasAttribute($name)
212
    {
213
        $result = parent::hasAttribute($name);
214
215
        if (!$this->applyDefaults || $this->defaultsApplied) {
216
            return $result;
217
        }
218
219
        if ($this->getIsNewRecord() && $result) {
220
            $this->applyDefaults(false);
221
        }
222
223
        return $result;
224
    }
225
226
    /**
227
     * Returns the named attribute value.
228
     * If this record is the result of a query and the attribute is not loaded,
229
     * null will be returned.
230
     * @param string $name the attribute name
231
     * @return mixed the attribute value. Null if the attribute is not set or does not exist.
232
     * @see hasAttribute()
233
     */
234
    public function getAttribute($name)
235
    {
236
        if ($this->hasAttribute($name)) {
237
            // we will now have optionally applied default values if this is a new record
238
            return parent::getAttribute($name);
239
        }
240
        return null;
241
    }
242
243
    /**
244
     * Sets the named attribute value.
245
     * @param string $name
246
     *        the attribute name
247
     * @param mixed $value
248
     *        the attribute value.
249
     * @throws InvalidParamException if the named attribute does not exist.
250
     * @throws Exception if the current record is read only
251
     * @see hasAttribute()
252
     */
253
    public function setAttribute($name, $value)
254
    {
255
        if ($this->getReadOnly()) {
256
            throw new Exception('Attempting to set attribute `' . $name . '` on a read only ' . Tools::getClassName($this) . ' model');
257
        }
258
259
        parent::setAttribute($name, $value);
260
    }
261
262
    /**
263
     * (non-PHPdoc)
264
     * @see \yii\base\Model::setAttributes()
265
     * @throws Exception if the current record is read only
266
     */
267
    public function setAttributes($values, $safeOnly = true)
268
    {
269
        if ($this->getReadOnly()) {
270
            throw new Exception('Attempting to set attributes on a read only ' . Tools::getClassName($this) . ' model');
271
        }
272
273
        parent::setAttributes($values, $safeOnly);
274
    }
275
276
277
    /**
278
     * Overriding BaseActiveRecord::setOldAttributes() because
279
     * we do not want to lose other old attributes following a call to this
280
     * function after a save which may have been limited to only some attributes effectively
281
     * marking all other attributes as dirty when they might not actually be
282
     * @see \yii\db\BaseActiveRecord::setOldAttributes($values)
283
     */
284
    public function setOldAttributes($values)
285
    {
286
        if ($values !== null && is_array($values) && $values) {
287
            parent::setOldAttributes(array_merge($this->getOldAttributes(), $values));
288
        } else {
289
            parent::setOldAttributes($values);
290
        }
291
    }
292
293
    /**
294
     * {@inheritDoc}
295
     * @see \yii\db\BaseActiveRecord::getDirtyAttributes($names)
296
     */
297
    public function getDirtyAttributes($names = null)
298
    {
299
        if ($names === null || is_bool($names)) {
300
            $names = $this->attributes();
301
        }
302
        return parent::getDirtyAttributes($names);
303
    }
304
305
    /**
306
     * (non-PHPdoc)
307
     * @see \yii\base\Model::load($data, $formName)
308
     * @throws Exception if the current record is read only
309
     */
310
    public function load($data, $formName = null)
311
    {
312
        if ($this->getReadOnly()) {
313
            throw new Exception('Attempting to load attributes on a read only ' . Tools::getClassName($this) . ' model');
314
        }
315
316
        if ($this->getIsNewRecord() && $this->applyDefaults && !$this->defaultsApplied) {
317
            $this->applyDefaults(false);
318
        }
319
320
        return parent::load($data, $formName);
321
    }
322
323
    /**
324
     * Apply defaults to the model
325
     * @param boolean $skipIfSet if existing value should be preserved
326
     */
327
    public function applyDefaults($skipIfSet = true)
328
    {
329
        if ($this->applyDefaults && !$this->defaultsApplied) {
330
            $this->loadDefaultValues($skipIfSet);
331
        }
332
    }
333
334
    /**
335
     * (non-PHPdoc)
336
     * @see \yii\db\ActiveRecord::loadDefaultValues()
337
     */
338
    public function loadDefaultValues($skipIfSet = true)
339
    {
340
        $this->defaultsApplied = true;
341
        $stru = self::getTableSchema();
342
        $columns = $stru->columns;
343
        foreach ($columns as $colName => $spec) {
344
            if ($spec->isPrimaryKey && $spec->autoIncrement) {
345
                // leave value alone
346
            } else {
347
                if (isset($this->$colName) && !is_null($this->$colName)) {
348
                    // use the default value provided by the variable within the model
349
                    $defaultValue = Tools::formatAttributeValue($this->$colName, $spec);
350
                    // we want this all to work with magic getters and setters so now is a good time to remove the attribute from the object
351
                    unset($this->$colName);
352
                } else {
353
                    // use the default value from the DB if available
354
                    $defaultValue = Tools::formatAttributeValue('__DEFAULT__', $spec);
355
                }
356
357
                if (is_null($defaultValue)) {
358
                    // leave as null
359
                } elseif ($skipIfSet && $this->getAttribute($colName) !== null) {
360
                    // leave as is
361
                } else {
362
                    $this->setAttribute($colName, $defaultValue);
363
                    if (($spec->type == 'text' || $spec->type == 'binary') && $defaultValue == '') {
364
                        // some fields need to have the default set and included in the sql statements even if not set to anything yet
365
                        $this->defaultsAppliedText = true;
366
                    } else {
367
                        $this->setOldAttribute($colName, $defaultValue);
368
                    }
369
                }
370
            }
371
        }
372
        return $this;
373
    }
374
375
    /**
376
     * Default ActiveRecord behaviors (typically createdBy, createdAt, modifiedBy and modifiedAt
377
     * (non-PHPdoc)
378
     * @see \yii\base\Component::behaviors()
379
     */
380
    public function behaviors()
381
    {
382
        $defaults = array();
383
384
        if ($this->disableAutoBehaviors) {
385
386
            // do not apply the default behaviors - by default even if not switched off the behaviors will work
387
            // even if fields required to support them do not exist, but this option allows for the behaviors not
388
            // to be attached in the first place
389
390
        } else {
391
392
            if ($this->createdByAttr || $this->modifiedByAttr) {
393
394
                $defaults['savedby'] = [
395
                    'class' => AutoSavedBy::className()
396
                ];
397
398
                $defaults['savedby']['attributes'][ActiveRecord::EVENT_BEFORE_INSERT] = array();
399
400
                if ($this->createdByAttr) {
401
                    $defaults['savedby']['attributes'][ActiveRecord::EVENT_BEFORE_INSERT][] = $this->createdByAttr;
402
                }
403
404
                if ($this->modifiedByAttr) {
405
                    $defaults['savedby']['attributes'][ActiveRecord::EVENT_BEFORE_INSERT][] = $this->modifiedByAttr;
406
                    $defaults['savedby']['attributes'][ActiveRecord::EVENT_BEFORE_UPDATE] = array();
407
                    $defaults['savedby']['attributes'][ActiveRecord::EVENT_BEFORE_UPDATE][] = $this->modifiedByAttr;
408
                }
409
410
            }
411
412
            if ($this->createdAtAttr || $this->modifiedAtAttr) {
413
414
                $defaults['datestamp'] = [
415
                    'class' => AutoDatestamp::className()
416
                ];
417
418
                $defaults['datestamp']['attributes'][ActiveRecord::EVENT_BEFORE_INSERT] = array();
419
420
                if ($this->createdAtAttr) {
421
                    $defaults['datestamp']['attributes'][ActiveRecord::EVENT_BEFORE_INSERT][] = $this->createdAtAttr;
422
                }
423
424
                if ($this->modifiedAtAttr) {
425
                    $defaults['datestamp']['attributes'][ActiveRecord::EVENT_BEFORE_INSERT][] = $this->modifiedAtAttr;
426
                    $defaults['datestamp']['attributes'][ActiveRecord::EVENT_BEFORE_UPDATE] = array();
427
                    $defaults['datestamp']['attributes'][ActiveRecord::EVENT_BEFORE_UPDATE][] = $this->modifiedAtAttr;
428
                }
429
430
            }
431
432
        }
433
434
        return $defaults;
435
    }
436
437
    /**
438
     * Determine if model has any unsaved changes
439
     *
440
     * @param boolean $checkRelations should changes in relations be checked as well
441
     * @return boolean
442
     */
443
    public function hasChanges($checkRelations = false)
444
    {
445
446
        if ($this->getOldAttributes() == [] && !$this->getIsNewRecord()) {
447
            $hasChanges = false;
448
        } elseif ($this->getIsNewRecord() && $this->defaultsApplied && $this->defaultsAppliedText) {
449
            $hasChanges = $this->getIsNewDirtyAttributes();
450
        } else {
451
            $hasChanges = $this->getDirtyAttributes();
452
        }
453
454
        if (!$hasChanges) {
455
            /*
456
             * check to see if any of our sub relations have unsaved changes that would be saved
457
             * if we called saveAll()
458
             */
459
            if ($checkRelations) {
460
                $this->processModelMap();
461
                if ($this->modelRelationMap) {
462
                    foreach ($this->modelRelationMap as $relation => $relationInfo) {
463
                        if ($relationInfo['onSaveAll'] == self::SAVE_CASCADE && $this->isRelationPopulated($relation)) {
464
465
                            $isReadOnly = ($relationInfo['readOnly'] === null || !$relationInfo['readOnly'] ? false : true);
466
                            if ($this->$relation instanceof ActiveRecordReadOnlyInterface) {
467
                                $isReadOnly = $this->$relation->getReadOnly();
468
                            }
469
470
                            if (!$isReadOnly) {
471
                                if ($this->$relation instanceof ActiveRecordSaveAllInterface) {
472
                                    $hasChanges = $this->$relation->hasChanges($checkRelations);
473
                                } elseif (method_exists($this->$relation, 'getDirtyAttributes')) {
474
                                    if ($this->$relation->getDirtyAttributes()) {
475
                                        $hasChanges = true;
476
                                    }
477
                                }
478
                            }
479
                        }
480
481
                        if ($hasChanges) {
482
                            break;
483
                        }
484
                    }
485
                }
486
            }
487
        }
488
489
        return ($hasChanges ? true : false);
490
    }
491
492
    /**
493
     * Return if an attribute has changed
494
     * @param string $attribute [optional] if not specified returns true if anything within the currect AR has changed
495
     * @return boolean
496
     */
497
    public function hasChanged($attribute = null)
498
    {
499
        if ($this->getOldAttributes() == [] && !$this->getIsNewRecord()) {
500
            $hasChanges = false;
501
        } elseif ($this->getIsNewRecord() && $this->defaultsApplied && $this->defaultsAppliedText) {
502
            $hasChanges = $this->getIsNewDirtyAttributes();
503
        } else {
504
            $hasChanges = $this->getDirtyAttributes();
505
        }
506
        if (is_null($attribute)) {
507
            return ($hasChanges ? true : false);
508
        } elseif ($hasChanges && isset($hasChanges[$attribute])) {
509
            return true;
510
        }
511
        return false;
512
    }
513
514
    /**
515
     * Returns the attribute values that have been modified in this new record since loading the default values
516
     * Only really gets used by self::hasChanges() when _oldAttributes == [] (typically when no default values are set
517
     * because they are all set to null in the table schema)
518
     * @param string[]|null $names the names of the attributes whose values may be returned if they are
519
     * changed recently. If null, [[attributes()]] will be used.
520
     * @return array the changed attribute values (name-value pairs)
521
     */
522
    public function getIsNewDirtyAttributes($names = null)
523
    {
524
        if (!$this->getIsNewRecord() || !$this->defaultsApplied) {
525
            return $this->getDirtyAttributes($names);
526
        }
527
        $attributes = [];
528
        $theAttributes = $this->getAttributes($names);
529
        if ($theAttributes) {
530
            $stru = self::getTableSchema();
531
            $columns = $stru->columns;
532
            foreach ($theAttributes as $name => $value) {
533
                $spec = ArrayHelper::getValue($columns, $name, []);
534
                if ($spec) {
535
                    if ($spec->isPrimaryKey && $spec->autoIncrement) {
536
                        if ($value !== null) {
537
                            $attributes[$name] = $value;
538
                        }
539
                    } else {
540
                        $defaultValue = Tools::formatAttributeValue('__DEFAULT__', $spec);
541
                        if ($defaultValue === null && $value !== null) {
542
                            $attributes[$name] = $value;
543
                        } elseif ($defaultValue != $value) {
544
                            $attributes[$name] = $value;
545
                        }
546
                    }
547
                }
548
            }
549
        }
550
        return $attributes;
551
    }
552
553
    /**
554
     * This method is called when the AR object is created and populated with the query result.
555
     * The default implementation will trigger an [[EVENT_AFTER_FIND]] event.
556
     * When overriding this method, make sure you call the parent implementation to ensure the
557
     * event is triggered.
558
     */
559
    public function afterFind()
560
    {
561
        $this->setIsNewRecord(false);
562
        parent::afterFind();
563
    }
564
565
    /**
566
     * Save the current record
567
     *
568
     * @see \yii\db\BaseActiveRecord::save()
569
     *
570
     * @param boolean $runValidation
571
     *        should validations be executed on all models before allowing save()
572
     * @param array $attributes
573
     *        which attributes should be saved (default null means all changed attributes)
574
     * @param boolean $hasParentModel
575
     *        whether this method was called from the top level or by a parent
576
     *        If false, it means the method was called at the top level
577
     * @param boolean $fromSaveAll
578
     *        has the save() call come from saveAll() or not
579
     * @return boolean
580
     *        did save() successfully process
581
     */
582
    public function save($runValidation = true, $attributes = null, $hasParentModel = false, $fromSaveAll = false)
583
    {
584
        if ($this->getReadOnly() && !$hasParentModel) {
585
586
            // return failure if we are at the top of the tree and should not be asking to saveAll
587
            // not allowed to amend or delete
588
            $message = 'Attempting to save on ' . Tools::getClassName($this) . ' readOnly model';
589
            //$this->addActionError($message);
590
            throw new Exception($message);
591
592
        } elseif ($this->getReadOnly() && $hasParentModel) {
593
594
            $message = 'Skipping save on ' . Tools::getClassName($this) . ' readOnly model';
595
            $this->addActionWarning($message);
596
            return true;
597
598
        } else {
599
            if ($this->hasChanges()) {
600
601
                try {
602
                    $ok = parent::save($runValidation, $attributes);
603
                    if ($ok) {
604
                        $this->setIsNewRecord(false);
605
                    }
606
                } catch (\Exception $e) {
607
                    $ok = false;
608
                    $this->addActionError($e->getMessage(), $e->getCode());
609
                }
610
611
                if (false) {
612
                    if ($this->hasActionErrors()) {
613
                        $actionError = $this->getFirstActionError();
614
                        throw new Exception(print_r($actionError['message'], true), $actionError['code']);
615
                    }
616
                }
617
618
                return $ok;
619
            } elseif ($this->getIsNewRecord() && !$hasParentModel) {
620
                $message = 'Attempting to save an empty ' . Tools::getClassName($this) . ' model';
621
                //$this->addActionError($message);
622
                throw new Exception($message);
623
            }
624
        }
625
626
        return true;
627
    }
628
629
    /**
630
     * (non-PHPdoc)
631
     * @see \yii\db\ActiveRecord::update()
632
     */
633
    public function update($runValidation = true, $attributeNames = null, $hasParentModel = false, $fromUpdateAll = false)
634
    {
635
        if ($this->getReadOnly() && !$hasParentModel) {
636
637
            // return failure if we are at the top of the tree and should not be asking to supdateAll
638
            // not allowed to amend or delete
639
            $message = 'Attempting to update on ' . Tools::getClassName($this) . ' readOnly model';
640
            //$this->addActionError($message);
641
            throw new Exception($message);
642
643
        } elseif ($this->getReadOnly() && $hasParentModel) {
644
645
            $message = 'Skipping update on ' . Tools::getClassName($this) . ' readOnly model';
646
            $this->addActionWarning($message);
647
            return true;
648
649
        } else {
650
            try {
651
                $ok = parent::update($runValidation, $attributeNames);
652
            } catch (\Exception $e) {
653
                $ok = false;
654
                $this->addActionError($e->getMessage(), $e->getCode());
655
            }
656
            return $ok;
657
        }
658
    }
659
660
    /**
661
     * Touch the model (update modified datetime stamps
662
     * @param boolean $incrementRevision default true
663
     */
664
    public function touch($incrementRevision = true)
665
    {
666
        if ($incrementRevision && $this->hasAttribute('revision')) {
667
            $this->revision++;
668
        } elseif ($this->hasAttribute('modified_at')) {
669
            $this->modified_at = date(Tools::DATETIME_DATABASE);
670
        } elseif ($this->hasAttribute('modifiedAt')) {
671
            $this->modifiedAt = date(Tools::DATETIME_DATABASE);
672
        }
673
        $this->saveAll();
674
    }
675
676
    /**
677
     * Optionally fix and truncate strings for fields that may contain unknown responses
678
     *
679
     * @see \yii\db\BaseActiveRecord::beforeSave($insert)
680
     */
681
    public function beforeSave($insert) {
682
        if (parent::beforeSave($insert)) {
683
            $beforeSaveStringFields = $this->beforeSaveStringFields();
684
            if ($beforeSaveStringFields) {
685
                $this->checkAndFixLongStrings($beforeSaveStringFields);
686
            }
687
            return true;
688
        }
689
        return false;
690
    }
691
692
    /**
693
     * (non-PHPdoc)
694
     * @see \yii\db\BaseActiveRecord::afterSave($insert, $changedAttributes)
695
     */
696
    public function afterSave($insert, $changedAttributes)
697
    {
698
        $this->setIsNewRecord(false);
699
        parent::afterSave($insert, $changedAttributes);
700
    }
701
702
    /**
703
     * Check changed attributes and compare the table schema, truncating any fields as required
704
     *
705
     * @param array $attributes Array of attributes to limit checking to those only
706
     */
707
    public function checkAndFixLongStrings($attributes = false) {
708
        $hasChanges = $this->getDirtyAttributes();
709
        if ($hasChanges) {
710
            $columns = self::getTableSchema()->columns;
711
            foreach ($hasChanges as $name => $value) {
712
                if (array_key_exists($name, $columns)) {
713
                    if (!$attributes || in_array($name, $attributes) !== false) {
714
                        if ($columns[$name]->type == 'string' && $columns[$name]->size) {
715
                            if (Tools::strlen($value) > $columns[$name]->size) {
716
                                $this->setAttribute($name, Tools::substr($value, 0, $columns[$name]->size));
717
                            }
718
                        }
719
                    }
720
                }
721
            }
722
        }
723
    }
724
725
    /**
726
     * Perform a saveAll() call but push the request down the model map including
727
     * models that are not currently loaded (perhaps because child models need to
728
     * pick up new values from parents
729
     *
730
     * @param boolean $runValidation
731
     *        should validations be executed on all models before allowing saveAll()
732
     * @return boolean
733
     *         did saveAll() successfully process
734
     */
735
    public function push($runValidation = true)
736
    {
737
        return $this->saveAll($runValidation, false, true);
738
    }
739
740
    /**
741
     * Saves the current record but also loops through defined relationships (if appropriate)
742
     * to save those as well
743
     *
744
     * @param boolean $runValidation
745
     *        should validations be executed on all models before allowing saveAll()
746
     * @param boolean $hasParentModel
747
     *        whether this method was called from the top level or by a parent
748
     *        If false, it means the method was called at the top level
749
     * @param boolean $push
750
     *        is saveAll being pushed onto lazy (un)loaded models as well
751
     * @param array $attributes
752
     *        which attributes should be saved (default null means all changed attributes)
753
     * @return boolean
754
     *         did saveAll() successfully process
755
     */
756
    public function saveAll($runValidation = true, $hasParentModel = false, $push = false, $attributes = null)
757
    {
758
759
        $this->clearActionErrors();
760
761
        if ($this->getReadOnly() && !$hasParentModel) {
762
763
            // return failure if we are at the top of the tree and should not be asking to saveAll
764
            // not allowed to amend or delete
765
            $message = 'Attempting to saveAll on ' . Tools::getClassName($this) . ' readOnly model';
766
            //$this->addActionError($message);
767
            throw new Exception($message);
768
769
        } elseif ($this->getReadOnly() && $hasParentModel) {
770
771
            $message = 'Skipping saveAll on ' . Tools::getClassName($this) . ' readOnly model';
772
            $this->addActionWarning($message);
773
            return true;
774
775
        } elseif (!$this->getReadOnly()) {
776
777
            if (!$hasParentModel) {
778
779
                // run beforeSaveAll and abandon saveAll() if it returns false
780
                if (!$this->beforeSaveAllInternal($runValidation, $hasParentModel, $push, $attributes)) {
781
                    \Yii::info('Model not saved due to beforeSaveALlInternal returning false.', __METHOD__);
782
                    return false;
783
                }
784
785
                /*
786
                 * note if validation was required it has already now been executed as part of the beforeSaveAll checks,
787
                 * so no need to do them again as part of save
788
                 */
789
                $runValidation = false;
790
            }
791
792
            // start with empty array
793
            $this->savedNewChildRelations = array();
794
795
            $isNewRecord = $this->getIsNewRecord();
796
797
            try {
798
799
                $ok = $this->saveRelation('fromChild', $runValidation, $push);
800
801
                if ($ok) {
802
803
                    $skipFromParentRelationSave = false;
804
805
                    if ($this->hasChanges()) {
806
807
                        $ok = $this->save($runValidation, $attributes, $hasParentModel, true);
808
809
                    } elseif ($isNewRecord && !$hasParentModel) {
810
811
                        // only return false for no point saving when on the top level
812
                        $message = 'Attempting to save an empty ' . Tools::getClassName($this) . ' model';
813
                        $this->addActionError($message);
814
                        $ok = false;
815
816
                    } elseif ($isNewRecord && $hasParentModel) {
817
818
                        // no point saving children if we have not saved this parent
819
                        $skipFromParentRelationSave = true;
820
821
                    }
822
823
                    if ($ok && !$skipFromParentRelationSave) {
824
825
                        $ok = $this->saveRelation('fromParent', $runValidation, $push);
826
827
                    }
828
829
                }
830
831
            } catch (\Exception $e) {
832
                $ok = false;
833
                $this->addActionError($e->getMessage(), $e->getCode());
834
            }
835
836
            if (!$hasParentModel) {
837
                if ($ok) {
838
                    $this->afterSaveAllInternal();
839
                } else {
840
                    $this->afterSaveAllFailedInternal();
841
                    if (false) {
842
                        if ($this->hasActionErrors()) {
843
                            $actionError = $this->getFirstActionError();
844
                            throw new Exception(print_r($actionError['message'], true), $actionError['code']);
845
                        }
846
                    }
847
                }
848
            }
849
850
            // reset
851
            $this->savedNewChildRelations = array();
852
853
            return $ok;
854
855
        }
856
857
        return true;
858
    }
859
860
    private function saveRelation($saveRelationType = 'fromParent', $runValidation = true, $push = false)
861
    {
862
        $allOk = true;
863
864
        $this->processModelMap();
865
        if ($this->modelRelationMap) {
866
867
            $limitAutoLinkType = self::LINK_NONE;
868
            if ($saveRelationType == 'fromParent') {
869
                $limitAutoLinkType = (self::LINK_FROM_PARENT | self::LINK_FROM_PARENT_MAINT | self::LINK_BI_DIRECT | self::LINK_BI_DIRECT_MAINT | self::LINK_BI_DIRECT_MAINT_FROM_PARENT | self:: LINK_BI_DIRECT_MAINT_FROM_CHILD);
870
            } elseif ($saveRelationType == 'fromChild') {
871
                $limitAutoLinkType = (self::LINK_FROM_CHILD | self::LINK_FROM_CHILD_MAINT | self::LINK_BI_DIRECT | self::LINK_BI_DIRECT_MAINT | self:: LINK_BI_DIRECT_MAINT_FROM_CHILD | self::LINK_BI_DIRECT_MAINT_FROM_PARENT | self::LINK_NONE);
872
            }
873
874
            foreach ($this->modelRelationMap as $relation => $relationInfo) {
875
876
                if ($relationInfo['onSaveAll'] == self::SAVE_CASCADE && ($this->isRelationPopulated($relation) || ($push && isset($this->$relation)))) {
877
878
                    if (($limitAutoLinkType & $relationInfo['autoLinkType']) == $relationInfo['autoLinkType']) {
879
880
                        $ok = true;
881
882
                        $isReadOnly = ($relationInfo['readOnly'] === null || !$relationInfo['readOnly'] ? false : true);
883
                        if ($this->$relation instanceof ActiveRecordReadOnlyInterface) {
884
                            $isReadOnly = $this->$relation->getReadOnly();
885
                        }
886
887
                        if (!$isReadOnly) {
888
889
                            $relationIsNew = false;
890
891
                            $isActiveRecordArray = ($this->$relation instanceof ActiveRecordArray);
892
893
                            if (!$isActiveRecordArray) {
894
                                $relationIsNew = $this->$relation->getIsNewRecord();
895
                            }
896
897
                            if ($saveRelationType == 'fromParent') {
898
899
                                $applyLinks = true;
900
                                $applyLinksNewOnly = false;
901
                                if ($relationInfo['autoLinkType'] == self::LINK_FROM_PARENT) {
902
                                    $applyLinksNewOnly = true;
903
                                } elseif ($relationInfo['autoLinkType'] == self::LINK_FROM_PARENT_MAINT) {
904
                                    // apply
905
                                } elseif ($relationInfo['autoLinkType'] == self::LINK_BI_DIRECT && ($limitAutoLinkType & self::LINK_FROM_PARENT) == self::LINK_FROM_PARENT) {
906
                                    $applyLinksNewOnly = true;
907
                                } elseif ($relationInfo['autoLinkType'] == self::LINK_BI_DIRECT_MAINT && ($limitAutoLinkType & self::LINK_FROM_PARENT_MAINT) == self::LINK_FROM_PARENT_MAINT) {
908
                                    // apply
909
                                } elseif ($relationInfo['autoLinkType'] == self::LINK_BI_DIRECT_MAINT_FROM_PARENT && ($limitAutoLinkType & self::LINK_BI_DIRECT_MAINT_FROM_PARENT) == self::LINK_BI_DIRECT_MAINT_FROM_PARENT) {
910
                                    // apply
911
                                } elseif ($relationInfo['autoLinkType'] == self::LINK_BI_DIRECT_MAINT_FROM_CHILD && ($limitAutoLinkType & self::LINK_BI_DIRECT_MAINT_FROM_CHILD) == self::LINK_BI_DIRECT_MAINT_FROM_CHILD) {
912
                                    $applyLinksNewOnly = true;
913
                                } else {
914
                                    $applyLinks = false;
915
                                }
916
917
                                if ($applyLinks && $applyLinksNewOnly && !$isActiveRecordArray) {
918
                                    if (in_array($relation, $this->savedNewChildRelations)) {
919
                                        // relation was new before it was saved in the fromChild iteration of saveRelation()
920
                                    } else {
921
                                        $applyLinks = $relationIsNew;
922
                                    }
923
                                }
924
925
                                if ($applyLinks && !$isActiveRecordArray && ($relationIsNew || in_array($relation, $this->savedNewChildRelations))) {
926
                                    // we only want to apply links fromParent on new records if something
927
                                    // else within the record has also been changed (to avoid saving a blank record)
928
                                    if (in_array($relation, $this->savedNewChildRelations)) {
929
                                        // definately can apply these values now that the child has been created
930
                                    } elseif ($this->$relation instanceof ActiveRecordSaveAllInterface) {
931
                                        $applyLinks = $this->$relation->hasChanges(true);
932
                                    } elseif (method_exists($this->$relation, 'getDirtyAttributes')) {
933
                                        if (!$this->$relation->getDirtyAttributes()) {
934
                                            $applyLinks = false;
935
                                        }
936
                                    }
937
                                }
938
939
                                if ($applyLinks) {
940
941
                                    if (isset($relationInfo['autoLink']['fromParent'])) {
942
                                        $autoLinkLink = $relationInfo['autoLink']['fromParent'];
943
                                    } else {
944
                                        $autoLinkLink = ($relationInfo['autoLink'] ? $relationInfo['autoLink'] : ($relationInfo['link'] ? $relationInfo['link'] : false));
945
                                    }
946
947
                                    if ($autoLinkLink) {
948
                                        foreach ($autoLinkLink as $k => $v) {
949
                                            if ($this->getAttribute($v) !== null) {
950
                                                if ($isActiveRecordArray) {
951
                                                    // only update objects in the array if they already have other changes, we don't want to save records that were otherwise not used
952
                                                    if ($v == '__KEY__') {
953
                                                        $this->$relation->setAttribute($k, $v, (!$push), $applyLinksNewOnly);
954
                                                    } else {
955
                                                        $this->$relation->setAttribute($k, $this->getAttribute($v), (!$push), $applyLinksNewOnly);
956
                                                    }
957
                                                } else {
958
                                                    if ($this->$relation->getAttribute($k) != $this->getAttribute($v)) {
959
                                                        $this->$relation->setAttribute($k, $this->getAttribute($v));
960
                                                    }
961
                                                }
962
                                            }
963
                                        }
964
                                    }
965
                                }
966
                            }
967
968
                            if ($isActiveRecordArray) {
969
970
                                $ok = $this->$relation->saveAll($runValidation, true, $push);
971
972
                            } else {
973
974
                                $hasChanges = true;
975
                                if (!$this->getIsNewRecord() && $this->$relation instanceof ActiveRecord) {
976
977
                                    // sub models may exist that have changes even though the relation itself does not have any changes
978
                                    // also we may need to apply auto link updates fromChild and fromParent depending on changes to this
979
                                    // and/or changes to sub models
980
981
                                    if ($relationIsNew || in_array($relation, $this->savedNewChildRelations)) {
982
                                        // we only want to apply links fromParent on new records if something
983
                                        // else within the record has also been changed (to avoid saving a blank record)
984
                                        if (in_array($relation, $this->savedNewChildRelations)) {
985
                                            // definately can apply these values now that the child has been created
986
                                        } elseif ($this->$relation instanceof ActiveRecordSaveAllInterface) {
987
                                            $hasChanges = $this->$relation->hasChanges(true);
988
                                        } elseif (method_exists($this->$relation, 'getDirtyAttributes')) {
989
                                            if (!$this->$relation->getDirtyAttributes()) {
990
                                                $hasChanges = false;
991
                                            }
992
                                        }
993
                                    }
994
995
                                } elseif ($this->$relation instanceof ActiveRecordSaveAllInterface) {
996
                                    $hasChanges = $this->$relation->hasChanges(true);
997
                                } elseif (method_exists($this->$relation, 'getDirtyAttributes')) {
998
                                    if (!$this->$relation->getDirtyAttributes()) {
999
                                        $hasChanges = false;
1000
                                    }
1001
                                }
1002
1003
                                if ($hasChanges) {
1004
                                    $ok = false;
1005
                                    if ($this->$relation instanceof ActiveRecordSaveAllInterface) {
1006
                                        $ok = $this->$relation->saveAll($runValidation, true, $push);
1007
                                    } elseif (method_exists($this->$relation, 'save')) {
1008
                                        $ok = $this->$relation->save($runValidation);
1009
                                        if ($ok) {
1010
                                            $this->$relation->setIsNewRecord(false);
1011
                                        }
1012
                                    }
1013
                                }
1014
1015
                                if ($ok && $saveRelationType == 'fromChild') {
1016
1017
                                    if ($relationIsNew && $hasChanges) {
1018
                                        // a record was saved
1019
                                        $this->savedNewChildRelations[] = $relation;
1020
                                    }
1021
1022
                                    $applyLinks = true;
1023
                                    $applyLinksNewOnly = false;
1024
                                    if ($relationInfo['autoLinkType'] == self::LINK_FROM_CHILD) {
1025
                                        $applyLinksNewOnly = true;
1026
                                    } elseif ($relationInfo['autoLinkType'] == self::LINK_FROM_CHILD_MAINT) {
1027
                                        // apply
1028
                                    } elseif (($relationInfo['autoLinkType'] == self::LINK_BI_DIRECT) && (($limitAutoLinkType & self::LINK_FROM_CHILD) == self::LINK_FROM_CHILD)) {
1029
                                        $applyLinksNewOnly = true;
1030
                                    } elseif (($relationInfo['autoLinkType'] == self::LINK_BI_DIRECT_MAINT) && (($limitAutoLinkType & self::LINK_FROM_CHILD_MAINT) == self::LINK_FROM_CHILD_MAINT)) {
1031
                                        // apply
1032
                                    } elseif ($relationInfo['autoLinkType'] == self::LINK_BI_DIRECT_MAINT_FROM_CHILD && ($limitAutoLinkType & self::LINK_BI_DIRECT_MAINT_FROM_CHILD) == self::LINK_BI_DIRECT_MAINT_FROM_CHILD) {
1033
                                        // apply
1034
                                    } elseif ($relationInfo['autoLinkType'] == self::LINK_BI_DIRECT_MAINT_FROM_PARENT && ($limitAutoLinkType & self::LINK_BI_DIRECT_MAINT_FROM_PARENT) == self::LINK_BI_DIRECT_MAINT_FROM_PARENT) {
1035
                                        $applyLinksNewOnly = true;
1036
                                    } else {
1037
                                        $applyLinks = false;
1038
                                    }
1039
1040
                                    if ($applyLinks && $applyLinksNewOnly && !$isActiveRecordArray) {
1041
                                        $applyLinks = $relationIsNew;
1042
                                    }
1043
1044
1045
                                    if ($applyLinks) {
1046
1047
                                        if (isset($relationInfo['autoLink']['fromChild'])) {
1048
                                            $autoLinkLink = $relationInfo['autoLink']['fromChild'];
1049
                                        } else {
1050
                                            $autoLinkLink = ($relationInfo['autoLink'] ? $relationInfo['autoLink'] : false);
1051
                                        }
1052
1053
                                        if ($autoLinkLink) {
1054
                                            foreach ($autoLinkLink as $k => $v) {
1055
                                                if ($this->$relation->getAttribute($k) !== null) {
1056
                                                    if ($this->getAttribute($v) != $this->$relation->getAttribute($k)) {
1057
                                                        $this->setAttribute($v, $this->$relation->getAttribute($k));
1058
                                                    }
1059
                                                }
1060
                                            }
1061
                                        }
1062
                                    }
1063
                                }
1064
                            }
1065
1066
                            if (method_exists($this->$relation, 'hasActionErrors')) {
1067
                                if ($this->$relation->hasActionErrors()) {
1068
                                    $this->mergeActionErrors($this->$relation->getActionErrors());
1069
                                }
1070
                            }
1071
1072
                            if (method_exists($this->$relation, 'hasActionWarnings')) {
1073
                                if ($this->$relation->hasActionWarnings()) {
1074
                                    $this->mergeActionWarnings($this->$relation->getActionWarnings());
1075
                                }
1076
                            }
1077
1078
                            if (!$ok) {
1079
                                $allOk = false;
1080
                                break;
1081
                            }
1082
1083
                        } else {
1084
1085
                            $message = 'Skipping saveAll (' . $saveRelationType . ') on ' . $relation . ' readOnly model';
1086
                            $this->addActionWarning($message);
1087
1088
                        }
1089
1090
                    }
1091
                }
1092
            }
1093
        }
1094
1095
        return $allOk;
1096
1097
    }
1098
1099
    /**
1100
     * This method is called at the beginning of a saveAll() request on a record or model map
1101
     *
1102
     * @param boolean $runValidation
1103
     *        should validations be executed on all models before allowing saveAll()
1104
     * @param boolean $hasParentModel
1105
     *        whether this method was called from the top level or by a parent
1106
     *        If false, it means the method was called at the top level
1107
     * @param boolean $push
1108
     *        is saveAll being pushed onto lazy (un)loaded models as well
1109
     * @param array $attributes
1110
     *        which attributes should be saved (default null means all changed attributes)
1111
     * @return boolean whether the saveAll() method call should continue
1112
     *        If false, saveAll() will be cancelled.
1113
     */
1114
    public function beforeSaveAllInternal($runValidation = true, $hasParentModel = false, $push = false, $attributes = null)
1115
    {
1116
1117
        $this->clearActionErrors();
1118
        $this->resetChildHasChanges();
1119
        $transaction = null;
1120
1121
        $canSaveAll = true;
1122
1123
        if (!$hasParentModel) {
1124
            $event = new ModelEvent;
1125
            $this->trigger(self::EVENT_BEFORE_SAVE_ALL, $event);
1126
            $canSaveAll = $event->isValid;
1127
        }
1128
1129
        if ($this->getReadOnly()) {
1130
            // will be ignored during saveAll()
1131
        } else {
1132
1133
            /**
1134
             * All saveAll() calls are treated as transactional and a transaction
1135
             * will be started if one has not already been on the db connection
1136
             */
1137
1138
            /** @var Connection $db */
1139
            $db = static::getDb();
1140
            $transaction = $db->getTransaction() === null ? $db->beginTransaction() : null;
1141
1142
            $canSaveAll = (!$canSaveAll ? $canSaveAll : $this->beforeSaveAll());
1143
1144
            if ($canSaveAll) {
1145
1146
                if ($runValidation) {
1147
1148
                    if ($this->hasChanges()) {
1149
1150
                        if (!$hasParentModel) {
1151
                            $this->setChildHasChanges('this');
1152
                            $this->setChildOldValues('this', $this->getResetDataForFailedSave());
1153
                        }
1154
1155
                        $canSaveAll = $this->validate($attributes);
1156
                        if (!$canSaveAll) {
1157
                            $errors = $this->getErrors();
1158
                            foreach ($errors as $errorField => $errorDescription) {
1159
                                $this->addActionError($errorDescription, 0, $errorField);
1160
                            }
1161
                        }
1162
                    }
1163
                }
1164
1165
                $this->processModelMap();
1166
                if ($this->modelRelationMap) {
1167
1168
                    foreach ($this->modelRelationMap as $relation => $relationInfo) {
1169
1170
                        if ($relationInfo['onSaveAll'] == self::SAVE_CASCADE && ($this->isRelationPopulated($relation) || ($push && isset($this->$relation)))) {
1171
1172
                            $isReadOnly = ($relationInfo['readOnly'] === null || !$relationInfo['readOnly'] ? false : true);
1173
                            if ($this->$relation instanceof ActiveRecordReadOnlyInterface) {
1174
                                $isReadOnly = $this->$relation->getReadOnly();
1175
                            }
1176
1177
                            if (!$isReadOnly) {
1178
1179
                                $needsCheck = true;
1180
                                $isActiveRecordArray = ($this->$relation instanceof ActiveRecordArray);
1181
1182
                                if (!$isActiveRecordArray) {
1183
                                    if ($this->$relation instanceof ActiveRecordSaveAllInterface) {
1184
                                        $needsCheck = $this->$relation->hasChanges(true);
1185
                                    } elseif (method_exists($this->$relation, 'getDirtyAttributes')) {
1186
                                        if (!$this->$relation->getDirtyAttributes()) {
1187
                                            $needsCheck = false;
1188
                                        }
1189
                                    }
1190
                                }
1191
1192
                                if ($needsCheck) {
1193
                                    $this->setChildHasChanges($relation);
1194
                                    if (!$isActiveRecordArray) {
1195
                                        if ($this->$relation instanceof ActiveRecordSaveAllInterface) {
1196
                                            $this->setChildOldValues($relation, $this->$relation->getResetDataForFailedSave());
1197
                                        } else {
1198
                                            $this->setChildOldValues(
1199
                                                $relation,
1200
                                                array(
1201
                                                    'new' => $this->$relation->getIsNewRecord(),
1202
                                                    'oldValues' => $this->$relation->getOldAttributes(),
1203
                                                    'current' => $this->$relation->getAttributes()
1204
                                                )
1205
                                            );
1206
                                        }
1207
                                    }
1208
1209
                                    $canSaveThis = true;
1210
                                    if ($this->$relation instanceof ActiveRecordSaveAllInterface) {
1211
                                        $canSaveThis = $this->$relation->beforeSaveAllInternal($runValidation, true, $push);
1212
                                        if (!$canSaveThis) {
1213
                                            if (method_exists($this->$relation, 'hasActionErrors')) {
1214
                                                if ($this->$relation->hasActionErrors()) {
1215
                                                    $this->mergeActionErrors($this->$relation->getActionErrors());
1216
                                                }
1217
                                            }
1218
                                        }
1219
                                    } elseif (method_exists($this->$relation, 'validate')) {
1220
                                        $canSaveThis = $this->$relation->validate();
1221
                                        if (!$canSaveThis) {
1222
                                            $errors = $this->$relation->getErrors();
1223
                                            foreach ($errors as $errorField => $errorDescription) {
1224
                                                $this->addActionError($errorDescription, 0, $errorField, Tools::getClassName($this->$relation));
1225
                                            }
1226
                                        }
1227
                                    }
1228
1229
                                    if (!$canSaveThis) {
1230
                                        $canSaveAll = false;
1231
                                    }
1232
1233
                                }
1234
                            }
1235
                        }
1236
                    }
1237
                }
1238
            }
1239
        }
1240
1241
        if ($this->hasActionErrors()) {
1242
            $canSaveAll = false;
1243
        } elseif (!$canSaveAll) {
1244
            $this->addActionError('beforeSaveAllInternal checks failed');
1245
        }
1246
1247
        if (!$canSaveAll) {
1248
            $this->resetChildHasChanges();
1249
            if ($transaction !== null) {
1250
                // cancel the started transaction
1251
                $transaction->rollback();
1252
            }
1253
        } else {
1254
            if ($transaction !== null) {
1255
                $this->setChildOldValues('_transaction_', $transaction);
1256
            }
1257
        }
1258
1259
        return $canSaveAll;
1260
    }
1261
1262
    /**
1263
     * Called by beforeSaveAllInternal on the current model to determine if the whole of saveAll
1264
     * can be processed - this is expected to be replaced in individual models when required
1265
     *
1266
     * @return boolean okay to continue with saveAll
1267
     */
1268
    public function beforeSaveAll()
1269
    {
1270
        return true;
1271
    }
1272
1273
    /**
1274
     * This method is called at the end of a successful saveAll()
1275
     * The default implementation will trigger an [[EVENT_AFTER_SAVE_ALL]] event
1276
     * When overriding this method, make sure you call the parent implementation so that
1277
     * the event is triggered.
1278
     *
1279
     * @param boolean $hasParentModel
1280
     *        whether this method was called from the top level or by a parent
1281
     *        If false, it means the method was called at the top level
1282
     */
1283
    public function afterSaveAllInternal($hasParentModel = false)
1284
    {
1285
        /** @var \yii\db\Transaction $transaction */
1286
        $transaction = $this->getChildOldValues('_transaction_');
1287
        if ($transaction) {
1288
            $transaction->commit();
1289
        }
1290
1291
        if ($this->getReadOnly()) {
1292
            // will be ignored during saveAll()
1293
        } else {
1294
1295
            $this->processModelMap();
1296
            if ($this->modelRelationMap) {
1297
1298
                foreach ($this->modelRelationMap as $relation => $relationInfo) {
1299
1300
                    if ($relationInfo['onSaveAll'] == self::SAVE_CASCADE && ($this->isRelationPopulated($relation))) {
1301
1302
                        if ($this->getChildHasChanges($relation)) {
1303
1304
                            if ($this->$relation instanceof ActiveRecordSaveAllInterface) {
1305
                                $this->$relation->afterSaveAllInternal(true);
1306
                            } elseif ($this->$relation instanceof YiiActiveRecord && method_exists($this->$relation, 'afterSaveAll')) {
1307
                                $this->$relation->afterSaveAll();
1308
                            }
1309
                        }
1310
                    }
1311
                }
1312
            }
1313
1314
            // any model specific actions to carry out
1315
            $this->afterSaveAll();
1316
1317
        }
1318
1319
        $this->resetChildHasChanges();
1320
1321
        if (!$hasParentModel) {
1322
            $this->trigger(self::EVENT_AFTER_SAVE_ALL);
1323
        }
1324
    }
1325
1326
    /**
1327
     * Called by afterSaveAllInternal on the current model once the whole of the saveAll() has
1328
     * been successfully processed
1329
     */
1330
    public function afterSaveAll()
1331
    {
1332
1333
    }
1334
1335
    /**
1336
     * This method is called at the end of a failed saveAll()
1337
     * The default implementation will trigger an [[EVENT_AFTER_SAVE_ALL_FAILED]] event
1338
     * When overriding this method, make sure you call the parent implementation so that
1339
     * the event is triggered.
1340
     *
1341
     * @param boolean $hasParentModel
1342
     *        whether this method was called from the top level or by a parent
1343
     *        If false, it means the method was called at the top level
1344
     */
1345
    public function afterSaveAllFailedInternal($hasParentModel = false)
1346
    {
1347
        /** @var \yii\db\Transaction $transaction */
1348
        $transaction = $this->getChildOldValues('_transaction_');
1349
        if ($transaction) {
1350
            $transaction->rollback();
1351
        }
1352
1353
        if ($this->getReadOnly()) {
1354
            // will be ignored during saveAll()
1355
        } else {
1356
1357
            $this->processModelMap();
1358
            if ($this->modelRelationMap) {
1359
1360
                foreach ($this->modelRelationMap as $relation => $relationInfo) {
1361
1362
                    if ($relationInfo['onSaveAll'] == self::SAVE_CASCADE && ($this->isRelationPopulated($relation))) {
1363
1364
                        if ($this->getChildHasChanges($relation)) {
1365
                            if (!($this->$relation instanceof ActiveRecordArray)) {
1366
                                if ($this->$relation instanceof ActiveRecordSaveAllInterface) {
1367
                                    $this->$relation->resetOnFailedSave($this->getChildOldValues($relation));
1368
                                } elseif ($this->$relation instanceof YiiActiveRecord) {
1369
                                    $this->$relation->setAttributes($this->getChildOldValues($relation, 'current'), false);
1370
                                    $this->$relation->setIsNewRecord($this->getChildOldValues($relation, 'new'));
1371
                                    $tempValue = $this->getChildOldValues($relation, 'oldValues');
1372
                                    $this->$relation->setOldAttributes($tempValue ? $tempValue : null);
1373
                                }
1374
                            }
1375
1376
                            if ($this->$relation instanceof ActiveRecordSaveAllInterface) {
1377
                                $this->$relation->afterSaveAllFailedInternal(true);
1378
                            } elseif ($this->$relation instanceof YiiActiveRecord && method_exists($this->$relation, 'afterSaveAllFailed')) {
1379
                                $this->$relation->afterSaveAllFailed();
1380
                            }
1381
                        }
1382
                    }
1383
                }
1384
            }
1385
1386
            if (!$hasParentModel) {
1387
                if ($this->getChildHasChanges('this')) {
1388
                    $this->resetOnFailedSave($this->getChildOldValues('this'));
1389
                }
1390
            }
1391
1392
            // any model specific actions to carry out
1393
            $this->afterSaveAllFailed();
1394
1395
        }
1396
1397
        $this->resetChildHasChanges();
1398
1399
        if (!$hasParentModel) {
1400
            $this->trigger(self::EVENT_AFTER_SAVE_ALL_FAILED);
1401
        }
1402
    }
1403
1404
    /**
1405
     * Called by afterSaveAllInternal on the current model once saveAll() fails
1406
     */
1407
    public function afterSaveAllFailed()
1408
    {
1409
1410
    }
1411
1412
    /**
1413
     * Obtain data required to reset current record to state before saveAll() was called in the event
1414
     * that saveAll() fails
1415
     * @return array array of data required to rollback the current model
1416
     */
1417
    public function getResetDataForFailedSave()
1418
    {
1419
        return array('new' => $this->getIsNewRecord(), 'oldValues' => $this->getOldAttributes(), 'current' => $this->getAttributes());
1420
    }
1421
1422
    /**
1423
     * Reset current record to state before saveAll() was called in the event
1424
     * that saveAll() fails
1425
     * @param array $data array of data required to rollback the current model
1426
     */
1427
    public function resetOnFailedSave($data)
1428
    {
1429
        $this->setAttributes($data['current'], false);
1430
        $this->setIsNewRecord($data['new']);
1431
        $tempValue = $data['oldValues'];
1432
        $this->setOldAttributes($tempValue ? $tempValue : null);
1433
    }
1434
1435
    /**
1436
     * Delete the current record
1437
     *
1438
     * @see \yii\db\BaseActiveRecord::delete()
1439
     *
1440
     * @param boolean $hasParentModel
1441
     *        whether this method was called from the top level or by a parent
1442
     *        If false, it means the method was called at the top level
1443
     * @param boolean $fromDeleteFull
1444
     *        has the delete() call come from deleteFull() or not
1445
     * @return boolean
1446
     *        did delete() successfully process
1447
     */
1448
    public function delete($hasParentModel = false, $fromDeleteFull = false)
1449
    {
1450
        $ok = true;
1451
        if (!$this->getReadOnly() && $this->getCanDelete()) {
1452
            try {
1453
                $ok = parent::delete();
0 ignored issues
show
Bug Compatibility introduced by
The expression parent::delete(); of type integer|false adds the type integer to the return on line 1469 which is incompatible with the return type declared by the interface fangface\db\ActiveRecordSaveAllInterface::delete of type boolean.
Loading history...
1454
            } catch (\Exception $e) {
1455
                $ok = false;
1456
                $this->addActionError($e->getMessage(), $e->getCode());
1457
            }
1458
            if ($ok && !$fromDeleteFull) {
1459
                $this->deleteWrapUp();
1460
            }
1461
        } elseif (!$hasParentModel) {
1462
            $message = 'Attempting to delete ' . Tools::getClassName($this) . ($this->getReadOnly() ? ' readOnly model' : ' model flagged as not deletable');
1463
            //$this->addActionError($message);
1464
            throw new Exception($message);
1465
        } else {
1466
            $this->addActionWarning('Skipped delete of ' . Tools::getClassName($this) . ' which is ' . ($this->getReadOnly() ? 'read only' : 'flagged as not deletable'));
1467
        }
1468
1469
        return $ok;
1470
    }
1471
1472
    /**
1473
     * reset the current record as much as possible after delete()
1474
     */
1475
    public function deleteWrapUp()
1476
    {
1477
        $attributes = $this->attributes();
1478
        foreach ($attributes as $name) {
1479
            $this->setAttribute($name, null);
1480
        }
1481
        //$this->setOldAttributes(null); // now done in Yii::ActiveRecord::deleteInternal()
1482
        $this->setIsNewRecord(true);
1483
        $this->defaultsApplied = false;
1484
        $this->defaultsAppliedText = false;
1485
        $relations = $this->getRelatedRecords();
1486
        foreach ($relations as $name => $value) {
1487
            $this->__unset($name);
1488
        }
1489
    }
1490
1491
    /**
1492
     * deletes the current record but also loops through defined relationships (if appropriate)
1493
     * to delete those as well
1494
     *
1495
     * @param boolean $hasParentModel
1496
     *        whether this method was called from the top level or by a parent
1497
     *        If false, it means the method was called at the top level
1498
     * @return boolean
1499
     *        did deleteFull() successfully process
1500
     */
1501
    public function deleteFull($hasParentModel = false)
1502
    {
1503
1504
        $this->clearActionErrors();
1505
1506
        if ($this->getIsNewRecord()) {
1507
1508
            // record does not exist yet anyway
1509
            $allOk = true;
1510
1511
        } elseif (!$hasParentModel && ($this->getReadOnly() || !$this->getCanDelete())) {
1512
1513
            // not allowed to amend or delete
1514
            $message = 'Attempting to delete ' . Tools::getClassName($this) . ($this->getReadOnly() ? ' readOnly model' : ' model flagged as not deletable');
1515
            //$this->addActionError($message);
1516
            throw new Exception($message);
1517
1518
        } elseif ($hasParentModel && ($this->getReadOnly() || !$this->getCanDelete())) {
1519
1520
            // not allowed to amend or delete but is a child model so we will treat as okay without deleting the record
1521
            $this->addActionWarning('Skipped delete of ' . Tools::getClassName($this) . ' which is ' . ($this->getReadOnly() ? 'read only' : 'flagged as not deletable'));
1522
            $allOk = true;
1523
1524
        } else {
1525
1526
            if (!$hasParentModel) {
1527
                // run beforeDeleteFull and abandon deleteFull() if it returns false
1528
                if (!$this->beforeDeleteFullInternal($hasParentModel)) {
1529
                    return false;
1530
                }
1531
            }
1532
1533
            try {
1534
1535
                $allOk = true;
1536
1537
                $this->processModelMap();
1538
                if ($this->modelRelationMap) {
1539
1540
                    foreach ($this->modelRelationMap as $relation => $relationInfo) {
1541
1542
                        if ($relationInfo['onDeleteFull'] == self::DELETE_CASCADE) {
1543
1544
                            $isReadOnly = ($relationInfo['readOnly'] === null || !$relationInfo['readOnly'] ? false : true);
1545
                            $canDelete = ($relationInfo['canDelete'] === null || $relationInfo['canDelete'] ? true : false);
1546
                            if ($this->isRelationPopulated($relation)) {
1547
                                if ($this->$relation instanceof ActiveRecordReadOnlyInterface) {
1548
                                    $isReadOnly = $this->$relation->getReadOnly();
1549
                                    $canDelete = $this->$relation->getCanDelete();
1550
                                }
1551
                            }
1552
1553
                            if (!$isReadOnly && $canDelete) {
1554
1555
                                $ok = true;
1556
1557
                                if (isset($this->$relation)) {
1558
1559
                                    if ($this->$relation instanceof ActiveRecordArray) {
1560
1561
                                        $ok = $this->$relation->deleteFull(true);
1562
1563
                                    } else {
1564
1565
                                        $ok = false;
1566
                                        if ($this->$relation instanceof ActiveRecordSaveAllInterface) {
1567
                                            $ok = $this->$relation->deleteFull(true);
1568
                                        } elseif (method_exists($this->$relation, 'delete')) {
1569
                                            $ok = $this->$relation->delete();
1570
                                        }
1571
1572
                                    }
1573
1574
                                    if (method_exists($this->$relation, 'hasActionErrors')) {
1575
                                        if ($this->$relation->hasActionErrors()) {
1576
                                            $this->mergeActionErrors($this->$relation->getActionErrors());
1577
                                        }
1578
                                    }
1579
1580
                                    if (method_exists($this->$relation, 'hasActionWarnings')) {
1581
                                        if ($this->$relation->hasActionWarnings()) {
1582
                                            $this->mergeActionWarnings($this->$relation->getActionWarnings());
1583
                                        }
1584
                                    }
1585
1586
                                }
1587
1588
                                if (!$ok) {
1589
                                    $allOk = false;
1590
                                }
1591
1592
                            } else {
1593
                                $this->addActionWarning('Skipped delete of ' . $relation . ' which is ' . ($isReadOnly ? 'read only' : 'flagged as not deletable'));
1594
                            }
1595
1596
                        }
1597
                    }
1598
1599
                }
1600
1601
                if ($allOk) {
1602
                    $allOk = $this->delete($hasParentModel, true);
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->delete($hasParentModel, true); of type integer|boolean adds the type integer to the return on line 1623 which is incompatible with the return type declared by the interface fangface\db\ActiveRecord...llInterface::deleteFull of type boolean.
Loading history...
1603
                    if ($allOk) {
1604
                        $allOk = true;
1605
                    }
1606
                }
1607
1608
            } catch (\Exception $e) {
1609
                $allOk = false;
1610
                $this->addActionError($e->getMessage(), $e->getCode());
1611
            }
1612
1613
            if (!$hasParentModel) {
1614
                if ($allOk) {
1615
                    $this->afterDeleteFullInternal();
1616
                } else {
1617
                    $this->afterDeleteFullFailedInternal();
1618
                }
1619
            }
1620
1621
        }
1622
1623
        return $allOk;
1624
    }
1625
1626
    /**
1627
     * This method is called at the beginning of a deleteFull() request on a record or model map
1628
     *
1629
     * @param boolean $hasParentModel
1630
     *        whether this method was called from the top level or by a parent
1631
     *        If false, it means the method was called at the top level
1632
     * @return boolean whether the deleteFull() method call should continue
1633
     *        If false, deleteFull() will be cancelled.
1634
     */
1635
    public function beforeDeleteFullInternal($hasParentModel = false)
1636
    {
1637
        $this->clearActionErrors();
1638
        $this->resetChildHasChanges();
1639
        $transaction = null;
1640
1641
        $canDeleteFull = true;
1642
1643
        if (!$hasParentModel) {
1644
            $event = new ModelEvent;
1645
            $this->trigger(self::EVENT_BEFORE_DELETE_FULL, $event);
1646
            $canDeleteFull = $event->isValid;
1647
        }
1648
1649
        if ($this->getIsNewRecord()) {
1650
            // will be ignored during deleteFull()
1651
        } elseif ($this->getReadOnly()) {
1652
            // will be ignored during deleteFull()
1653
        } elseif (!$this->getCanDelete()) {
1654
            // will be ignored during deleteFull()
1655
        } else {
1656
1657
            /**
1658
             * All deleteFull() calls are treated as transactional and a transaction
1659
             * will be started if one has not already been on the db connection
1660
             */
1661
1662
            /** @var Connection $db */
1663
            $db = static::getDb();
1664
            $transaction = $db->getTransaction() === null ? $db->beginTransaction() : null;
1665
1666
            $canDeleteFull = (!$canDeleteFull ? $canDeleteFull : $this->beforeDeleteFull());
1667
1668
            if ($canDeleteFull) {
1669
1670
                if (!$hasParentModel) {
1671
                    $this->setChildHasChanges('this');
1672
                    $this->setChildOldValues('this', $this->getResetDataForFailedSave());
1673
                }
1674
1675
                $this->processModelMap();
1676
                if ($this->modelRelationMap) {
1677
1678
                    foreach ($this->modelRelationMap as $relation => $relationInfo) {
1679
1680
                        if ($relationInfo['onDeleteFull'] == self::DELETE_CASCADE) {
1681
1682
                            $isReadOnly = ($relationInfo['readOnly'] === null || !$relationInfo['readOnly'] ? false : true);
1683
                            $canDelete = ($relationInfo['canDelete'] === null || $relationInfo['canDelete'] ? true : false);
1684
                            if ($this->isRelationPopulated($relation)) {
1685
                                if ($this->$relation instanceof ActiveRecordReadOnlyInterface) {
1686
                                    $isReadOnly = $this->$relation->getReadOnly();
1687
                                    $canDelete = $this->$relation->getCanDelete();
1688
                                }
1689
                            }
1690
1691
                            if (!$isReadOnly && $canDelete) {
1692
1693
                                if (isset($this->$relation)) {
1694
1695
                                    $isActiveRecordArray = ($this->$relation instanceof ActiveRecordArray);
1696
1697
                                    $this->setChildHasChanges($relation);
1698
                                    if (!$isActiveRecordArray) {
1699
                                        if ($this->$relation instanceof ActiveRecordSaveAllInterface) {
1700
                                            $this->setChildOldValues($relation, $this->$relation->getResetDataForFailedSave());
1701
                                        } else {
1702
                                            $this->setChildOldValues(
1703
                                                $relation,
1704
                                                array(
1705
                                                    'new' => $this->$relation->getIsNewRecord(),
1706
                                                    'oldValues' => $this->$relation->getOldAttributes(),
1707
                                                    'current' => $this->$relation->getAttributes()
1708
                                                )
1709
                                            );
1710
                                        }
1711
                                    }
1712
1713
                                    $canDeleteThis = true;
1714
                                    if ($this->$relation instanceof ActiveRecordSaveAllInterface) {
1715
                                        $canDeleteThis = $this->$relation->beforeDeleteFullInternal(true);
1716
                                        if (!$canDeleteThis) {
1717
                                            if (method_exists($this->$relation, 'hasActionErrors')) {
1718
                                                if ($this->$relation->hasActionErrors()) {
1719
                                                    $this->mergeActionErrors($this->$relation->getActionErrors());
1720
                                                }
1721
                                            }
1722
                                        }
1723
                                    } elseif (method_exists($this->$relation, 'beforeDeleteFull')) {
1724
                                        $canDeleteThis = $this->$relation->beforeDeleteFull();
1725
                                        if (!$canDeleteThis) {
1726
                                            $errors = $this->$relation->getErrors();
1727
                                            foreach ($errors as $errorField => $errorDescription) {
1728
                                                $this->addActionError($errorDescription, 0, $errorField, Tools::getClassName($this->$relation));
1729
                                            }
1730
                                        }
1731
                                    }
1732
1733
                                    if (!$canDeleteThis) {
1734
                                        $canDeleteFull = false;
1735
                                    }
1736
1737
                                }
1738
                            }
1739
                        }
1740
                    }
1741
                }
1742
            }
1743
        }
1744
1745
        if ($this->hasActionErrors()) {
1746
            $canDeleteFull = false;
1747
        } elseif (!$canDeleteFull) {
1748
            $this->addActionError('beforeDeleteFullInternal checks failed');
1749
        }
1750
1751
        if (!$canDeleteFull) {
1752
            $this->resetChildHasChanges();
1753
            if ($transaction !== null) {
1754
                // cancel the started transaction
1755
                $transaction->rollback();
1756
            }
1757
        } else {
1758
            if ($transaction !== null) {
1759
                $this->setChildOldValues('_transaction_', $transaction);
1760
            }
1761
        }
1762
1763
        return $canDeleteFull;
1764
    }
1765
1766
    /**
1767
     * Called by beforeDeleteFullInternal on the current model to determine if the whole of deleteFull
1768
     * can be processed - this is expected to be replaced in individual models when required
1769
     *
1770
     * @return boolean okay to continue with deleteFull
1771
     */
1772
    public function beforeDeleteFull()
1773
    {
1774
        return true;
1775
    }
1776
1777
    /**
1778
     * This method is called at the end of a successful deleteFull()
1779
     *
1780
     * @param boolean $hasParentModel
1781
     *        whether this method was called from the top level or by a parent
1782
     *        If false, it means the method was called at the top level
1783
     */
1784
    public function afterDeleteFullInternal($hasParentModel = false)
1785
    {
1786
        /** @var \yii\db\Transaction $transaction */
1787
        $transaction = $this->getChildOldValues('_transaction_');
1788
        if ($transaction) {
1789
            $transaction->commit();
1790
        }
1791
1792
        if ($this->getIsNewRecord()) {
1793
            // will have been ignored during deleteFull()
1794
        } elseif ($this->getReadOnly()) {
1795
            // will have been ignored during deleteFull()
1796
        } elseif (!$this->getCanDelete()) {
1797
            // will have been ignored during deleteFull()
1798
        } else {
1799
1800
            $this->processModelMap();
1801
            if ($this->modelRelationMap) {
1802
                foreach ($this->modelRelationMap as $relation => $relationInfo) {
1803
                    if ($relationInfo['onDeleteFull'] == self::DELETE_CASCADE) {
1804
                        if ($this->getChildHasChanges($relation)) {
1805
                            if ($this->$relation instanceof ActiveRecordSaveAllInterface) {
1806
                                $this->$relation->afterDeleteFullInternal(true);
1807
                            } elseif ($this->$relation instanceof YiiActiveRecord && method_exists($this->$relation, 'afterDeleteFull')) {
1808
                                $this->$relation->afterDeleteFull();
1809
                            }
1810
                            $this->__unset($relation);
1811
                        }
1812
                    }
1813
                }
1814
            }
1815
1816
            if (!$hasParentModel) {
1817
                $this->deleteWrapUp();
1818
            }
1819
1820
            // any model specific actions to carry out
1821
            $this->afterDeleteFull();
1822
1823
        }
1824
1825
        $this->resetChildHasChanges();
1826
1827
        if (!$hasParentModel) {
1828
            $this->trigger(self::EVENT_AFTER_DELETE_FULL);
1829
        }
1830
    }
1831
1832
    /**
1833
     * Called by afterDeleteFullInternal on the current model once the whole of the deleteFull() has
1834
     * been successfully processed
1835
     */
1836
    public function afterDeleteFull()
1837
    {
1838
1839
    }
1840
1841
    /**
1842
     * This method is called at the end of a failed deleteFull()
1843
     *
1844
     * @param boolean $hasParentModel
1845
     *        whether this method was called from the top level or by a parent
1846
     *        If false, it means the method was called at the top level
1847
     */
1848
    public function afterDeleteFullFailedInternal($hasParentModel = false)
1849
    {
1850
        /** @var \yii\db\Transaction $transaction */
1851
        $transaction = $this->getChildOldValues('_transaction_');
1852
        if ($transaction) {
1853
            $transaction->rollback();
1854
        }
1855
1856
        if ($this->getIsNewRecord()) {
1857
            // will have been ignored during deleteFull()
1858
        } elseif ($this->getReadOnly()) {
1859
            // will have been ignored during deleteFull()
1860
        } elseif (!$this->getCanDelete()) {
1861
            // will have been ignored during deleteFull()
1862
        } else {
1863
1864
            $this->processModelMap();
1865
            if ($this->modelRelationMap) {
1866
1867
                foreach ($this->modelRelationMap as $relation => $relationInfo) {
1868
                    if ($relationInfo['onDeleteFull'] == self::DELETE_CASCADE) {
1869
1870
                        if ($this->getChildHasChanges($relation)) {
1871
                            if (!($this->$relation instanceof ActiveRecordArray)) {
1872
                                if ($this->$relation instanceof ActiveRecordSaveAllInterface) {
1873
                                    $this->$relation->resetOnFailedSave($this->getChildOldValues($relation));
1874
                                } elseif ($this->$relation instanceof YiiActiveRecord) {
1875
                                    $this->$relation->setAttributes($this->getChildOldValues($relation, 'current'), false);
1876
                                    $this->$relation->setIsNewRecord($this->getChildOldValues($relation, 'new'));
1877
                                    $tempValue = $this->getChildOldValues($relation, 'oldValues');
1878
                                    $this->$relation->setOldAttributes($tempValue ? $tempValue : null);
1879
                                }
1880
                            }
1881
1882
                            if ($this->$relation instanceof ActiveRecordSaveAllInterface) {
1883
                                $this->$relation->afterDeleteFullFailedInternal(true);
1884
                            } elseif ($this->$relation instanceof YiiActiveRecord && method_exists($this->$relation, 'afterDeleteFullFailed')) {
1885
                                $this->$relation->afterDeleteFullFailed();
1886
                            }
1887
                        }
1888
1889
                    }
1890
                }
1891
            }
1892
1893
            if (!$hasParentModel) {
1894
                if ($this->getChildHasChanges('this')) {
1895
                    $this->resetOnFailedSave($this->getChildOldValues('this'));
1896
                }
1897
            }
1898
1899
            // any model specific actions to carry out
1900
            $this->afterDeleteFullFailed();
1901
1902
        }
1903
1904
        $this->resetChildHasChanges();
1905
1906
        if (!$hasParentModel) {
1907
            $this->trigger(self::EVENT_AFTER_DELETE_FULL_FAILED);
1908
        }
1909
    }
1910
1911
    /**
1912
     * Called by afterDeleteFullFailedInternal on the current model once deleteFull() has
1913
     * failed processing
1914
     */
1915
    public function afterDeleteFullFailed()
1916
    {
1917
1918
    }
1919
1920
    /**
1921
     * Obtain list of fields that need to have string lengths checked as part of beforeSave()
1922
     * @return array
1923
     */
1924
    public function beforeSaveStringFields()
1925
    {
1926
        return [];
1927
    }
1928
1929
    /**
1930
     * Obtain the model relation map array (this function should be overwritten inside AR classes)
1931
     * @return array
1932
     */
1933
    public function modelRelationMap()
1934
    {
1935
        return [];
1936
    }
1937
1938
    /**
1939
     * Process model map to ensure all missing values have their defaults applied
1940
     * (saves on isset() checking attributes when ever the model map is used
1941
     */
1942
    public function processModelMap()
1943
    {
1944
        if ($this->modelRelationMap) {
1945
            // already processed
1946
        } else {
1947
            $this->modelRelationMap = $this->modelRelationMap();
1948
            if ($this->modelRelationMap) {
1949
                foreach ($this->modelRelationMap as $relation => $relationInfo) {
1950
                    $this->modelRelationMap[$relation] = array_merge(
1951
                        array(
1952
                            'type' => 'hasOne',
1953
                            'class' => '',
1954
                            'link' => array(),
1955
                            'config' => array(),
1956
                            'onSaveAll' => self::SAVE_NO_ACTION,
1957
                            'onDeleteFull' => self::DELETE_NO_ACTION,
1958
                            'autoLinkType' => self::LINK_NONE,
1959
                            'autoLink' => array(),
1960
                            'allToArray' => false,
1961
                            'skipNullLinkCheck' => false,
1962
                            'readOnly' => null,
1963
                            'canDelete' => null,
1964
                            'activeAttributesInParent' => false,
1965
                    ),
1966
                        $relationInfo
1967
                    );
1968
                    // for now we do not want to cascade save or delete on belongsTo relations
1969
                    if ($this->modelRelationMap[$relation]['type'] == 'belongsTo' && $this->modelRelationMap[$relation]['onSaveAll'] != self::SAVE_NO_ACTION) {
1970
                        $this->modelRelationMap[$relation]['onSaveAll'] = self::SAVE_NO_ACTION;
1971
                    }
1972
                    if ($this->modelRelationMap[$relation]['type'] == 'belongsTo' && $this->modelRelationMap[$relation]['onDeleteFull'] != self::DELETE_NO_ACTION) {
1973
                        $this->modelRelationMap[$relation]['onDeleteFull'] = self::DELETE_NO_ACTION;
1974
                    }
1975
                }
1976
            }
1977
        }
1978
    }
1979
1980
    /**
1981
     * Check if the model attribute name is a defined relation
1982
     * @param string $name
1983
     * @return boolean
1984
     */
1985
    public function isDefinedRelation($name) {
1986
        $this->processModelMap();
1987
        if ($this->modelRelationMap && array_key_exists($name, $this->modelRelationMap)) {
1988
            if (is_array($this->modelRelationMap[$name]) && $this->modelRelationMap[$name]) {
1989
                // relation is included in the defined array along with some setup information
1990
                return true;
1991
            }
1992
        }
1993
        return false;
1994
    }
1995
1996
    /**
1997
     * Get defined relation info by relation name or return false if the name is not a defined relation
1998
     * @param string $name
1999
     * @param string|false $key
2000
     * @return array|string|false
2001
     */
2002
    public function getDefinedRelationInfo($name, $key = false)
2003
    {
2004
        $this->processModelMap();
2005
        if ($this->modelRelationMap && array_key_exists($name, $this->modelRelationMap)) {
2006
            if ($key) {
2007
                if (is_array($this->modelRelationMap[$name]) && array_key_exists($key, $this->modelRelationMap[$name])) {
2008
                    return $this->modelRelationMap[$name][$key];
2009
                }
2010
            } else {
2011
                // return defined relation with default values for anything that is missing
2012
                return $this->modelRelationMap[$name];
2013
            }
2014
        }
2015
        return false;
2016
    }
2017
2018
    /**
2019
     * (non-PHPdoc)
2020
     * @see \yii\base\Model::toArray()
2021
     */
2022
    public function toArray(array $fields = [], array $expand = [], $recursive = true)
2023
    {
2024
        if ($this->getIsNewRecord() && $this->applyDefaults && !$this->defaultsApplied) {
2025
            $this->applyDefaults(false);
2026
        }
2027
        $recursive = false;
2028
        return parent::toArray($fields, $expand, $recursive);
2029
    }
2030
2031
    /**
2032
     * Return whole active record model including relationships as an array
2033
     *
2034
     * @param boolean $loadedOnly [OPTIONAL] only show populated relations default is false
2035
     * @param boolean $excludeNewAndBlankRelations [OPTIONAL] exclude new blank records, default true
2036
     * @return array
2037
     */
2038
    public function allToArray($loadedOnly = false, $excludeNewAndBlankRelations = true) {
2039
2040
        $data = $this->toArray();
2041
2042
        $this->processModelMap();
2043
        if ($this->modelRelationMap) {
2044
2045
            foreach ($this->modelRelationMap as $relationName => $relationInfo) {
2046
2047
                if ($relationInfo['allToArray']) {
2048
2049
                    switch ($relationInfo['type'])
2050
                    {
2051
                        case 'hasMany':
2052
                            if (($loadedOnly && $this->isRelationPopulated($relationName)) || (!$loadedOnly && isset($this->$relationName))) {
2053
2054
                                if ($this->$relationName instanceof ActiveRecordArray) {
2055
                                    $data[$relationName] = $this->$relationName->allToArray($loadedOnly, $excludeNewAndBlankRelations);
2056
                                } else {
2057
                                    $data[$relationName] = array();
2058
                                    foreach ($this->$relationName as $key => $value) {
2059
2060
                                        $excludeFromArray = false;
2061
                                        if ($excludeNewAndBlankRelations && $value->getIsNewRecord()) {
2062
                                            if ($value instanceof ActiveRecordSaveAllInterface) {
2063
                                                if (!$value->hasChanges(true)) {
2064
                                                    $excludeFromArray = true;
2065
                                                }
2066
                                            } elseif (method_exists($value, 'getDirtyAttributes')) {
2067
                                                if (!$value->getDirtyAttributes()) {
2068
                                                    $excludeFromArray = true;
2069
                                                }
2070
                                            }
2071
                                        }
2072
2073
                                        if ($excludeFromArray) {
2074
                                            // exclude
2075
                                        } elseif (method_exists($value, 'allToArray')) {
2076
                                            $data[$relationName][$key] = $value->allToArray($loadedOnly, $excludeNewAndBlankRelations);
2077
                                        } elseif (method_exists($value, 'toArray')) {
2078
                                            $data[$relationName][$key] = $value->toArray(array(), array(), false);
2079
                                        }
2080
                                    }
2081
                                }
2082
2083
                            }
2084
                            break;
2085
2086
                        case 'hasOne':
2087
                        case 'hasEav':
2088
                        case '__belongsTo': // disabled (can enable but be careful of recursion)
2089
2090
                            if (($loadedOnly && $this->isRelationPopulated($relationName)) || (!$loadedOnly && isset($this->$relationName))) {
2091
2092
                                if ($this->$relationName instanceof $this && $this->$relationName->getIsNewRecord()) {
2093
                                    // would lead to infinite recursion
2094
                                } else {
2095
2096
                                    $excludeFromArray = false;
2097
                                    if ($excludeNewAndBlankRelations && $this->$relationName->getIsNewRecord()) {
2098
                                        if (true && $relationInfo['type'] === 'hasEav') {
2099
                                            // typically appear like a new record when first loaded until attributes are accessed or output
2100
                                        } elseif ($this->$relationName instanceof ActiveRecordSaveAllInterface) {
2101
                                            if (!$this->$relationName->hasChanges(true)) {
2102
                                                $excludeFromArray = true;
2103
                                            }
2104
                                        } elseif (method_exists($this->$relationName, 'getDirtyAttributes')) {
2105
                                            if (!$this->$relationName->getDirtyAttributes()) {
2106
                                                $excludeFromArray = true;
2107
                                            }
2108
                                        }
2109
                                    }
2110
2111
                                    if ($excludeFromArray) {
2112
                                        // exclude
2113
                                    } elseif (method_exists($this->$relationName, 'allToArray')) {
2114
                                        $data[$relationName] = $this->$relationName->allToArray($loadedOnly, $excludeNewAndBlankRelations);
2115
                                    } elseif (method_exists($this->$relationName, 'toArray')) {
2116
                                        $data[$relationName] = $this->$relationName->toArray();
2117
                                    }
2118
                                }
2119
2120
                            }
2121
                            break;
2122
2123
                        default:
2124
2125
                            // exclude from allToArray
2126
                            break;
2127
                    }
2128
                }
2129
            }
2130
        }
2131
2132
        return $data;
2133
    }
2134
2135
    /**
2136
     * Automatically establish the relationship if defined in the $modelRelationMap array
2137
     *
2138
     * @param string $name
2139
     * @param boolean $new Is this a new record
2140
     * @prarm boolean $fromGetCall Has request come from a get request via __call()
2141
     * @return mixed NULL
2142
     */
2143
    public function getDefinedRelationship($name, $new = false, $fromGetCall = false)
2144
    {
2145
        if ($this->isDefinedRelation($name)) {
2146
2147
            $relationInfo = $this->getDefinedRelationInfo($name);
2148
2149
            if ($relationInfo) {
2150
2151
                if ($relationInfo['class'] && $relationInfo['link']) {
2152
2153
                    if ($new && !$fromGetCall) {
2154
2155
                        switch ($relationInfo['autoLinkType']) {
2156
2157
                            case self::LINK_ONLY:
2158
                            case self::LINK_FROM_PARENT:
2159
                            case self::LINK_FROM_CHILD:
2160
                            case self::LINK_BI_DIRECT:
2161
                            case self::LINK_FROM_PARENT_MAINT:
2162
                            case self::LINK_FROM_CHILD_MAINT:
2163
                            case self::LINK_BI_DIRECT_MAINT:
2164
                            case self::LINK_BI_DIRECT_MAINT_FROM_PARENT:
2165
                            case self::LINK_BI_DIRECT_MAINT_FROM_CHILD:
2166
2167
                                if (class_exists($relationInfo['class'])) {
2168
2169
                                    switch ($relationInfo['type']) {
2170
                                        case 'hasOne':
2171
                                        case 'hasEav':
2172
2173
                                            if ($relationInfo['config']) {
2174
                                                $value = new $relationInfo['class']($relationInfo['config']);
2175
                                            } else {
2176
                                                $value = new $relationInfo['class']();
2177
                                            }
2178
                                            return $value;
2179
2180
                                        case 'hasMany':
2181
2182
                                            $value = new ActiveRecordArray();
2183
                                            return $value;
2184
2185
                                        default:
2186
2187
                                            // we don't want to extend any others
2188
                                    }
2189
2190
                                }
2191
2192
                                break;
2193
2194
                            default:
2195
                        }
2196
                    } else {
2197
2198
                        $canLoad = ($fromGetCall ? true : $this->getNullLinkCheckOk($name, $relationInfo));
2199
2200
                        if ($canLoad) {
2201
2202
                            $config = array();
2203
                            $config['class'] = $relationInfo['class'];
2204
                            $config['link'] = $relationInfo['link'];
2205
                            if ($relationInfo['config']) {
2206
                                $config['config'] = $relationInfo['config'];
2207
                            }
2208
2209
                            $relationType = ($relationInfo['type'] == 'belongsTo' ? 'hasOne' : $relationInfo['type']);
2210
2211
                            $value = call_user_func_array(array(
2212
                                $this,
2213
                                $relationType
2214
                            ), $config);
2215
2216
                            return $value;
2217
                        }
2218
                    }
2219
                }
2220
            }
2221
        }
2222
2223
        return null;
2224
    }
2225
2226
    /**
2227
     * @param string $relationName
2228
     */
2229
    public function getNullLinkCheckOk($relationName, $relationInfo = null)
2230
    {
2231
        $success = true;
2232
        if (is_null($relationInfo)) {
2233
            if ($this->isDefinedRelation($relationName)) {
2234
                $relationInfo = $this->getDefinedRelationInfo($relationName);
2235
            } else {
2236
                $success = false;
2237
            }
2238
        }
2239
2240
        if ($success) {
2241
2242
            if ($relationInfo['link'] && !$relationInfo['skipNullLinkCheck']) {
2243
2244
                foreach ($relationInfo['link'] as $remoteAttr => $localAttr) {
2245
                    if (!$this->hasAttribute($localAttr)) {
2246
                        $success = false;
2247
                    } else {
2248
                        $attrValue = $this->getAttribute($localAttr);
2249
                        if (is_numeric($attrValue) && $attrValue > 0) {
2250
                        } elseif (is_string($attrValue) && $attrValue != '') {
2251
                        } else {
2252
                            $success = false;
2253
                        }
2254
                    }
2255
                    if (!$success) {
2256
                        break;
2257
                    }
2258
                }
2259
2260
            }
2261
2262
        }
2263
2264
        return $success;
2265
    }
2266
2267
    /**
2268
     * Declares a `has-eav` relation.
2269
     * @param string $class the class name of the related record (must not be 'attributes')
2270
     * @param array $link the primary-foreign key constraint. The keys of the array refer to
2271
     * the attributes of the record associated with the `$class` model, while the values of the
2272
     * array refer to the corresponding attributes in **this** AR class.
2273
     * @param array $config [OPTIONAL] array of config paramaters
2274
     * @return ActiveAttributeRecord
2275
     */
2276
    public function hasEav($class, $link, $config=array())
2277
    {
2278
        if (!is_array($config)) {
2279
            if ($config && is_numeric($config)) {
2280
                $entityId = $config;
2281
                $config = array();
2282
                $config['entityId'] = $entityId;
2283
            } else {
2284
                $config = array();
2285
            }
2286
        }
2287
2288
        $config['link'] = $link;
2289
        $config['parentModel'] = $this;
2290
2291
        return new $class($config);
2292
    }
2293
2294
    /**
2295
     * Declares a `has-many` relation.
2296
     * @param string $class the class name of the related record
2297
     * @param array $link the primary-foreign key constraint. The keys of the array refer to
2298
     * the attributes of the record associated with the `$class` model, while the values of the
2299
     * array refer to the corresponding attributes in **this** AR class.
2300
     * adjust the result to use the primary key as the array key
2301
     * @return ActiveQueryInterface the relational query object.
2302
     */
2303
    public function hasMany($class, $link)
2304
    {
2305
        /* @var $class ActiveRecordInterface */
2306
        /* @var $query ActiveQuery */
2307
        $query = parent::hasMany($class, $link);
2308
        if ($query->multiple) {
2309
            $keys = $class::primaryKey();
2310
            if (count($keys) === 1) {
2311
                $query->indexBy = $keys[0];
2312
            } elseif (count($keys) === 2) {
2313
                $query->indexBy = $keys[1];
2314
            }
2315
        }
2316
        return $query;
2317
    }
2318
2319
    /**
2320
     * PHP setter magic method.
2321
     * This method is overridden so that AR attributes can be accessed like properties,
2322
     * but only if the current model is not read only
2323
     * @param string $name property name
2324
     * @param mixed $value property value
2325
     * @throws Exception if the current record is read only
2326
     */
2327
    public function __set($name, $value)
2328
    {
2329
        if ($this->getReadOnly()) {
2330
            throw new Exception('Attempting to set attribute `' . $name . '` on a read only ' . Tools::getClassName($this) . ' model');
2331
        }
2332
        parent::__set($name, $value);
2333
    }
2334
2335
    /**
2336
     * PHP getter magic method. Override \yii\db\ActiveRecord so that we can automatically
2337
     * setup any defined relationships if the method to set them up does not exist
2338
     * as well as calling the method instead if it does exist (so we can push ActiveQueryInterface->multiple
2339
     * values into ActiveRecordArray()
2340
     * @param string $name property name
2341
     * @return mixed property value
2342
     * @see getAttribute()
2343
     */
2344
    public function __get($name)
2345
    {
2346
2347
        if (!$this->isRelationPopulated($name) && $this->isDefinedRelation($name)) {
2348
2349
            if ($this->getIsNewRecord()) {
2350
2351
                // quite possible the new sub record is also new - let's check
2352
                // to see if we can support the auto creation of the empty sub record
2353
                $value = $this->getDefinedRelationship($name, true);
2354
2355
            } else {
2356
2357
                if (method_exists($this, 'get' . $name)) {
2358
2359
                    $method = new \ReflectionMethod($this, 'get' . $name);
2360
                    $realName = lcfirst(substr($method->getName(), 3));
2361
                    if ($realName !== $name) {
2362
                        throw new \yii\base\InvalidParamException('Relation names are case sensitive. ' . get_class($this) . " has a relation named \"$realName\" instead of \"$name\".");
2363
                    }
2364
2365
                    $value = call_user_func(array($this, 'get' . $name));
2366
2367
                } else {
2368
2369
                    // we will automatically apply this relation now
2370
                    $value = $this->getDefinedRelationship($name);
2371
2372
                }
2373
2374
            }
2375
2376
            if ($value !== null) {
2377
2378
                if ($value instanceof ActiveQueryInterface) {
2379
                    if ($value->multiple) {
2380
                        // put result into a special ArrayObject extended object
2381
                        $value2 = new ActiveRecordArray($value->all());
2382
                    } else {
2383
                        $value2 = $value->one();
2384
                        if (is_null($value2) && !$this->getIsNewRecord()) {
2385
                            // relational record does not exist yet so we will create an empty object now allowing user to start to populate values
2386
                            $value2 = $this->getDefinedRelationship($name, true);
2387
                        }
2388
                    }
2389
                    if ($value2 instanceof ActiveRecordArray) {
2390
                        $value2->setDefaultObjectClass($this->getDefinedRelationInfo($name, 'class'));
2391
                    }
2392
                    if ($value2 instanceof ActiveRecordParentalInterface) {
2393
                        $value2->setParentModel($this);
2394
                    }
2395
                    if ($value2 instanceof ActiveRecordReadOnlyInterface) {
2396
                        $readOnly = $this->getDefinedRelationInfo($name, 'readOnly');
2397
                        if ($readOnly !== null) {
2398
                            $value2->setReadOnly($readOnly);
2399
                        }
2400
                        $canDelete = $this->getDefinedRelationInfo($name, 'canDelete');
2401
                        if ($canDelete !== null) {
2402
                            $value2->setCanDelete($canDelete);
2403
                        }
2404
                    }
2405
                    $this->populateRelation($name, $value2);
2406
                    return $value2;
2407
2408
                } elseif ($value instanceof ActiveRecordParentalInterface) {
2409
2410
                    $value->setParentModel($this);
2411
                    if ($value instanceof ActiveRecordArray) {
2412
                        $defaultClass = $this->getDefinedRelationInfo($name, 'class');
2413
                        if ($defaultClass) {
2414
                            $value->setDefaultObjectClass($defaultClass);
0 ignored issues
show
Bug introduced by
It seems like $defaultClass defined by $this->getDefinedRelationInfo($name, 'class') on line 2412 can also be of type array; however, fangface\db\ActiveRecord...setDefaultObjectClass() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
2415
                        }
2416
                    }
2417
                    if ($value instanceof ActiveRecordReadOnlyInterface) {
2418
                        $readOnly = $this->getDefinedRelationInfo($name, 'readOnly');
2419
                        if ($readOnly !== null) {
2420
                            $value->setReadOnly($readOnly);
2421
                        }
2422
                        $canDelete = $this->getDefinedRelationInfo($name, 'canDelete');
2423
                        if ($canDelete !== null) {
2424
                            $value->setCanDelete($canDelete);
2425
                        }
2426
                    }
2427
                    $this->populateRelation($name, $value);
2428
2429
                } elseif ($value instanceof YiiActiveRecord) {
2430
2431
                    $this->populateRelation($name, $value);
2432
2433
                }
2434
2435
                return $value;
2436
2437
            }
2438
2439
        }
2440
2441
        return parent::__get($name);
2442
    }
2443
2444
    /**
2445
     * PHP magic method to handle calls to functions that do not exist
2446
     * Initially here to allow successful self::getRelation() call when using the model
2447
     * map array but also used to allow AR variables to get set/get via a set/get call
2448
     * if the method does not already exist
2449
     * @see \yii\base\Component::__call()
2450
     */
2451
    public function __call($methodName, $args) {
2452
        if (preg_match('~^(set|get)(.*)$~', $methodName, $matches)) {
2453
            $name = lcfirst($matches[2]);
2454
            if ($this->isDefinedRelation($name)) {
2455
                if ($matches[1] == 'get') {
2456
                    return $this->getDefinedRelationship($name, true, true);
2457
                } else {
2458
                    throw new UnknownMethodException('Calling unknown method: ' . get_class($this) . "::$methodName()");
2459
                }
2460
            } else {
2461
2462
                $allow = false;
2463
                if ($this->hasAttribute($name)) {
2464
                    $allow = true;
2465
                } else {
2466
                    $name = ucfirst($name);
2467
                    if ($this->hasAttribute($name)) {
2468
                        $allow = true;
2469
                    }
2470
                }
2471
2472
                if ($allow) {
2473
                    switch($matches[1]) {
2474
                        case 'set':
2475
                            $this->checkArguments($args, 1, 1, $methodName);
2476
                            $this->setAttribute($name, $args[0]);
2477
                            return $this;
2478
                        case 'get':
2479
                            $this->checkArguments($args, 0, 0, $methodName);
2480
                            return $this->getAttribute($name);
2481
                    }
2482
                }
2483
            }
2484
        }
2485
        return parent::__call($methodName, $args);
2486
    }
2487
2488
    /**
2489
     * Quickly validate method argument count matches expectations else throw exception
2490
     *
2491
     * @param array $args
2492
     * @param integer $min
2493
     * @param integer $max
2494
     * @param string $methodName
2495
     * @throws InvalidParamException
2496
     */
2497
    protected function checkArguments(array $args, $min, $max, $methodName) {
2498
        $argc = count($args);
2499
        if ($argc < $min || $argc > $max) {
2500
            throw new InvalidParamException('Method ' . $methodName . ' needs min ' . $min . ' and max ' . $max . ' arguments. ' . $argc . ' arguments given.');
2501
        }
2502
    }
2503
2504
    /**
2505
     * @inheritdoc
2506
     */
2507
    public function __clone()
2508
    {
2509
        parent::__clone();
2510
        $attributes = $this->getAttributes();
2511
        $this->deleteWrapUp(); // helps set up the new active record again
2512
        $this->setOldAttributes(null);
2513
        foreach ($attributes as $name => $value) {
2514
            $this->setAttribute($name, $value);
2515
        }
2516
        $keys = self::primaryKey();
2517
        $keysCount = count($keys);
2518
        if ($keysCount == 1) {
2519
            $this->setAttribute($keys[0], null);
2520
        }
2521
    }
2522
2523
    /**
2524
     * Call the debugTest method on all objects in the model map (used for testing)
2525
     *
2526
     * @param boolean $loadedOnly [OPTIONAL] only include populated relations default is false
2527
     * @param boolean $excludeNewAndBlankRelations [OPTIONAL] exclude new blank records, default true
2528
     * @return array
2529
     */
2530
    public function callDebugTestOnAll($loadedOnly=false, $excludeNewAndBlankRelations = true) {
2531
2532
        $data = $this->debugTest();
2533
2534
        $this->processModelMap();
2535
        if ($this->modelRelationMap) {
2536
2537
            foreach ($this->modelRelationMap as $relationName => $relationInfo) {
2538
2539
                if ($relationInfo['allToArray']) {
2540
2541
                    switch ($relationInfo['type'])
2542
                    {
2543
                        case 'hasMany':
2544
2545
                            if (($loadedOnly && $this->isRelationPopulated($relationName)) || (!$loadedOnly && isset($this->$relationName))) {
2546
2547
                                if ($this->$relationName instanceof ActiveRecordArray) {
2548
                                    $data[$relationName] = $this->$relationName->callDebugTestOnAll($loadedOnly, $excludeNewAndBlankRelations);
2549
                                } else {
2550
                                    $data[$relationName] = array();
2551
                                    foreach ($this->$relationName as $key => $value) {
2552
2553
                                        $excludeFromArray = false;
2554
                                        if ($excludeNewAndBlankRelations && $value->getIsNewRecord()) {
2555
                                            if ($value instanceof ActiveRecordSaveAllInterface) {
2556
                                                if (!$value->hasChanges(true)) {
2557
                                                    $excludeFromArray = true;
2558
                                                }
2559
                                            } elseif (method_exists($value, 'getDirtyAttributes')) {
2560
                                                if (!$value->getDirtyAttributes()) {
2561
                                                    $excludeFromArray = true;
2562
                                                }
2563
                                            }
2564
                                        }
2565
2566
                                        if ($excludeFromArray) {
2567
                                            // exclude
2568
                                        } elseif (method_exists($value, 'callDebugTestOnAll')) {
2569
                                            $data[$relationName][$key] = $value->callDebugTestOnAll($loadedOnly, $excludeNewAndBlankRelations);
2570
                                        }
2571
                                    }
2572
                                }
2573
2574
                            }
2575
                            break;
2576
2577
                        case 'hasOne':
2578
                        case 'hasEav':
2579
                        case '__belongsTo': // disabled (can enable but be careful of recursion)
2580
2581
                            if (($loadedOnly && $this->isRelationPopulated($relationName)) || (!$loadedOnly && isset($this->$relationName))) {
2582
2583
                                if ($this->$relationName instanceof $this && $this->$relationName->getIsNewRecord()) {
2584
                                    // would lead to infinite recursion
2585
                                } else {
2586
2587
                                    $excludeFromArray = false;
2588
                                    if ($excludeNewAndBlankRelations && $this->$relationName->getIsNewRecord()) {
2589
                                        if (true && $relationInfo['type'] === 'hasEav') {
2590
                                            // typically appear like a new record when first loaded until attributes are accessed or output
2591
                                        } elseif ($this->$relationName instanceof ActiveRecordSaveAllInterface) {
2592
                                            if (!$this->$relationName->hasChanges(true)) {
2593
                                                $excludeFromArray = true;
2594
                                            }
2595
                                        } elseif (method_exists($this->$relationName, 'getDirtyAttributes')) {
2596
                                            if (!$this->$relationName->getDirtyAttributes()) {
2597
                                                $excludeFromArray = true;
2598
                                            }
2599
                                        }
2600
                                    }
2601
2602
                                    if ($excludeFromArray) {
2603
                                        // exclude
2604
                                    } elseif (method_exists($this->$relationName, 'callDebugTestOnAll')) {
2605
                                        $data[$relationName] = $this->$relationName->callDebugTestOnAll($loadedOnly, $excludeNewAndBlankRelations);
2606
                                    } elseif (method_exists($this->$relationName, 'debugTest')) {
2607
                                        $data[$relationName] = $this->$relationName->debugTest();
2608
                                    }
2609
                                }
2610
2611
                            }
2612
                            break;
2613
2614
                        default:
2615
2616
                            // exclude from allToArray
2617
                            break;
2618
                    }
2619
                }
2620
            }
2621
        }
2622
2623
        return $data;
2624
    }
2625
2626
}
2627