ActiveRecord::isDefinedRelation()   B
last analyzed

Complexity

Conditions 5
Paths 3

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

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