ActiveAttributeRecord::beforeDeleteFullInternal()   C
last analyzed

Complexity

Conditions 13
Paths 192

Size

Total Lines 55
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 55
rs 6.0838
c 0
b 0
f 0
cc 13
eloc 27
nc 192
nop 1

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
/**
3
 * This file is part of the fangface/yii2-concord package
4
 *
5
 * For the full copyright and license information, please view
6
 * the file LICENSE.md that was distributed with this source code.
7
 *
8
 * @package fangface/yii2-concord
9
 * @author Fangface <[email protected]>
10
 * @copyright Copyright (c) 2014 Fangface <[email protected]>
11
 * @license https://github.com/fangface/yii2-concord/blob/master/LICENSE.md MIT License
12
 *
13
 */
14
15
namespace fangface\db;
16
17
use fangface\Tools;
18
use fangface\base\traits\ActionErrors;
19
use fangface\base\traits\ServiceGetter;
20
use fangface\db\ActiveRecordParentalInterface;
21
use fangface\db\ActiveRecordParentalTrait;
22
use fangface\db\ActiveRecordReadOnlyInterface;
23
use fangface\db\ActiveRecordReadOnlyTrait;
24
use fangface\db\ActiveRecordSaveAllInterface;
25
use fangface\db\Exception;
26
use fangface\models\eav\AttributeDefinitions;
27
use fangface\models\eav\AttributeEntities;
28
use fangface\models\eav\AttributeValues;
29
use yii\base\InvalidConfigException;
30
use yii\base\ModelEvent;
31
32
class ActiveAttributeRecord implements ActiveRecordParentalInterface, ActiveRecordReadOnlyInterface, ActiveRecordSaveAllInterface
33
{
34
35
    use ActionErrors;
36
    use ActiveRecordParentalTrait;
37
    use ActiveRecordReadOnlyTrait;
38
    use ServiceGetter;
39
40
    /**
41
     * @var string|false
42
     */
43
    protected $attributeEntitiesClass = false;
44
45
    /**
46
     * @var string|false
47
     */
48
    protected $attributeDefinitionsClass = false;
49
50
    /**
51
     * @var string|false
52
     */
53
    protected $attributeValuesClass = false;
54
55
    /**
56
     * Provided by $config or by class extension
57
     *
58
     * @var array|false The attribtue mapping details to use against the $parentModel to obtain the $objectId
59
     */
60
    protected $link = false;
61
62
    /**
63
     * Provided by $config or by class extension
64
     *
65
     * @var integer|false The entityId for this attribute class if it us using a shared set of attribute tables
66
     */
67
    protected $entityId = false;
68
69
    /**
70
     * Provided by $config or $parentModel
71
     *
72
     * @var integer|false The objectId for which attributes will be loaded, saved or deleted
73
     */
74
    protected $objectId = false;
75
76
    /**
77
     * Array of existing attribute value models ready to use during delete or save
78
     *
79
     * @var AttributeValues[]|array
80
     */
81
    private $attributeValues = array();
82
83
    /**
84
     *
85
     * @var boolean Indicates if this attribute class has been loaded with the current entityId and objectId attributes
86
     */
87
    private $loaded = false;
88
89
    /**
90
     *
91
     * @var boolean Indicates if this new record has ahd the default attributes setup ready for population
92
     */
93
    private $isNewPrepared = false;
94
95
    /**
96
     * Array of existing attribute values including defaults that will be interacted with until time to save, delete or create
97
     *
98
     * @var array
99
     */
100
    private $data = array();
101
102
    /**
103
     * Array of attributes that have not been loaded up yet because they have been specified as only requiring lazy loading at time of use
104
     *
105
     * @var array
106
     */
107
    private $lazyAttributes = array();
108
109
    /**
110
     * Array of changed attributes and their original values
111
     *
112
     * @var array
113
     */
114
    private $changedData = array();
115
116
117
    /**
118
     * Note if a loaded attribute class is assigned a new objectId for propagation during saveAll()
119
     * to all attributes not just those that have changes
120
     *
121
     * @var boolean|integer
122
     */
123
    private $newObjectId = false;
124
125
    /**
126
     * Canonical name of the class
127
     * each time a definitions model is required
128
     *
129
     * @var string
130
     */
131
    private $cleanDefinitionsClassName = '';
132
133
    /**
134
     * Holds an array of attribute definitions so that they do not need to be reloaded from the database
135
     * each time a definitions model is required
136
     *
137
     * @var array
138
     */
139
    private static $attributeDefinitions = array();
140
141
    /**
142
     * Holds an array of attribute definition maps so that they do not need to be reprocessed
143
     * each time a definitions model is required
144
     *
145
     * @var array
146
     */
147
    private static $attributeDefinitionsMap = array();
148
149
    /**
150
     * @var array validation errors (attribute name => array of errors)
151
     */
152
    private $errors;
153
154
    /**
155
     * @event ModelEvent an event that is triggered before saveAll()
156
     * You may set [[ModelEvent::isValid]] to be false to stop the update.
157
     */
158
    const EVENT_BEFORE_SAVE_ALL = 'beforeSaveAll';
159
    /**
160
     * @event Event an event that is triggered after saveAll() has completed
161
     */
162
    const EVENT_AFTER_SAVE_ALL = 'afterSaveAll';
163
    /**
164
     * @event Event an event that is triggered after saveAll() fails
165
     */
166
    const EVENT_AFTER_SAVE_ALL_FAILED = 'afterSaveAllFailed';
167
168
    /**
169
     * @event ModelEvent an event that is triggered before saveAll()
170
     * You may set [[ModelEvent::isValid]] to be false to stop the update.
171
     */
172
    const EVENT_BEFORE_DELETE_FULL = 'beforeDeleteFull';
173
174
    /**
175
     * @event Event an event that is triggered after saveAll() has completed
176
     */
177
    const EVENT_AFTER_DELETE_FULL = 'afterDeleteFull';
178
179
    /**
180
     * @event Event an event that is triggered after saveAll() has failed
181
     */
182
    const EVENT_AFTER_DELETE_FULL_FAILED = 'afterDeleteFullFailed';
183
184
185
    /**
186
     * Constructor
187
     * @param array|integer|false $config
188
     */
189
    public function __construct($config = array())
190
    {
191
        if (!empty($config)) {
192
            if (is_array($config)) {
193
                $this->configure($config);
194
            } elseif (is_numeric($config)) {
195
                $this->objectId = $config;
196
            }
197
        }
198
        $this->init();
199
    }
200
201
202
    /**
203
     * @param mixed $config
204
     */
205
    public function configure($config)
206
    {
207
        foreach ($config as $name => $value) {
208
            switch ($name) {
209
                case 'entityId':
210
                case 'attributeEntitiesClass':
211
                case 'attributeDefinitionsClass':
212
                case 'attributeValuesClass':
213
                case 'link':
214
                case 'objectId':
215
                case 'parentModel':
216
217
                    if (!is_null($value)) {
218
                        $this->$name = $value;
219
                    }
220
221
                    break;
222
223
                default:
224
                    break;
225
            }
226
        }
227
    }
228
229
230
    public function init()
231
    {
232
        if (!$this->attributeEntitiesClass) {
233
            $this->attributeEntitiesClass = AttributeEntities::className();
234
        }
235
236
        if (!$this->attributeDefinitionsClass) {
237
            $this->attributeDefinitionsClass = AttributeDefinitions::className();
238
        }
239
240
        if (!$this->attributeValuesClass) {
241
            $this->attributeValuesClass = AttributeValues::className();
242
        }
243
244
        if (!class_exists($this->attributeEntitiesClass)) {
245
            throw new Exception('Attribute entity class ' . $this->attributeEntitiesClass . ' not found in ' . __METHOD__ . '()');
246
        }
247
248
        if (!class_exists($this->attributeDefinitionsClass)) {
249
            throw new Exception('Attribute definition class ' . $this->attributeDefinitionsClass . ' not found in ' . __METHOD__ . '()');
250
        }
251
252
        if (!class_exists($this->attributeValuesClass)) {
253
            throw new Exception('Attribute value class ' . $this->attributeValuesClass . ' not found in ' . __METHOD__ . '()');
254
        }
255
256
        if ($this->entityId === false) {
257
            throw new Exception('No entity id available after ' . __METHOD__ . '()');
258
        }
259
260
        if (!$this->objectId && $this->parentModel && $this->parentModel instanceof \yii\db\ActiveRecord && is_array($this->link) && $this->link) {
261
            if (isset($this->link['objectId']) && $this->link['objectId']) {
262
                $this->objectId = $this->parentModel->getAttribute($this->link['objectId']);
263
            } else {
264
                $this->objectId = $this->parentModel->getAttribute($this->link[0]);
265
            }
266
        }
267
    }
268
269
270
    /**
271
     * Obtain class name
272
     *
273
     * @return string the fully qualified name of this class.
274
     */
275
    public static function className()
276
    {
277
        return get_called_class();
278
    }
279
280
281
    /**
282
     * Reset current attribute list
283
     *
284
     * @param boolean $resetObject
285
     *        [OPTIONAL] reset objectId and parent model
286
     * @param boolean $resetAll
287
     *        [OPTIONAL] reset all
288
     */
289
    public function reset($resetObject = false, $resetAll = false)
290
    {
291
        if ($resetAll) {
292
            $this->attributeEntitiesClass = false;
293
            $this->attributeDefinitionsClass = false;
294
            $this->attributeValuesClass = false;
295
            $this->entityId = false;
296
            $this->link = false;
297
        }
298
299
        if ($resetObject) {
300
            $this->parentModel = false;
301
            $this->objectId = false;
302
        }
303
304
        $this->attributeValues = array();
305
        $this->loaded = false;
306
        $this->isNewRecord = true;
307
        $this->isNewPrepared = false;
308
        $this->data = array();
309
        $this->lazyAttributes = array();
310
        $this->changedData = array();
311
        $this->newObjectId = false;
312
    }
313
314
315
    /**
316
     * Load attribute values into the object either from the db or assign the default values
317
     * if this is a new record
318
     *
319
     * @param boolean $forceLoadLazyLoad
320
     * @throws Exception
321
     */
322
    public function loadAttributeValues($forceLoadLazyLoad = false)
323
    {
324
        $this->reset();
325
326
        if ($this->entityId === false) {
327
            throw new Exception('No entity id available for ' . __METHOD__ . '()');
328
        }
329
330
        if (!$this->objectId && !$this->isNewRecord) {
331
            throw new Exception('No object id available for ' . __METHOD__ . '()');
332
        } elseif (!$this->objectId) {
333
            // setup all attributes with their default values ready for this new record
334
            $forceLoadLazyLoad = true;
335
        }
336
337
        $excludeAttributeIDList = array();
338
        $tempMap = array();
339
340
        $attributeDefs = $this->getEntityAttributeList();
341
342
        foreach ($attributeDefs as $k => $v) {
343
            if ($v['lazyLoad'] && !$forceLoadLazyLoad) {
344
                // we don't want to load up the values from these fields until they are explicitly accessed
345
                $this->lazyAttributes[$k] = $v['id'];
346
                $excludeAttributeIDList[] = $v['id'];
347
            } else {
348
                $this->data[$k] = $v['defaultValue'];
349
            }
350
            $tempMap[$v['id']] = $k;
351
        }
352
353
        if ($this->objectId) {
354
355
            $attributeValuesClass = $this->attributeValuesClass;
356
            $query = $attributeValuesClass::find();
357
358
            if ($this->entityId) {
359
                $query->where(array(
360
                    'entityId' => $this->entityId,
361
                    'objectId' => $this->objectId
362
                ));
363
            } else {
364
                $query->where(array(
365
                    'objectId' => $this->objectId
366
                ));
367
            }
368
369
            if ($excludeAttributeIDList) {
370
                $query->andWhere(array(
371
                    'NOT IN',
372
                    'attributeId',
373
                    $excludeAttributeIDList
374
                ));
375
            }
376
377
            $rows = $query->all();
378
379
            if ($rows) {
380
                foreach ($rows as $k => $v) {
381
                    $this->attributeValues[$tempMap[$v->attributeId]] = $v;
382
                    $this->data[$tempMap[$v->attributeId]] = $v->value;
383
                }
384
            }
385
386
            $this->loaded = true;
387
            $this->isNewRecord = false;
388
        } else {
389
            $this->isNewPrepared = true;
390
        }
391
392
        foreach ($this->data as $k => $v) {
393
            // now apply value formatters because all values are trpically stored in string fields at the moment
394
            $this->data[$k] = Tools::formatAttributeValue($v, $attributeDefs[$k]);
395
        }
396
    }
397
398
399
    /**
400
     * Load all or specific outstanding lazy loaded attributes
401
     *
402
     * @param string|array|null $attributeNames
403
     */
404
    public function loadLazyAttribute($attributeNames = null)
405
    {
406
        if ($this->entityId === false) {
407
            throw new Exception('No entity id available for ' . __METHOD__ . '()');
408
        }
409
        if (!$this->objectId) {
410
            throw new Exception('No object id available for ' . __METHOD__ . '()');
411
        }
412
        $singleAttributeName = false;
413
        $newAttributes = array();
414
        if (is_null($attributeNames) && $this->lazyAttributes) {
415
            $attributeNames = array_flip($this->lazyAttributes);
416
        } elseif (!is_array($attributeNames) && is_string($attributeNames) && $attributeNames) {
417
            $singleAttributeName = $attributeNames;
418
            $attributeNames = array(
419
                $attributeNames
420
            );
421
        } elseif (is_array($attributeNames) && count($attributeNames) == 1) {
422
            $singleAttributeName = $attributeNames[0];
423
        }
424
        if ($attributeNames) {
425
            $attributeIdList = array();
426
            $leftOverAttributeIdToNameMap = array();
427
            foreach ($attributeNames as $k => $v) {
428
                if ($this->lazyAttributes && array_key_exists($v, $this->lazyAttributes)) {
429
                    $attributeIdList[] = $this->lazyAttributes[$v];
430
                    $leftOverAttributeIdToNameMap[$this->lazyAttributes[$v]] = $v;
431
                }
432
            }
433
            if ($attributeIdList) {
434
                $attributeValuesClass = $this->attributeValuesClass;
435
                $query = $attributeValuesClass::find();
436
                if ($this->entityId) {
437
                    $query->where(array(
438
                        'entityId' => $this->entityId,
439
                        'objectId' => $this->objectId
440
                    ));
441
                } else {
442
                    $query->where(array(
443
                        'objectId' => $this->objectId
444
                    ));
445
                }
446
                if (count($attributeIdList) > 1) {
447
                    $query->andWhere(array(
448
                        'attributeId' => $attributeIdList
449
                    ));
450
                } else {
451
                    $query->andWhere(array(
452
                        'attributeId' => $attributeIdList[0]
453
                    ));
454
                }
455
                $rows = $query->all();
456
                if ($rows) {
457
                    foreach ($rows as $k => $v) {
458
                        if (isset($leftOverAttributeIdToNameMap[$v->attributeId])) {
459
                            $this->attributeValues[$leftOverAttributeIdToNameMap[$v->attributeId]] = $v;
460
                            $newAttributes[$leftOverAttributeIdToNameMap[$v->attributeId]] = $v->value;
461
                            unset($leftOverAttributeIdToNameMap[$v->attributeId]);
462
                        }
463
                    }
464
                }
465
                if ($newAttributes || $leftOverAttributeIdToNameMap) {
466
                    $attributeDefs = $this->getEntityAttributeList();
467
                    if ($attributeDefs) {
468
                        if ($leftOverAttributeIdToNameMap) {
469
                            foreach ($leftOverAttributeIdToNameMap as $k => $v) {
470
                                $newAttributes[$v] = $attributeDefs[$v]['defaultValue'];
471
                            }
472
                        }
473
                        // now apply value formatters because all values are trpically stores in string fields
474
                        foreach ($newAttributes as $k => $v) {
475
                            $this->data[$k] = Tools::formatAttributeValue($v, $attributeDefs[$k]);
476
                            unset($this->lazyAttributes[$k]);
477
                        }
478
                    }
479
                }
480
            }
481
            if ($singleAttributeName && isset($newAttributes[$singleAttributeName])) {
482
                return true;
483
            } elseif (!$singleAttributeName && $newAttributes) {
484
                return true;
485
            }
486
        }
487
        return false;
488
    }
489
490
491
    /**
492
     * Determine if model has been loaded
493
     *
494
     * @return boolean
495
     */
496
    public function isLoaded()
497
    {
498
        return $this->loaded;
499
    }
500
501
502
    /**
503
     * Determine if model has any unsaved changes optionally checking to see if any sub
504
     * models in the current model map also have any changes even if the current model
505
     * does not
506
     *
507
     * @param boolean $checkRelations
508
     *        should changes in relations be checked as well
509
     * @return boolean
510
     *        changes exist
511
     */
512
    public function hasChanges($checkRelations=false)
513
    {
514
        if ($this->loaded && $this->newObjectId) {
515
            return true;
516
        }
517
        return ($this->changedData ? true : false);
518
    }
519
520
521
    /**
522
     * Determine if attribute has chanegd and not been saved
523
     *
524
     * @return boolean
525
     */
526
    public function isAttributeChanged($attributeName)
527
    {
528
        return (array_key_exists($attributeName, $this->changedData) ? true : false);
529
    }
530
531
532
    /**
533
     * Returns the attribute values that have been modified since they are loaded or saved most recently.
534
     *
535
     * @param string[]|null $names
536
     *        the names of the attributes whose values may be returned if they are
537
     *        changed recently. If null all current changed attribtues will be returned.
538
     * @return array the changed attribute values
539
     */
540
    public function getDirtyAttributes($names = null)
541
    {
542
        if ($names === null) {
543
            return $this->changedData;
544
        }
545
        $names = array_flip($names);
546
        $attributes = array();
547
        foreach ($this->changedData as $name => $value) {
548
            if (isset($names[$name])) {
549
                $attributes[$name] = $value;
550
            }
551
        }
552
        return $attributes;
553
    }
554
555
556
    /**
557
     * Returns array of attributes as they were before changes were made
558
     *
559
     * @return array
560
     */
561
    public function getOldAttributes()
562
    {
563
        return ($this->changedData ? $this->changedData : array());
564
    }
565
566
567
    /**
568
     * Returns array of attributes as they were before changes were made
569
     *
570
     * @return array
571
     */
572
    public function setOldAttributes($values)
573
    {
574
        if (is_array($values) && $values) {
575
            $this->changedData = $values;
576
        } else {
577
            $this->changedData = array();
578
        }
579
    }
580
581
582
    /**
583
     * Check to see if the attributes have been loaded and if not trigger the loading process
584
     *
585
     * @param boolean $forceLoadLazyLoad
586
     *        [OPTIONAL] should lazy load attributes be forced to load, default is false
587
     */
588
    public function checkAndLoad($forceLoadLazyLoad = false)
589
    {
590
        if (!$this->loaded && !$this->isNewPrepared) {
591
            $this->loadAttributeValues($forceLoadLazyLoad);
592
        } elseif ($forceLoadLazyLoad) {
593
            $this->forceLoadLazyLoad(true);
594
        }
595
    }
596
597
598
    /**
599
     * Return list of attributes as an array (forcing the load of any attributes that have not been loaded yet)
600
     *
601
     * @return array
602
     */
603
    public function toArray(array $fields = [], array $expand = [], $recursive = true)
604
    {
605
        $this->checkAndLoad(true);
606
        if ($fields) {
607
            $data = array();
608
            foreach ($fields as $field) {
609
                if (array_key_exists($field, $this->data)) {
610
                    $data[$field] = $this->data[$field];
611
                }
612
            }
613
        }
614
        return $this->data;
615
    }
616
617
618
    /**
619
     * Return list of attributes as an array (by default forcing the load of any attributes that have not been loaded yet)
620
     *
621
     * @param boolean $loadedOnly
622
     *        [OPTIONAL] default false
623
     * @param boolean $excludeNewAndBlankRelations
624
     *        [OPTIONAL] exclude new blank records, default true
625
     * @return array
626
     */
627
    public function allToArray($loadedOnly=false, $excludeNewAndBlankRelations=true)
628
    {
629
        $this->checkAndLoad(!$loadedOnly);
630
        return $this->data;
631
    }
632
633
634
    /**
635
     * Check if lazy loaded attributes exist and should be fully loaded and if so load them
636
     *
637
     * @param boolean $forceLoadLazyLoad
638
     *        [OPTIONAL] default is false
639
     */
640
    public function forceLoadLazyLoad($forceLoadLazyLoad = false)
641
    {
642
        if ($forceLoadLazyLoad && $this->lazyAttributes) {
643
            $this->loadLazyAttribute();
644
        }
645
    }
646
647
648
    public function getEntityAttributeList($attributeName = '', $entityId = null)
649
    {
650
        $entityId = (is_null($entityId) ? $this->entityId : $entityId);
651
652
        if ($entityId === false) {
653
            throw new Exception('No entity id available for ' . __METHOD__ . '()');
654
        }
655
656
        if (isset(self::$attributeDefinitions[Tools::getClientId()][$this->cleanDefinitionsClassName][$entityId])) {
657
            if ($attributeName) {
658
                if (isset(self::$attributeDefinitions[Tools::getClientId()][$this->cleanDefinitionsClassName][$entityId][$attributeName])) {
659
                    return self::$attributeDefinitions[Tools::getClientId()][$this->cleanDefinitionsClassName][$entityId][$attributeName];
660
                }
661
                return false;
662
            }
663
            return self::$attributeDefinitions[Tools::getClientId()][$this->cleanDefinitionsClassName][$entityId];
664
        }
665
666
        $conditions = array();
667
        if ($this->entityId) {
668
            $conditions['entityId'] = $this->entityId;
669
        }
670
671
        $attributeDefinitionsClass = $this->attributeDefinitionsClass;
672
        $attributeDefinitions = $attributeDefinitionsClass::find()->where($conditions)
673
            ->orderBy('sortOrder')
674
            ->asArray()
675
            ->all();
676
677
        $ok = ($attributeDefinitions ? true : false);
678
679
        if ($ok) {
680
            $result = array();
681
            foreach ($attributeDefinitions as $k => $v) {
682
                $result[$v['attributeName']] = $v;
683
            }
684
            self::$attributeDefinitions[Tools::getClientId()][$this->cleanDefinitionsClassName][$entityId] = $result;
685
        }
686
687
        if ($attributeName) {
688
            if (isset($result[$attributeName])) {
689
                return $result[$attributeName];
690
            }
691
            return false;
692
        }
693
694
        if (!$ok) {
695
            $result = false;
696
        }
697
698
        return $result;
699
    }
700
701
702
    /**
703
     * Obtain a mapping array that tells us which attribute name belongs to which id and vice versa
704
     *
705
     * @param integer|null|false $entityId
706
     * @throws Exception if entity id is not set
707
     * @return array|false
708
     */
709
    public function getEntityAttributeMap($entityId = null)
710
    {
711
        $entityId = (is_null($entityId) ? $this->entityId : $entityId);
712
713
        if ($this->entityId === false) {
714
            throw new Exception('No entity id available for ' . __METHOD__ . '()');
715
        }
716
717
        if (isset(self::$attributeDefinitionsMap[Tools::getClientId()][$this->cleanDefinitionsClassName][$entityId])) {
718
            return self::$attributeDefinitionsMap[Tools::getClientId()][$this->cleanDefinitionsClassName][$entityId];
719
        }
720
721
        $attributeList = $this->getEntityAttributeList('', $entityId);
722
723
        self::$attributeDefinitionsMap[Tools::getClientId()][$this->cleanDefinitionsClassName][$entityId] = array(
724
            'id' => array(),
725
            'name' => array()
726
        );
727
728
        if ($attributeList) {
729
            foreach ($attributeList as $k => $v) {
730
                self::$attributeDefinitionsMap[Tools::getClientId()][$this->cleanDefinitionsClassName][$entityId]['id'][$v['id']] = $v['attributeName'];
731
                self::$attributeDefinitionsMap[Tools::getClientId()][$this->cleanDefinitionsClassName][$entityId]['name'][$v['attributeName']] = $v['id'];
732
                self::$attributeDefinitionsMap[Tools::getClientId()][$this->cleanDefinitionsClassName]['__ALL__'][$v['id']] = array(
733
                    'name' => $v['attributeName'],
734
                    'entity' => $v['entityId']
735
                );
736
            }
737
        }
738
739
        return self::$attributeDefinitionsMap[Tools::getClientId()][$this->cleanDefinitionsClassName][$entityId];
740
    }
741
742
743
    /**
744
     * Return attribute list structure as an array compatible with the response from DB::getTableMetaData()
745
     *
746
     * @param integer $entityId
747
     *        [OPTIONAL] defaults to current initialised entityId
748
     * @return array
749
     */
750
    public function getEntityAttributeListAsStructure($entityId = null)
751
    {
752
        $entityId = (is_null($entityId) ? $this->entityId : $entityId);
753
754
        if ($this->entityId === false) {
755
            throw new Exception('No entity id available for ' . __METHOD__ . '()');
756
        }
757
758
        $attributeList = $this->getEntityAttributeList('', $entityId);
759
760
        $structure = array();
761
        foreach ($attributeList as $k => $v) {
762
763
            $type = trim($v['dataType']);
764
            $characterlength = null;
765
            $numericPrecision = null;
766
            $numericScale = null;
767
768
            switch ($type) {
769
                case 'int':
770
                case 'integer':
771
                case 'tinyint':
772
                case 'smallint':
773
                case 'bigint':
774
                case 'double':
775
                case 'float':
776
                case 'decimal':
777
                case 'serial':
778
                case 'numeric':
779
                case 'dec':
780
                case 'fixed':
781
                    $numericPrecision = $v['length'];
782
                    $numericScale = $v['decimals'];
783
                    break;
784
                default:
785
                    $characterlength = $v['length'];
786
                    break;
787
            }
788
789
            $structure[$k] = array(
790
                'columnDefault' => $v['defaultValue'],
791
                'isNullable' => ($v['isNullable'] ? true : false),
792
                'dataType' => $type,
793
                'characterMaximumLength' => $characterlength,
794
                'isNumeric' => (!is_null($numericPrecision) && $numericPrecision ? true : false),
795
                'numericPrecision' => $numericPrecision,
796
                'numericScale' => $numericScale,
797
                'numericUnsigned' => ($v['unsigned'] ? true : false),
798
                'autoIncrement' => false,
799
                'primaryKey' => false,
800
                'zeroFill' => ($v['zerofill'] ? true : false)
801
            );
802
        }
803
804
        return $structure;
805
    }
806
807
808
    /**
809
     * Return an attribute id for a given attribute name (assumes entityId not in use in this model
810
     *
811
     * @param string $attributeName
812
     * @return integer|false
813
     */
814
    public function getAttributeIdByName($attributeName)
815
    {
816
        return $this->getEntityAttributeIdByName($attributeName);
817
    }
818
819
820
    /**
821
     * Return an attribute id for a given entity and attribute name
822
     *
823
     * @param string $attributeName
824
     * @param integer $entityId
825
     *        [OPTIONAL] defaults to current initialised entityId
826
     * @return integer|false
827
     */
828
    public function getEntityAttributeIdByName($attributeName, $entityId = null)
829
    {
830
        $entityId = (is_null($entityId) ? $this->entityId : $entityId);
831
832
        if ($this->entityId === false) {
833
            throw new Exception('No entity id available for ' . __METHOD__ . '()');
834
        }
835
836
        if ($attributeName) {
837
            $attributeMap = $this->getEntityAttributeMap($entityId);
838
            if (isset($attributeMap['name'][$attributeName])) {
839
                return $attributeMap['name'][$attributeName];
840
            }
841
        }
842
        return false;
843
    }
844
845
846
    /**
847
     * Return an attribute name for a given entity and attribute id
848
     *
849
     * @param integer $attributeId
850
     * @param integer $entityId
851
     *        [OPTIONAL] defaults to current initialised entityId
852
     * @return string false
853
     */
854
    public function getEntityAttributeNameById($attributeId, $entityId = null)
855
    {
856
        $entityId = (is_null($entityId) ? $this->entityId : $entityId);
857
858
        if ($this->entityId === false) {
859
            throw new Exception('No entity id available for ' . __METHOD__ . '()');
860
        }
861
862
        if ($attributeId) {
863
            $attributeMap = $this->getEntityAttributeMap($entityId);
864
            if (isset($attributeMap['id'][$attributeId])) {
865
                return $attributeMap['id'][$attributeId];
866
            }
867
        }
868
        return false;
869
    }
870
871
872
    /**
873
     * Reads an attribute value by its name
874
     * <code>
875
     * echo $attributes->getAttribute('name');
876
     * </code>
877
     *
878
     * @param string $attributeName
879
     * @return mixed
880
     */
881
    public function getAttribute($attributeName)
882
    {
883
        if ($attributeName == 'objectId') {
884
            return $this->objectId;
885
        }
886
        return $this->__get($attributeName);
887
    }
888
889
890
    /**
891
     * Writes an attribute value by its name
892
     * <code>
893
     * $attributes->setAttribute('name', 'Rosey');
894
     * </code>
895
     *
896
     * @param string $attributeName
897
     * @param mixed $value
898
     */
899
    public function setAttribute($attributeName, $value)
900
    {
901
        if ($attributeName == 'objectId') {
902
            if ($this->loaded && $this->objectId && $this->objectId != $value) {
903
                $this->newObjectId = $value;
904
            } else {
905
                $this->objectId = $value;
906
            }
907
        } else {
908
            $this->__set($attributeName, $value);
909
        }
910
    }
911
912
913
    /**
914
     * Check to see if this attribute model includes the specified field name
915
     *
916
     * @param string $attributeName
917
     * @return boolean
918
     */
919
    public function hasAttribute($attributeName)
920
    {
921
        return $this->__isset($attributeName);
922
    }
923
924
925
    /**
926
     * Returns attribute values.
927
     *
928
     * @param array $names
929
     *        list of attributes whose value needs to be returned.
930
     *        Defaults to null, meaning all attributes listed in [[attributes()]] will be returned.
931
     *        If it is an array, only the attributes in the array will be returned.
932
     * @param array $except
933
     *        list of attributes whose value should NOT be returned.
934
     * @param boolean $loadedOnly
935
     *        [OPTIONAL] default true
936
     * @return array attribute values (name => value).
937
     */
938
    public function getAttributes($names = null, $except = [], $loadedOnly = true)
939
    {
940
        $valuesAll = $this->allToArray($loadedOnly);
941
942
        $values = array();
943
        if ($names === null) {
944
            $names = array_keys($valuesAll);
945
        }
946
        foreach ($names as $name) {
947
            $values[$name] = $valuesAll[$name];
948
        }
949
        foreach ($except as $name) {
950
            unset($values[$name]);
951
        }
952
953
        return $values;
954
    }
955
956
957
    /**
958
     * Sets the attribute values in a massive way.
959
     *
960
     * @param array $values
961
     *        attribute values (name => value) to be assigned to the model.
962
     * @param boolean $safeOnly
963
     *        whether the assignments should only be done to the safe attributes.
964
     *        A safe attribute is one that is associated with a validation rule in the current [[scenario]].
965
     * @see safeAttributes()
966
     * @see attributes()
967
     */
968
    public function setAttributes($values, $safeOnly = true)
969
    {
970
        if (is_array($values)) {
971
            foreach ($values as $name => $value) {
972
                $this->__set($name, $value);
973
            }
974
        }
975
    }
976
977
978
    /**
979
     * Magic get method to return attributes from the data array
980
     *
981
     * @param string $property
982
     *        property name
983
     * @return mixed
984
     */
985
    public function &__get($property)
986
    {
987
        if (!$this->loaded && !$this->isNewPrepared) {
988
            $this->loadAttributeValues();
989
            return $this->__get($property);
990
        } elseif (array_key_exists($property, $this->data)) {
991
            return $this->data[$property];
992
        } elseif (array_key_exists($property, $this->lazyAttributes)) {
993
            if ($this->loadLazyAttribute($property)) {
994
                return $this->__get($property);
995
            }
996
        }
997
        $trace = debug_backtrace();
998
        trigger_error('Undefined relation property via __get() in ' . get_class($this) . '::' . $property . ' in ' . $trace[0]['file'] . ' on line ' . $trace[0]['line'], E_USER_NOTICE);
999
        $null = null;
1000
        return $null;
1001
    }
1002
1003
1004
    /**
1005
     * Magic get method to return attributes from the data array
1006
     *
1007
     * @param string $property
1008
     *        property name
1009
     * @param mixed $value
1010
     *        property value
1011
     */
1012
    public function __set($property, $value)
1013
    {
1014
        if (!$this->loaded && !$this->isNewPrepared) {
1015
            $this->loadAttributeValues();
1016
            $this->__set($property, $value);
1017
        } elseif (array_key_exists($property, $this->data)) {
1018
            if (!array_key_exists($property, $this->changedData) && $value != $this->data[$property]) {
1019
                // changed for the first time since loaded
1020
                $this->changedData[$property] = $this->data[$property];
1021
                $this->data[$property] = $value;
1022
            } elseif (array_key_exists($property, $this->changedData) && $value == $this->changedData[$property]) {
1023
                // reverting back to what it was when loaded
1024
                $this->data[$property] = $this->changedData[$property];
1025
                unset($this->changedData[$property]);
1026
            } elseif (array_key_exists($property, $this->changedData) && $value != $this->data[$property]) {
1027
                // previously changed and changing again
1028
                $this->data[$property] = $value;
1029
            }
1030
        } elseif (array_key_exists($property, $this->lazyAttributes)) {
1031
            if ($this->loadLazyAttribute($property)) {
1032
                $this->__set($property, $value);
1033
            }
1034
        } else {
1035
            $trace = debug_backtrace();
1036
            trigger_error('Undefined relation property via __set() in ' . get_class($this) . '::' . $property . ' in ' . $trace[0]['file'] . ' on line ' . $trace[0]['line'], E_USER_NOTICE);
1037
        }
1038
    }
1039
1040
1041
    /**
1042
     * Magic isset method to check if attribute exists
1043
     *
1044
     * @param string $property
1045
     *        property name
1046
     * @return boolean
1047
     */
1048
    public function __isset($property)
1049
    {
1050
        $attributeDefs = $this->getEntityAttributeList();
1051
1052
        if (array_key_exists($property, $attributeDefs)) {
1053
            return true;
1054
        }
1055
1056
        return false;
1057
    }
1058
1059
1060
    /**
1061
     * Set multiple attributes by array
1062
     *
1063
     * @param array $inputData
1064
     * @param array $inputDataOnChange
1065
     *        [OPTIONAL] extra attributes to apply if $inputData causes changes
1066
     * @return boolean
1067
     */
1068
    public function setValuesByArray($inputData, $inputDataOnChange = false)
1069
    {
1070
        $hasChanges = false;
1071
1072
        if (is_array($inputData) && $inputData) {
1073
1074
            if (!$this->loaded && !$this->isNewPrepared) {
1075
                $this->loadAttributeValues();
1076
                $this->setValuesByArray($inputData, $inputDataOnChange);
1077
            } else {
1078
                foreach ($inputData as $k => $v) {
1079
                    if (array_key_exists($k, $this->data)) {
1080
                        if ($this->data[$k] != $v) {
1081
                            $hasChanges = true;
1082
                            $this->__set($k, $v);
1083
                        }
1084
                    } elseif ($this->lazyAttributes && array_key_exists($k, $this->lazyAttributes)) {
1085
                        $currentValue = $this->__get($k);
1086
                        if ($currentValue != $v) {
1087
                            $hasChanges = true;
1088
                            $this->__set($k, $v);
1089
                        }
1090
                    }
1091
                }
1092
                if ($hasChanges) {
1093
                    if (is_array($inputDataOnChange) && $inputDataOnChange) {
1094
                        foreach ($inputDataOnChange as $k => $v) {
1095
                            $this->__set($k, $v);
1096
                        }
1097
                    }
1098
                }
1099
            }
1100
        }
1101
1102
        return $hasChanges;
1103
    }
1104
1105
1106
    /**
1107
     * deletes the current objects attributesrd but also loops through defined
1108
     * relationships (if appropriate) to delete those as well
1109
     *
1110
     * @param boolean $hasParentModel
1111
     *        whether this method was called from the top level or by a parent
1112
     *        If false, it means the method was called at the top level
1113
     * @return boolean
1114
     *        did deleteFull() successfully process
1115
     */
1116
    public function deleteFull($hasParentModel = false)
1117
    {
1118
        $this->clearActionErrors();
1119
1120
        $allOk = false;
1121
1122
        if ($this->getIsNewRecord()) {
1123
1124
            // record does not exist yet anyway
1125
            $allOk = true;
1126
1127
        } elseif (!$hasParentModel && ($this->getReadOnly() || !$this->getCanDelete())) {
1128
1129
            // not allowed to amend or delete
1130
            $message = 'Attempting to delete ' . Tools::getClassName($this) . ($this->getReadOnly() ? ' readOnly model' : ' model flagged as not deletable');
1131
            $this->addActionError($message);
1132
            throw new Exception($message);
1133
1134
        } elseif ($hasParentModel && ($this->getReadOnly() || !$this->getCanDelete())) {
1135
1136
            // not allowed to amend or delete
1137
            $message = 'Attempting to delete ' . Tools::getClassName($this) . ($this->getReadOnly() ? ' readOnly model' : ' model flagged as not deletable');
1138
            $this->addActionError($message);
1139
            throw new Exception($message);
1140
1141
        } else {
1142
1143
            if (!$hasParentModel) {
1144
                // run beforeSaveAll and abandon saveAll() if it returns false
1145
                if (!$this->beforeDeleteFullInternal($hasParentModel)) {
1146
                    return false;
1147
                }
1148
            }
1149
1150
            try {
1151
                $allOk = $this->delete($hasParentModel, true);
1152
            } catch (\Exception $e) {
1153
                $allOk = false;
1154
                $this->addActionError($e->getMessage(), $e->getCode());
1155
            }
1156
1157
            if (!$hasParentModel) {
1158
                if ($allOk) {
1159
                    $this->afterDeleteFullInternal();
1160
                } else {
1161
                    $this->afterDeleteFullFailedInternal();
1162
                }
1163
            }
1164
1165
        }
1166
1167
        return $allOk;
1168
    }
1169
1170
1171
    /**
1172
     * This method is called at the beginning of a deleteFull() request on a record or model map
1173
     *
1174
     * @param boolean $hasParentModel
1175
     *        whether this method was called from the top level or by a parent
1176
     *        If false, it means the method was called at the top level
1177
     * @return boolean whether the deleteFull() method call should continue
1178
     *        If false, deleteFull() will be cancelled.
1179
     */
1180
    public function beforeDeleteFullInternal($hasParentModel = false)
1181
    {
1182
        $this->clearActionErrors();
1183
        $this->resetChildHasChanges();
1184
        $transaction = null;
1185
1186
        $canDeleteFull = true;
1187
1188
        if (!$hasParentModel) {
1189
            //$event = new ModelEvent;
1190
            //$this->trigger(self::EVENT_BEFORE_SAVE_ALL, $event);
1191
            //$canDeleteFull = $event->isValid;
1192
        }
1193
1194
        if ($canDeleteFull) {
1195
            if ($this->getIsNewRecord()) {
1196
                // will be ignored during deleteFull()
1197
            } elseif ($this->getReadOnly()) {
1198
                // will be ignored during deleteFull()
1199
            } elseif (!$this->getCanDelete()) {
1200
                // will be ignored during deleteFull()
1201
            } else {
1202
1203
                /**
1204
                 * All deleteFull() calls are treated as transactional and a transaction
1205
                 * will be started if one has not already been on the db connection
1206
                 */
1207
                $attributeValuesClass = $this->attributeValuesClass;
1208
                $db = $attributeValuesClass::getDb();
1209
                $transaction = $db->getTransaction() === null ? $db->beginTransaction() : null;
1210
1211
                $canDeleteFull = (!$canDeleteFull ? $canDeleteFull : $this->beforeDeleteFull());
1212
            }
1213
        }
1214
1215
        if ($this->hasActionErrors()) {
1216
            $canDeleteFull = false;
1217
        } elseif (!$canDeleteFull) {
1218
            $this->addActionError('beforeDeleteFullInternal checks failed');
1219
        }
1220
1221
        if (!$canDeleteFull) {
1222
            $this->resetChildHasChanges();
1223
            if ($transaction !== null) {
1224
                // cancel the started transaction
1225
                $transaction->rollback();
1226
            }
1227
        } else {
1228
            if ($transaction !== null) {
1229
                $this->setChildOldValues('_transaction_', $transaction);
1230
            }
1231
        }
1232
1233
        return $canDeleteFull;
1234
    }
1235
1236
1237
    /**
1238
     * Called by beforeDeleteFullInternal on the current model to determine if the whole of deleteFull
1239
     * can be processed - this is expected to be replaced in individual models when required
1240
     *
1241
     * @return boolean okay to continue with deleteFull
1242
     */
1243
    public function beforeDeleteFull()
1244
    {
1245
        return true;
1246
    }
1247
1248
1249
    /**
1250
     * This method is called at the end of a successful deleteFull()
1251
     *
1252
     * @param boolean $hasParentModel
1253
     *        whether this method was called from the top level or by a parent
1254
     *        If false, it means the method was called at the top level
1255
     */
1256
    public function afterDeleteFullInternal($hasParentModel = false)
1257
    {
1258
        /** @var \yii\db\Transaction $transaction */
1259
        $transaction = $this->getChildOldValues('_transaction_');
1260
        if ($transaction) {
1261
            $transaction->commit();
1262
        }
1263
1264
        if ($this->getIsNewRecord()) {
1265
            // will have been ignored during deleteFull()
1266
        } elseif ($this->getReadOnly()) {
1267
            // will have been ignored during deleteFull()
1268
        } elseif (!$this->getCanDelete()) {
1269
            // will have been ignored during deleteFull()
1270
        } else {
1271
            $this->reset();
1272
            $this->afterDeleteFull();
1273
        }
1274
1275
        $this->resetChildHasChanges();
1276
1277
        if (!$hasParentModel) {
1278
            //$this->trigger(self::EVENT_AFTER_DELETE_FULL);
1279
        }
1280
    }
1281
1282
1283
    /**
1284
     * Called by afterDeleteFullInternal on the current model once the whole of the deleteFull() has
1285
     * been successfully processed
1286
     */
1287
    public function afterDeleteFull()
1288
    {
1289
1290
    }
1291
1292
1293
    /**
1294
     * This method is called at the end of a failed deleteFull()
1295
     *
1296
     * @param boolean $hasParentModel
1297
     *        whether this method was called from the top level or by a parent
1298
     *        If false, it means the method was called at the top level
1299
     */
1300
    public function afterDeleteFullFailedInternal($hasParentModel = false)
1301
    {
1302
        /** @var \yii\db\Transaction $transaction */
1303
        $transaction = $this->getChildOldValues('_transaction_');
1304
        if ($transaction) {
1305
            $transaction->rollback();
1306
        }
1307
1308
        $this->resetChildHasChanges();
1309
1310
        if (!$hasParentModel) {
1311
            //$this->trigger(self::EVENT_AFTER_DELETE_FULL_FAILED);
1312
        }
1313
    }
1314
1315
1316
    /**
1317
     * Called by afterDeleteFullFailedInternal on the current model once deleteFull() has
1318
     * failed processing
1319
     */
1320
    public function afterDeleteFullFailed()
1321
    {
1322
1323
    }
1324
1325
1326
    /**
1327
     * Delete the current objects attributes
1328
     *
1329
     * @param boolean $hasParentModel
1330
     *        whether this method was called from the top level or by a parent
1331
     *        If false, it means the method was called at the top level
1332
     * @param boolean $fromDeleteFull
1333
     *        has the delete() call come from deleteFull() or not
1334
     * @return boolean
1335
     *        did delete() successfully process
1336
     */
1337
    public function delete($hasParentModel = false, $fromDeleteFull = false)
1338
    {
1339
        $ok = true;
1340
        if (!$this->getReadOnly() && $this->getCanDelete()) {
1341
1342
            if ($this->entityId === false) {
1343
                throw new Exception('No entity id available for ' . __METHOD__ . '()');
1344
            }
1345
1346
            if (!$this->objectId) {
1347
                throw new Exception('No object id available for ' . __METHOD__ . '()');
1348
            }
1349
1350
            $attributeValuesClass = $this->attributeValuesClass;
1351
1352
            try {
1353
                $ok = $attributeValuesClass::deleteAll(array(
1354
                    'entityId' => $this->entityId,
1355
                    'objectId' => $this->objectId
1356
                ));
1357
                if (!$ok) {
1358
                    // no exception thrown and data may no longer exist in the table
1359
                    // for this entity, so we are happy to return a good delete
1360
                    $ok = true;
1361
                }
1362
            } catch (\Exception $e) {
1363
                $ok = false;
1364
                $this->addActionError($e->getMessage(), $e->getCode());
1365
            }
1366
1367
            if ($ok) {
1368
                if (!$fromDeleteFull) {
1369
                    $this->reset();
1370
                }
1371
            }
1372
1373
        } elseif (!$hasParentModel) {
1374
            $message = 'Attempting to delete ' . Tools::getClassName($this) . ($this->getReadOnly() ? ' readOnly model' : ' model flagged as not deletable');
1375
            //$this->addActionError($message);
1376
            throw new Exception($message);
1377
        } else {
1378
            $this->addActionWarning('Skipped delete of ' . Tools::getClassName($this) . ' which is ' . ($this->getReadOnly() ? 'read only' : 'flagged as not deletable'));
1379
        }
1380
1381
        return $ok;
1382
    }
1383
1384
    public function validate($attributes = null, $clearErrors = true) {
1385
        if ($clearErrors) {
1386
            $this->clearErrors();
1387
        }
1388
1389
        /*
1390
        $this->addError('fieldx', 'Forced error on fieldx as proof of concept on activeattributerecord');
1391
        $this->addError('fieldx', 'Another error value');
1392
        */
1393
1394
        return !$this->hasErrors();
1395
    }
1396
1397
1398
    /**
1399
     * This method is called at the beginning of a saveAll() request on a record or model map
1400
     *
1401
     * @param boolean $runValidation
1402
     *        should validations be executed on all models before allowing saveAll()
1403
     * @param boolean $hasParentModel
1404
     *        whether this method was called from the top level or by a parent
1405
     *        If false, it means the method was called at the top level
1406
     * @param boolean $push
1407
     *        is saveAll being pushed onto lazy (un)loaded models as well
1408
     * @return boolean whether the saveAll() method call should continue
1409
     *        If false, saveAll() will be cancelled.
1410
     */
1411
    public function beforeSaveAllInternal($runValidation = true, $hasParentModel = false, $push = false)
1412
    {
1413
1414
        $this->clearActionErrors();
1415
        $this->resetChildHasChanges();
1416
        $transaction = null;
1417
1418
        $canSaveAll = true;
1419
1420
        if (!$hasParentModel) {
1421
            //$event = new ModelEvent;
1422
            //$this->trigger(self::EVENT_BEFORE_SAVE_ALL, $event);
1423
            //$canSaveAll = $event->isValid;
1424
        }
1425
1426
        if ($this->getReadOnly()) {
1427
            // will be ignored during saveAll() and should have been caught by saveAll() if called directly
1428
        } else {
1429
1430
            /**
1431
             * All saveAll() calls are treated as transactional and a transaction
1432
             * will be started if one has not already been on the db connection
1433
             */
1434
            $attributeValuesClass = $this->attributeValuesClass;
1435
            $db = $attributeValuesClass::getDb();
1436
            $transaction = $db->getTransaction() === null ? $db->beginTransaction() : null;
1437
1438
            $canSaveAll = (!$canSaveAll ? $canSaveAll : $this->beforeSaveAll());
1439
1440
            if ($canSaveAll) {
1441
1442
                if ($runValidation) {
1443
1444
                    if ($this->hasChanges()) {
1445
1446
                        if (!$hasParentModel) {
1447
                            $this->setChildHasChanges('this');
1448
                            $this->setChildOldValues('this', $this->getResetDataForFailedSave());
1449
                        }
1450
1451
                        $canSaveAll = $this->validate();
1452
                        if (!$canSaveAll) {
1453
                            $errors = $this->getErrors();
1454
                            foreach ($errors as $errorField => $errorDescription) {
1455
                                $this->addActionError($errorDescription, 0, $errorField);
1456
                            }
1457
                        }
1458
                    }
1459
                }
1460
1461
                foreach ($this->attributeValues as $attributeName => $attributeValue) {
1462
                    $this->setChildHasChanges($attributeName);
1463
                    $this->setChildOldValues($attributeName, $attributeValue->getResetDataForFailedSave());
1464
                }
1465
            }
1466
        }
1467
1468
        if ($this->hasActionErrors()) {
1469
            $canSaveAll = false;
1470
        } elseif (!$canSaveAll) {
1471
            $this->addActionError('beforeSaveAllInternal checks failed');
1472
        }
1473
1474
        if (!$canSaveAll) {
1475
            $this->resetChildHasChanges();
1476
            if ($transaction !== null) {
1477
                // cancel the started transaction
1478
                $transaction->rollback();
1479
            }
1480
        } else {
1481
            if ($transaction !== null) {
1482
                $this->setChildOldValues('_transaction_', $transaction);
1483
            }
1484
        }
1485
1486
        return $canSaveAll;
1487
    }
1488
1489
1490
    /**
1491
     * Called by beforeSaveAllInternal on the current model to determine if the whole of saveAll
1492
     * can be processed - this is expected to be replaced in individual models when required
1493
     *
1494
     * @return boolean okay to continue with saveAll
1495
     */
1496
    public function beforeSaveAll()
1497
    {
1498
        return true;
1499
    }
1500
1501
1502
    /**
1503
     * Saves the current record but also loops through defined relationships (if appropriate)
1504
     * to save those as well
1505
     *
1506
     * @param boolean $runValidation
1507
     *        should validations be executed on all models before allowing saveAll()
1508
     * @param boolean $hasParentModel
1509
     *        whether this method was called from the top level or by a parent
1510
     *        If false, it means the method was called at the top level
1511
     * @param boolean $push
1512
     *        is saveAll being pushed onto lazy (un)loaded models as well
1513
     * @return boolean|null
1514
     *        did saveAll() successfully process
1515
     */
1516
    public function saveAll($runValidation = true, $hasParentModel = false, $push = false)
1517
    {
1518
1519
        $this->clearActionErrors();
1520
1521
        if ($this->getReadOnly() && !$hasParentModel) {
1522
1523
            // return failure if we are at the top of the tree and should not be asking to saveAll
1524
            // not allowed to amend or delete
1525
            $message = 'Attempting to saveAll on ' . Tools::getClassName($this) . ' readOnly model';
1526
            //$this->addActionError($message);
1527
            throw new Exception($message);
1528
1529
        } elseif ($this->getReadOnly() && $hasParentModel) {
1530
1531
            $message = 'Skipping saveAll on ' . Tools::getClassName($this) . ' readOnly model';
1532
            $this->addActionWarning($message);
1533
            return true;
1534
1535
        } elseif (!$this->getReadOnly()) {
1536
1537
            if ($this->hasChanges()) {
1538
1539
                if (!$hasParentModel) {
1540
1541
                    // run beforeSaveAll and abandon saveAll() if it returns false
1542
                    if (!$this->beforeSaveAllInternal($runValidation, $hasParentModel, $push)) {
1543
                        return false;
1544
                    }
1545
1546
                    /*
1547
                     * note if validation was required it has already now been executed as part of the beforeSaveAll checks,
1548
                     * so no need to do them again as part of save
1549
                     */
1550
                    $runValidation = false;
1551
                }
1552
1553
                try {
1554
                    $ok = $this->save($runValidation, $hasParentModel, $push, true);
1555
                } catch (\Exception $e) {
1556
                    $ok = false;
1557
                    $this->addActionError($e->getMessage(), $e->getCode());
1558
                    //throw $e;
1559
                }
1560
1561
                if (!$hasParentModel) {
1562
                    if ($ok) {
1563
                        $this->afterSaveAllInternal();
1564
                    } else {
1565
                        $this->afterSaveAllFailedInternal();
1566
                    }
1567
                }
1568
1569
                return $ok;
1570
            }
1571
1572
            return true;
1573
1574
        }
1575
    }
1576
1577
1578
    /**
1579
     * Perform a saveAll() call but push the request down the model map including
1580
     * models that are not currently loaded (perhaps because child models need to
1581
     * pick up new values from parents
1582
     *
1583
     * @param boolean $runValidation
1584
     *        should validations be executed on all models before allowing saveAll()
1585
     * @return boolean|null
1586
     *        did saveAll() successfully process
1587
     */
1588
    public function push($runValidation = true)
1589
    {
1590
        return $this->saveAll($runValidation, false, true);
1591
    }
1592
1593
1594
    /**
1595
     * Save the current objects attributes
1596
     *
1597
     * @param boolean $runValidation
1598
     *        should validations be executed on all models before allowing saveAll()
1599
     * @param boolean $hasParentModel
1600
     *        whether this method was called from the top level or by a parent
1601
     *        If false, it means the method was called at the top level
1602
     * @param boolean $fromSaveAll
1603
     *        has the save() call come from saveAll() or not
1604
     * @return boolean
1605
     *        did save() successfully process
1606
     */
1607
    public function save($runValidation = true, $hasParentModel = false, $push = false, $fromSaveAll = false)
1608
    {
1609
1610
        if ($this->getReadOnly() && !$hasParentModel) {
1611
1612
            // return failure if we are at the top of the tree and should not be asking to saveAll
1613
            // not allowed to amend or delete
1614
            $message = 'Attempting to save on ' . Tools::getClassName($this) . ' readOnly model';
1615
            //$this->addActionError($message);
1616
            throw new Exception($message);
1617
1618
        } elseif ($this->getReadOnly() && $hasParentModel) {
1619
1620
            $message = 'Skipping save on ' . Tools::getClassName($this) . ' readOnly model';
1621
            $this->addActionWarning($message);
1622
            return true;
1623
1624
        }
1625
1626
        $allOk = true;
1627
1628
        if (($this->loaded || ($this->isNewRecord && $this->isNewPrepared)) && $this->changedData) {
1629
1630
            if ($this->entityId === false) {
1631
                throw new Exception('No entity id available for ' . __METHOD__ . '()');
1632
            }
1633
1634
            if (!$this->objectId) {
1635
                throw new Exception('No object id available for ' . __METHOD__ . '()');
1636
            }
1637
1638
            $thisTime = time();
1639
1640
            $attributeDefs = $this->getEntityAttributeList();
1641
1642
            // we do not record modified, modifiedBy, created or createdBy against individual attributes but we will support
1643
            // automatically updating them if these attributeNames have been setup as their own attributes for this entity
1644
1645
            if (\Yii::$app->has('user')) {
1646
                try {
1647
                    if (\Yii::$app->user->isGuest) {
1648
                        $userId = 0;
1649
                    } else {
1650
                        $userId = \Yii::$app->user->getId();
1651
                    }
1652
                } catch (InvalidConfigException $e) {
1653
                    if ($e->getMessage() == 'User::identityClass must be set.') {
1654
                        $userId = 0;
1655
                    } else {
1656
                        throw $e;
1657
                    }
1658
                }
1659
            }
1660
1661
            $extraChangeFields = array();
1662
            if (array_key_exists('modifiedAt', $attributeDefs)) {
1663
                if (!array_key_exists('modifiedAt', $this->changedData)) {
1664
                    $exists = array_key_exists('modifiedAt', $this->data);
1665
                    $this->changedData['modifiedAt'] = (array_key_exists('modifiedAt', $this->data) ? $this->data['modifiedAt'] : Tools::DATE_TIME_DB_EMPTY);
1666
                    $this->data['modifiedAt'] = date(Tools::DATETIME_DATABASE, $thisTime);
1667
                    if ($this->lazyAttributes && array_key_exists('modifiedAt', $this->lazyAttributes)) {
1668
                        unset($this->lazyAttributes['modifiedAt']);
1669
                    }
1670
                }
1671
            }
1672
1673
            if (array_key_exists('modifiedBy', $attributeDefs)) {
1674
                if (!array_key_exists('modifiedBy', $this->changedData)) {
1675
                    if (!isset($this->data['modifiedBy']) || $this->data['modifiedBy'] != $userId) {
1676
                        $this->changedData['modifiedBy'] = (array_key_exists('modifiedBy', $this->data) ? $this->data['modifiedBy'] : 0);
1677
                        $this->data['modifiedBy'] = $userId;
1678
                        if ($this->lazyAttributes && array_key_exists('modifiedBy', $this->lazyAttributes)) {
1679
                            unset($this->lazyAttributes['modifiedBy']);
1680
                        }
1681
                    }
1682
                }
1683
            }
1684
1685
            if (array_key_exists('createdAt', $attributeDefs)) {
1686
                if (!array_key_exists('createdAt', $this->changedData)) {
1687
                    $exists = array_key_exists('createdAt', $this->data);
1688
                    if (!$exists || ($exists && $this->data['createdAt'] == Tools::DATE_TIME_DB_EMPTY)) {
1689
                        $this->changedData['createdAt'] = (array_key_exists('createdAt', $this->data) ? $this->data['createdAt'] : Tools::DATE_TIME_DB_EMPTY);
1690
                        $this->data['createdAt'] = date(Tools::DATETIME_DATABASE, $thisTime);
1691
                        if ($this->lazyAttributes && array_key_exists('created', $this->lazyAttributes)) {
1692
                            unset($this->lazyAttributes['createdAt']);
1693
                        }
1694
                    }
1695
                }
1696
            }
1697
1698
            if (array_key_exists('createdBy', $attributeDefs)) {
1699
                if (!array_key_exists('createdBy', $this->changedData)) {
1700
                    $exists = array_key_exists('createdBy', $this->data);
1701
                    if (!$exists || ($exists && $this->data['createdBy'] != $userId)) {
1702
                        $this->changedData['createdBy'] = (array_key_exists('createdBy', $this->data) ? $this->data['createdBy'] : 0);
1703
                        $this->data['createdBy'] = $userId;
1704
                        if ($this->lazyAttributes && array_key_exists('createdBy', $this->lazyAttributes)) {
1705
                            unset($this->lazyAttributes['createdBy']);
1706
                        }
1707
                    }
1708
                }
1709
            }
1710
1711
            if (!$this->changedData) {
1712
                $updateColumns = $this->data;
1713
            } else {
1714
                $updateColumns = array();
1715
                foreach ($this->changedData as $field => $value) {
1716
                    $updateColumns[$field] = $this->data[$field];
1717
                }
1718
            }
1719
1720
            foreach ($updateColumns as $attributeName => $attributeValue) {
1721
1722
                $attributeId = 0;
1723
                $attributeDef = (isset($attributeDefs[$attributeName]) ? $attributeDefs[$attributeName] : false);
1724
                if ($attributeDef) {
1725
                    $attributeId = $attributeDef['id'];
1726
                }
1727
1728
                $ok = false;
1729
1730
                if ($attributeId) {
1731
1732
                    $attributeValue = Tools::formatAttributeValue($attributeValue, $attributeDef);
1733
1734
                    if ($attributeDef['deleteOnDefault'] && $attributeValue === Tools::formatAttributeValue($attributeDef['defaultValue'], $attributeDef)) {
1735
1736
                        // value is default so we will remove it from the attribtue table as not required
1737
                        $ok = true;
1738
                        if (array_key_exists($attributeName, $this->attributeValues)) {
1739
                            $ok = $this->attributeValues[$attributeName]->deleteFull(true);
1740
                            if ($ok) {
1741
                                $this->setChildOldValues($attributeName, true, 'deleted');
1742
                            } else {
1743
                                if ($this->attributeValues[$attributeName]->hasActionErrors()) {
1744
                                    $this->mergeActionErrors($this->attributeValues[$attributeName]->getActionErrors());
1745
                                } else {
1746
                                    $this->addActionError('Failed to delete attribute', 0, $attributeName);
1747
                                }
1748
                            }
1749
                        }
1750
1751
                    } else {
1752
1753
                        switch (strtolower($attributeDef['dataType'])) {
1754
                            case 'boolean':
1755
                                $attributeValue = ($attributeValue ? '1' : '0');
1756
                                break;
1757
                            default:
1758
                                break;
1759
                        }
1760
1761
                        if (is_null($attributeValue)) {
1762
                            // typically where null is permitted it will be the default value with deleteOnDefault set, so should have been caught in the deleteOnDefault
1763
                            if ($attributeDef['isNullable']) {
1764
                                $attributeValue = '__NULL__'; // we do not want to allow null in the attribute database so use this string to denote null when it is permitted
1765
                            } else {
1766
                                $attributeValue = '__NULL__'; // needs to be caught elsewhere
1767
                            }
1768
                        }
1769
1770
                        if (!array_key_exists($attributeName, $this->attributeValues)) {
1771
1772
                            $this->attributeValues[$attributeName] = new $this->attributeValuesClass();
1773
                            $this->attributeValues[$attributeName]->entityId = $this->entityId;
1774
                            $this->attributeValues[$attributeName]->attributeId = $attributeId;
1775
1776
                            // this is a new entry that has not been included in the childHasChanges array yet
1777
                            $this->setChildHasChanges($attributeName);
1778
                            $this->setChildOldValues($attributeName, $this->attributeValues[$attributeName]->getResetDataForFailedSave());
1779
                        }
1780
1781
                        if ($this->newObjectId) {
1782
                            $this->attributeValues[$attributeName]->objectId = $this->newObjectId;
1783
                        } else {
1784
                            $this->attributeValues[$attributeName]->objectId = $this->objectId;
1785
                        }
1786
                        $this->attributeValues[$attributeName]->value = $attributeValue;
1787
                        $ok = $this->attributeValues[$attributeName]->save(false, null, true, true);
1788
                        if (!$ok) {
1789
                            if ($this->attributeValues[$attributeName]->hasActionErrors()) {
1790
                                $this->mergeActionErrors($this->attributeValues[$attributeName]->getActionErrors());
1791
                            } else {
1792
                                $this->addActionError('Failed to save attribute', 0, $attributeName);
1793
                            }
1794
                        }
1795
1796
                    }
1797
                }
1798
1799
                if (!$ok) {
1800
                    $allOk = false;
1801
                }
1802
            }
1803
1804
            if ($allOk) {
1805
                $this->changedData = array();
1806
                $this->loaded = true;
1807
                $this->isNewRecord = false;
1808
            }
1809
1810
        }
1811
1812
        if ($allOk && $this->loaded && $this->newObjectId) {
1813
            // we need to update the objectId for all attributes belonging to
1814
            // the current object to a new value taking into account that not
1815
            // all attributes might have been loaded yet, if any.
1816
            foreach ($this->attributeValues as $attributeName => $attributeValue) {
1817
                $this->attributeValues[$attributeName]->objectId = $this->newObjectId;
1818
                $this->attributeValues[$attributeName]->setOldAttribute('objectId', $this->newObjectId);
1819
            }
1820
            $attributeValuesClass = $this->attributeValuesClass;
1821
            $ok = $attributeValuesClass::updateAll(array('objectId' => $this->newObjectId), array('objectId' => $this->objectId));
1822
            $this->objectId = $this->newObjectId;
1823
            $this->newObjectId = false;
1824
        }
1825
1826
        return $allOk;
1827
1828
    }
1829
1830
    /**
1831
     * This method is called at the end of a successful saveAll()
1832
     * The default implementation will trigger an [[EVENT_AFTER_SAVE_ALL]] event
1833
     * When overriding this method, make sure you call the parent implementation so that
1834
     * the event is triggered.
1835
     *
1836
     * @param boolean $hasParentModel
1837
     *        whether this method was called from the top level or by a parent
1838
     *        If false, it means the method was called at the top level
1839
     */
1840
    public function afterSaveAllInternal($hasParentModel = false)
1841
    {
1842
        /** @var \yii\db\Transaction $transaction */
1843
        $transaction = $this->getChildOldValues('_transaction_');
1844
        if ($transaction) {
1845
            $transaction->commit();
1846
        }
1847
1848
        if ($this->getReadOnly()) {
1849
            // will have been ignored during saveAll()
1850
        } else {
1851
1852
            // remove any deleted attributeValues
1853
            foreach ($this->attributeValues as $attributeName => $attributeValue) {
1854
                if ($this->getChildHasChanges($attributeName)) {
1855
                    if ($this->getChildOldValues($attributeName, 'deleted')) {
1856
                        unset($this->attributeValues[$attributeName]);
1857
                    }
1858
                }
1859
            }
1860
1861
            $this->afterSaveAll();
1862
1863
        }
1864
1865
        $this->resetChildHasChanges();
1866
1867
        if (!$hasParentModel) {
1868
            //$this->trigger(self::EVENT_AFTER_SAVE_ALL);
1869
        }
1870
    }
1871
1872
1873
    /**
1874
     * Called by afterSaveAllInternal on the current model once the whole of the saveAll() has
1875
     * been successfully processed
1876
     */
1877
    public function afterSaveAll()
1878
    {
1879
1880
    }
1881
1882
1883
    /**
1884
     * This method is called at the end of a failed saveAll()
1885
     * The default implementation will trigger an [[EVENT_AFTER_SAVE_ALL_FAILED]] event
1886
     * When overriding this method, make sure you call the parent implementation so that
1887
     * the event is triggered.
1888
     *
1889
     * @param boolean $hasParentModel
1890
     *        whether this method was called from the top level or by a parent
1891
     *        If false, it means the method was called at the top level
1892
     */
1893
    public function afterSaveAllFailedInternal($hasParentModel = false)
1894
    {
1895
        /** @var \yii\db\Transaction $transaction */
1896
        $transaction = $this->getChildOldValues('_transaction_');
1897
        if ($transaction) {
1898
            $transaction->rollback();
1899
        }
1900
1901
        if ($this->getReadOnly()) {
1902
            // will have been ignored during saveAll()
1903
        } else {
1904
1905
            foreach ($this->attributeValues as $attributeName => $attributeValue) {
1906
                if ($this->getChildHasChanges($attributeName)) {
1907
                    $attributeValue->resetOnFailedSave($this->getChildOldValues($attributeName));
1908
                }
1909
            }
1910
1911
            if (!$hasParentModel) {
1912
                if ($this->getChildHasChanges('this')) {
1913
                    $this->resetOnFailedSave($this->getChildOldValues('this'));
1914
                }
1915
            }
1916
1917
            // any model specific actions to carry out
1918
            $this->afterSaveAllFailed();
1919
1920
        }
1921
1922
        $this->resetChildHasChanges();
1923
1924
        if (!$hasParentModel) {
1925
            //$this->trigger(self::EVENT_AFTER_SAVE_ALL_FAILED);
1926
        }
1927
    }
1928
1929
1930
    /**
1931
     * Called by afterSaveAllFailedInternal on the current model once saveAll() has failed
1932
     */
1933
    public function afterSaveAllFailed()
1934
    {
1935
1936
    }
1937
1938
1939
    /**
1940
     * Obtain data required to reset current record to state before saveAll() was called in the event
1941
     * that saveAll() fails
1942
     * @return array array of data required to rollback the current model
1943
     */
1944
    public function getResetDataForFailedSave()
1945
    {
1946
        return array('new' => $this->getIsNewRecord(), 'oldValues' => $this->getOldAttributes(), 'current' => $this->getAttributes());
1947
    }
1948
1949
1950
    /**
1951
     * Reset current record to state before saveAll() was called in the event
1952
     * that saveAll() fails
1953
     * @param array $data array of data required to rollback the current model
1954
     */
1955
    public function resetOnFailedSave($data)
1956
    {
1957
        $this->setAttributes($data['current'], false);
1958
        $this->setIsNewRecord($data['new']);
1959
        $tempValue = $data['oldValues'];
1960
        $this->setOldAttributes($tempValue ? $tempValue : null);
1961
1962
        if ($data['new']) {
1963
            $this->loaded = false;
1964
            $this->attributeValues = array();
1965
        }
1966
1967
    }
1968
1969
1970
    public function hasErrors($attribute = null)
1971
    {
1972
        return $attribute === null ? !empty($this->errors) : isset($this->errors[$attribute]);
1973
    }
1974
1975
1976
    public function getErrors($attribute = null)
1977
    {
1978
        if ($attribute === null) {
1979
            return $this->errors === null ? array() : $this->errors;
1980
        } else {
1981
            return isset($this->errors[$attribute]) ? $this->errors[$attribute] : array();
1982
        }
1983
    }
1984
1985
1986
    public function getFirstErrors()
1987
    {
1988
        if (empty($this->errors)) {
1989
            return array();
1990
        } else {
1991
            $errors = array();
1992
            foreach ($this->errors as $attributeErrors) {
1993
                if (isset($attributeErrors[0])) {
1994
                    $errors[] = $attributeErrors[0];
1995
                }
1996
            }
1997
        }
1998
        return $errors;
1999
    }
2000
2001
2002
    public function getFirstError($attribute)
2003
    {
2004
        return isset($this->errors[$attribute]) ? reset($this->errors[$attribute]) : null;
2005
    }
2006
2007
2008
    /**
2009
     * Adds a new error for the specified attribute.
2010
     * @param string $attribute attribute name
2011
     * @param string $error new error message
2012
     */
2013
    public function addError($attribute, $error = '')
2014
    {
2015
        $this->errors[$attribute][] = $error;
2016
    }
2017
2018
2019
    /**
2020
     * Removes errors for all attributes or a single attribute.
2021
     * @param string $attribute attribute name. null will remove errors for all attribute.
2022
     */
2023
    public function clearErrors($attribute = null)
2024
    {
2025
        if ($attribute === null) {
2026
            $this->errors = array();
2027
        } else {
2028
            unset($this->errors[$attribute]);
2029
        }
2030
    }
2031
2032
}
2033