ActiveRecordArray::setAutoCreateObjectOnNewKey()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
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 fangface\Tools;
18
use fangface\base\traits\ActionErrors;
19
use fangface\db\ActiveAttributeRecord;
20
use fangface\db\ActiveRecord;
21
use fangface\db\ActiveRecordArrayException;
22
use fangface\db\ActiveRecordParentalInterface;
23
use fangface\db\ActiveRecordParentalTrait;
24
use fangface\db\ActiveRecordReadOnlyInterface;
25
use fangface\db\ActiveRecordReadOnlyTrait;
26
use fangface\db\ActiveRecordSaveAllInterface;
27
use fangface\db\Exception;
28
use yii\base\ModelEvent;
29
use yii\db\ActiveRecord as YiiActiveRecord;
30
31
class ActiveRecordArray extends \ArrayObject implements ActiveRecordParentalInterface, ActiveRecordReadOnlyInterface, ActiveRecordSaveAllInterface
32
{
33
34
    use ActionErrors;
35
    use ActiveRecordParentalTrait;
36
    use ActiveRecordReadOnlyTrait;
37
38
    /**
39
     * @var string|object|false|null object class for use by $this->newElement()
40
     */
41
    protected $defaultObjectClass       = false;
42
43
    /**
44
     * @var boolean can elements automatically be created in the array when
45
     * a new key is used e.g. $myObject['key1]->myVar1 = 'myVal'; without first setting up $myObject['key1]
46
     * if false an exception will be thrown
47
     */
48
    protected $autoCreateObjectOnNewKey = true;
49
50
    /**
51
     * @event ModelEvent an event that is triggered before saveAll()
52
     * You may set [[ModelEvent::isValid]] to be false to stop the update.
53
     */
54
    const EVENT_BEFORE_SAVE_ALL = 'beforeSaveAll';
55
56
    /**
57
     * @event Event an event that is triggered after saveAll() has completed
58
     */
59
    const EVENT_AFTER_SAVE_ALL = 'afterSaveAll';
60
61
    /**
62
     * @event Event an event that is triggered after saveAll() has failed
63
     */
64
    const EVENT_AFTER_SAVE_ALL_FAILED = 'afterSaveAllFailed';
65
66
    /**
67
     * @event ModelEvent an event that is triggered before saveAll()
68
     * You may set [[ModelEvent::isValid]] to be false to stop the update.
69
     */
70
    const EVENT_BEFORE_DELETE_FULL = 'beforeDeleteFull';
71
72
    /**
73
     * @event Event an event that is triggered after saveAll() has completed
74
     */
75
    const EVENT_AFTER_DELETE_FULL = 'afterDeleteFull';
76
77
    /**
78
     * @event Event an event that is triggered after saveAll() has failed
79
     */
80
    const EVENT_AFTER_DELETE_FULL_FAILED = 'afterDeleteFullFailed';
81
82
    /**
83
     * Construct
84
     *
85
     * @param string|null|array $input
86
     * @param integer $flags
87
     * @param string $iterator_class
88
     */
89
    public function __construct($input = null, $flags = 0, $iterator_class = 'ArrayIterator')
90
    {
91
        $inp = $input;
92
        $input = (is_null($input) ? array() : $input);
93
94
        parent::__construct($input, $flags, $iterator_class);
95
96
        if (is_array($inp) && $inp) {
97
            $this->isNewRecord = false;
98
        }
99
    }
100
101
102
    /**
103
     * Appends the value
104
     *
105
     * @param ActiveRecord|ActiveAttributeRecord $value
106
     * @return void
107
     */
108
    public function append($value)
109
    {
110
        $this->offsetSet(null, $value);
111
    }
112
113
114
    /**
115
     * Appends the value but with a specific key value
116
     *
117
     * @param ActiveRecord|ActiveAttributeRecord $value
118
     * @param mixed $key
119
     * @return void
120
     */
121
    public function appendWithKey($value, $key)
122
    {
123
        $this->offsetSet($key, $value);
124
    }
125
126
127
    /**
128
     * (non-PHPdoc)
129
     *
130
     * @see ArrayObject::offsetSet()
131
     * @throws ActiveRecordArrayException
132
     */
133
    public function offsetSet($key, $value)
134
    {
135
        if ($this->defaultObjectClass && !($value instanceof $this->defaultObjectClass)) {
136
137
            throw new ActiveRecordArrayException('Item added to array not of type `' . $this->defaultObjectClass . '`' . (is_object($value) ? ' it is of type `' . get_class($value) . '`' : ''));
138
139
        } elseif ($this->getReadOnly()) {
140
141
            throw new ActiveRecordArrayException('Attempting to add an element to a read only array of ' . Tools::getClassName($this->defaultObjectClass));
142
143
        } else {
144
145
            if (is_null($key)) {
146
147
                // assign a temp key and create the new element
148
                $this->newElement($key, $value);
149
150
            } else {
151
152
                if ($this->parentModel && $value instanceof ActiveRecordParentalInterface) {
153
                    $value->setParentModel($this->parentModel);
154
                }
155
156
                if ($value instanceof ActiveRecordReadOnlyInterface) {
157
                    if ($this->readOnly !== null) {
158
                        $value->setReadOnly($this->readOnly);
159
                    }
160
                    if ($this->canDelete !== null) {
161
                        $value->setCanDelete($this->canDelete);
162
                    }
163
                }
164
165
                parent::offsetSet($key, $value);
166
            }
167
        }
168
    }
169
170
171
    /**
172
     * (non-PHPdoc)
173
     * @see ArrayObject::offsetGet()
174
     * @throws ActiveRecordArrayException
175
     */
176
    public function offsetGet($key)
177
    {
178
        if (!$this->offsetExists($key)) {
179
            if ($this->autoCreateObjectOnNewKey) {
180
                $this->newElement($key);
181
            } else {
182
                throw new ActiveRecordArrayException('Undefined index: ' . $key);
183
            }
184
185
        }
186
187
        return parent::offsetGet($key);
188
    }
189
190
191
    /**
192
     * Returns the value at the specified key
193
     *
194
     * @param mixed $key
195
     * @return ActiveRecord ActiveAttributeRecord
196
     */
197
    public function get($key)
198
    {
199
        return $this->offsetGet($key);
200
    }
201
202
203
    /**
204
     * Returns the value at the specified key
205
     *
206
     * @param mixed $key
207
     * @return ActiveRecord|ActiveAttributeRecord
208
     */
209
    public function row($key)
210
    {
211
        return $this->offsetGet($key);
212
    }
213
214
215
    /**
216
     * Create a new entry of the relevant object in the array and return the
217
     * temp array key to that new object
218
     *
219
     * @param mixed $key
220
     *        [OPTIONAL] specify the temp key to be used for the new entry
221
     *        if it already exists then return false. default to assigning a temp key
222
     * @param mixed $value
223
     *        [OPTIONAL] add a specific object rather than creating one
224
     *        based on $this->defaultObjectClass
225
     * @return string false success return the key used to create the new element
226
     */
227
    public function newElement($key = null, $value = null)
228
    {
229
        if (!is_null($key)) {
230
            if ($this->offsetExists($key)) {return false;}
231
        } else {
232
            $counter = 0;
233
            $key = 'temp_' . strval($counter);
234
            while ($this->offsetExists($key)) {
235
                $counter++;
236
                $key = 'temp_' . strval($counter);
237
            }
238
        }
239
240
        if (!is_null($value)) {
241
            $this->offsetSet($key, $value);
242
            return $key;
243
        } elseif (class_exists($this->defaultObjectClass)) {
244
            $value = new $this->defaultObjectClass();
245
            if ($value instanceof $this->defaultObjectClass) {
246
                $this->offsetSet($key, $value);
247
                return $key;
248
            }
249
        }
250
251
        return false;
252
    }
253
254
255
    /**
256
     * Remove existing element
257
     *
258
     * @param mixed $index
259
     *       specify index/key of the array element to remove - this will not delete data from the db
260
     */
261
    public function removeElement($index)
262
    {
263
        $this->offsetUnset($index);
264
    }
265
266
    /**
267
     * Delete existing element if it exists in the db and then Remove existing element from array
268
     *
269
     * @param mixed $index
270
     *       specify index/key of the array element to remove - this will not delete data from the db
271
     * @return boolean Success
272
     */
273
    public function deleteOne($index)
274
    {
275
        if ($this->offsetExists($index)) {
276
            if ($this->offsetGet($index)->delete()) {
277
                $this->offsetUnset($index);
278
                return true;
279
            }
280
        }
281
        return false;
282
    }
283
284
285
    /**
286
     * Perform a saveAll() call but push the request down the model map including
287
     * models that are not currently loaded (perhaps because child models need to
288
     * pick up new values from parents
289
     *
290
     * @param boolean $runValidation
291
     *        should validations be executed on all models before allowing saveAll()
292
     * @return boolean
293
     *        did saveAll() successfully process
294
     */
295
    public function push($runValidation = true)
296
    {
297
        return $this->saveAll($runValidation, false, true);
298
    }
299
300
301
    /**
302
     * This method is called at the beginning of a saveAll() request
303
     *
304
     * @param boolean $runValidation
305
     *        should validations be executed on all models before allowing saveAll()
306
     * @param boolean $hasParentModel
307
     *        whether this method was called from the top level or by a parent
308
     *        If false, it means the method was called at the top level
309
     * @param boolean $push
310
     *        is saveAll being pushed onto lazy (un)loaded models as well
311
     * @return boolean whether the saveAll() method call should continue
312
     *        If false, saveAll() will be cancelled.
313
     */
314
    public function beforeSaveAllInternal($runValidation = true, $hasParentModel = false, $push = false)
315
    {
316
317
        $this->clearActionErrors();
318
        $this->resetChildHasChanges();
319
320
        $canSaveAll = true;
321
322
        if (!$hasParentModel) {
323
            //$event = new ModelEvent;
324
            //$this->trigger(self::EVENT_BEFORE_SAVE_ALL, $event);
325
            //$canSaveAll = $event->isValid;
326
        }
327
328
        if ($this->getReadOnly()) {
329
            // will be ignored during saveAll()
330
        } else {
331
332
            if ($canSaveAll) {
333
334
                if ($runValidation) {
335
336
                }
337
338
                if ($this->count()) {
339
340
                    $iterator = $this->getIterator();
341
                    while ($iterator->valid()) {
342
343
                        $isReadOnly = false;
344
                        if ($iterator->current() instanceof ActiveRecordReadOnlyInterface) {
345
                            $isReadOnly = $iterator->current()->getReadOnly();
346
                        }
347
348
                        if (!$isReadOnly) {
349
350
                            $needsCheck = true;
351
                            if ($iterator->current() instanceof ActiveRecordSaveAllInterface) {
352
                                $needsCheck = $iterator->current()->hasChanges(true);
353
                            } elseif (method_exists($iterator->current(), 'getDirtyAttributes')) {
354
                                if ($iterator->current()->getDirtyAttributes()) {
355
                                    $needsCheck = true;
356
                                }
357
                            }
358
359
                            if ($needsCheck) {
360
361
                                $this->setChildHasChanges($iterator->key());
362
                                if ($iterator->current() instanceof ActiveRecordSaveAllInterface) {
363
                                    $this->setChildOldValues($iterator->key(), $iterator->current()->getResetDataForFailedSave());
364
                                } else {
365
                                    $this->setChildOldValues(
366
                                        $iterator->key(),
367
                                        array(
368
                                            'new' => $iterator->current()->getIsNewRecord(),
369
                                            'oldValues' => $iterator->current()->getOldAttributes(),
370
                                            'current' => $iterator->current()->getAttributes()
371
                                        )
372
                                    );
373
                                }
374
375
                                $canSaveThis = true;
376
                                if ($iterator->current() instanceof ActiveRecordSaveAllInterface) {
377
                                    $canSaveThis = $iterator->current()->beforeSaveAllInternal($runValidation, true, $push);
378
                                    if (!$canSaveThis) {
379
                                        if (method_exists($iterator->current(), 'hasActionErrors')) {
380
                                            if ($iterator->current()->hasActionErrors()) {
381
                                                $this->mergeActionErrors($iterator->current()->getActionErrors());
382
                                            }
383
                                        }
384
                                    }
385
                                } elseif (method_exists($iterator->current(), 'validate')) {
386
                                    $canSaveThis = $iterator->current()->validate();
387
                                    if (!$canSaveThis) {
388
                                        $errors = $iterator->current()->getErrors();
389
                                        foreach ($errors as $errorField => $errorDescription) {
390
                                            $this->addActionError($errorDescription, 0, $errorField, Tools::getClassName($iterator->current()));
391
                                        }
392
                                    }
393
                                }
394
395
                                if (!$canSaveThis) {
396
                                    $canSaveAll = false;
397
                                }
398
                            }
399
                        }
400
401
                        $iterator->next();
402
                    }
403
                }
404
            }
405
        }
406
407
        if ($this->hasActionErrors()) {
408
            $canSaveAll = false;
409
        } elseif (!$canSaveAll) {
410
            $this->addActionError('beforeSaveAllInternal checks failed');
411
        }
412
413
        if (!$canSaveAll) {
414
            $this->resetChildHasChanges();
415
        }
416
417
        return $canSaveAll;
418
419
    }
420
421
422
    /**
423
     * Saves all models in the array but also loops through defined
424
     * relationships (if appropriate) to save those as well
425
     *
426
     * @param boolean $runValidation
427
     *        should validations be executed on all models before allowing saveAll()
428
     * @param boolean $hasParentModel
429
     *        whether this method was called from the top level or by a parent
430
     *        If false, it means the method was called at the top level
431
     * @param boolean $push
432
     *        is saveAll being pushed onto lazy (un)loaded models as well
433
     * @return boolean
434
     *        did saveAll() successfully process
435
     */
436
    public function saveAll($runValidation = true, $hasParentModel = false, $push = false)
437
    {
438
439
        $this->clearActionErrors();
440
441
        $allOk = true;
442
443
        if ($hasParentModel && $this->getReadOnly()) {
444
445
            // not allowed to amend or delete but is a child model so we will treat as okay without deleting the record
446
            $this->addActionWarning('Skipped saveAll on ' . Tools::getClassName($this->defaultObjectClass) . '(s) which is read only');
447
            $allOk = true;
448
449
        } elseif (!$hasParentModel && $this->getReadOnly()) {
450
451
            // not allowed to amend
452
            throw new Exception('Attempting to saveAll on ' . Tools::getClassName($this->defaultObjectClass) . '(s) which is read only');
453
454
        } else {
455
456
            if ($this->count()) {
457
458
                if (!$hasParentModel) {
459
460
                    // run beforeSaveAll and abandon saveAll() if it returns false
461
                    if (!$this->beforeSaveAllInternal($runValidation, $hasParentModel, $push)) {
462
                        return false;
463
                    }
464
465
                    /*
466
                     * note if validation was required it has already now been executed as part of the beforeSaveAll checks,
467
                    * so no need to do them again as part of save
468
                    */
469
                    $runValidation = false;
470
471
                }
472
473
                $alterKeys = array();
474
                $savedKeys = array();
475
476
                $iterator = $this->getIterator();
477
                while ($iterator->valid()) {
478
479
                    $hasChanges = true;
480
                    if ($iterator->current() instanceof ActiveRecordSaveAllInterface) {
481
                        $hasChanges = $iterator->current()->hasChanges(true);
482
                    } elseif (method_exists($iterator->current(), 'getDirtyAttributes')) {
483
                        if (!$iterator->current()->getDirtyAttributes()) {
484
                            $hasChanges = false;
485
                        }
486
                    }
487
488
                    if ($hasChanges) {
489
490
                        $isNewRecord = $iterator->current()->getIsNewRecord();
491
492
                        $ok = false;
493
                        if ($iterator->current() instanceof ActiveRecordSaveAllInterface) {
494
                            $ok = $iterator->current()->saveAll($runValidation, true, $push, true);
495
                        } elseif (method_exists($iterator->current(), 'save')) {
496
                            $ok = $iterator->current()->save($runValidation);
497
                            if ($ok) {
498
                                $iterator->current()->setIsNewRecord(false);
499
                            }
500
                        }
501
502
                        if (method_exists($iterator->current(), 'hasActionErrors')) {
503
                            if ($iterator->current()->hasActionErrors()) {
504
                                $this->mergeActionErrors($iterator->current()->getActionErrors());
505
                            }
506
                        }
507
508
                        if (method_exists($iterator->current(), 'hasActionWarnings')) {
509
                            if ($iterator->current()->hasActionWarnings()) {
510
                                $this->mergeActionWarnings($iterator->current()->getActionWarnings());
511
                            }
512
                        }
513
514
                        if (!$ok) {
515
516
                            $allOk = false;
517
518
                        } else {
519
520
                            $primaryKey = $iterator->current()->getPrimaryKey();
521
                            if (!is_array($primaryKey) && $primaryKey) {
522
                                $savedKeys[] = array_merge(array('key' => $primaryKey), $this->getChildOldValues($iterator->key()));
523
                            }
524
525
                            if ($isNewRecord) {
526
527
                                if (!is_array($primaryKey) && $primaryKey && $primaryKey != $iterator->key()) {
528
                                    $alterKeys[$iterator->key()] = $primaryKey;
529
                                }
530
                            }
531
                        }
532
                    }
533
534
                    $iterator->next();
535
                }
536
537
                if ($alterKeys) {
538
                    foreach ($alterKeys as $oldKey => $newKey) {
539
                        $this->offsetSet($newKey, $this->offsetGet($oldKey));
540
                        $this->offsetUnset($oldKey);
541
                    }
542
                }
543
544
                if ($savedKeys) {
545
                    // need to update childHasChanges flags ready for afterSaveAllInternal()
546
                    $this->resetChildHasChanges();
547
                    foreach ($savedKeys as $key => $value) {
548
                        $tempKey = $value['key'];
549
                        unset($value['key']);
550
                        $this->setChildHasChanges($tempKey);
551
                        $this->setChildOldValues(
552
                            $tempKey,
553
                            $value
554
                        );
555
556
                    }
557
                }
558
559
                if (!$hasParentModel) {
560
                    if ($allOk) {
561
                        $this->afterSaveAllInternal();
562
                    } else {
563
                        $this->afterSaveAllFailedInternal();
564
                    }
565
                }
566
567
            }
568
        }
569
570
        if (!$allOk) {
571
            return false;
572
        }
573
574
        return true;
575
    }
576
577
578
    /**
579
     * This method is called at the end of a successful saveAll()
580
     * The default implementation will trigger an [[EVENT_AFTER_SAVE_ALL]] event
581
     * When overriding this method, make sure you call the parent implementation so that
582
     * the event is triggered.
583
     *
584
     * @param boolean $hasParentModel
585
     *        whether this method was called from the top level or by a parent
586
     *        If false, it means the method was called at the top level
587
     */
588
    public function afterSaveAllInternal($hasParentModel = false)
589
    {
590
591
        if ($this->getReadOnly()) {
592
            // will have been ignored during saveAll()
593
        } else {
594
595
            if ($this->count()) {
596
597
                $iterator = $this->getIterator();
598
                while ($iterator->valid()) {
599
600
                    if ($this->getChildHasChanges($iterator->key())) {
601
                        if ($iterator->current() instanceof ActiveRecordSaveAllInterface) {
602
                            $iterator->current()->afterSaveAllInternal(true);
603
                        } elseif ($iterator->current() instanceof YiiActiveRecord && method_exists($iterator->current(), 'afterSaveAll')) {
604
                            $iterator->current()->afterSaveAll();
605
                        }
606
                    }
607
608
                    $iterator->next();
609
                }
610
            }
611
        }
612
613
        $this->resetChildHasChanges();
614
615
        if (!$hasParentModel) {
616
            //$this->trigger(self::EVENT_AFTER_SAVE_ALL);
617
        }
618
    }
619
620
621
    /**
622
     * This method is called at the end of a failed saveAll()
623
     * The default implementation will trigger an [[EVENT_AFTER_SAVE_ALL_FAILED]] event
624
     * When overriding this method, make sure you call the parent implementation so that
625
     * the event is triggered.
626
     *
627
     * @param boolean $hasParentModel
628
     *        whether this method was called from the top level or by a parent
629
     *        If false, it means the method was called at the top level
630
     */
631
    public function afterSaveAllFailedInternal($hasParentModel = false)
632
    {
633
634
        if ($this->getReadOnly()) {
635
            // will have been ignored during saveAll()
636
        } else {
637
638
            if ($this->count()) {
639
640
                $iterator = $this->getIterator();
641
                while ($iterator->valid()) {
642
643
                    if ($this->getChildHasChanges($iterator->key())) {
644
645
                        if ($iterator->current() instanceof ActiveRecordSaveAllInterface) {
646
                            $iterator->current()->resetOnFailedSave($this->getChildOldValues($iterator->key()));
647
                        } elseif ($iterator->current() instanceof YiiActiveRecord) {
648
                            $iterator->current()->setAttributes($this->getChildOldValues($iterator->key(), 'current'), false);
649
                            $iterator->current()->setIsNewRecord($this->getChildOldValues($iterator->key(), 'new'));
650
                            $tempValue = $this->getChildOldValues($iterator->key(), 'oldValues');
651
                            $iterator->current()->setOldAttributes($tempValue ? $tempValue : null);
652
                        }
653
654
                        if ($iterator->current() instanceof ActiveRecordSaveAllInterface) {
655
                            $iterator->current()->afterSaveAllFailedInternal(true);
656
                        } elseif ($iterator->current() instanceof YiiActiveRecord && method_exists($iterator->current(), 'afterSaveAllFailed')) {
657
                            $iterator->current()->afterSaveAllFailed();
658
                        }
659
                    }
660
661
                    $iterator->next();
662
                }
663
            }
664
        }
665
666
        $this->resetChildHasChanges();
667
668
        if (!$hasParentModel) {
669
            //$this->trigger(self::EVENT_AFTER_SAVE_ALL_FAILED);
670
        }
671
    }
672
673
674
    /**
675
     * Loops through the current array of objects and delete them
676
     *
677
     * @see \yii\db\BaseActiveRecord::delete()
678
     */
679
    public function deleteFull($hasParentModel = false)
680
    {
681
682
        $this->clearActionErrors();
683
684
        $allOk = true;
685
686
        if ($hasParentModel && ($this->getReadOnly() || !$this->getCanDelete())) {
687
688
            // not allowed to amend or delete but is a child model so we will treat as okay without deleting the record
689
            $this->addActionWarning('Skipped delete of ' . Tools::getClassName($this->defaultObjectClass) . '(s)' . ' which is ' . ($this->getReadOnly() ? 'read only' : 'flagged as not deletable'));
690
            $allOk = true;
691
692
        } elseif (!$hasParentModel && ($this->getReadOnly() || !$this->getCanDelete())) {
693
694
            // not allowed to delete
695
            throw new Exception('Attempting to delete ' . Tools::getClassName($this->defaultObjectClass) . '(s)' . ($this->getReadOnly() ? ' readOnly model' : ' model flagged as not deletable'));
696
697
        } else {
698
699
            if ($this->count()) {
700
701
                if (!$hasParentModel) {
702
                    // run beforeDeleteFull and abandon deleteFull() if it returns false
703
                    if (!$this->beforeDeleteFullInternal($hasParentModel)) {
704
                        return false;
705
                    }
706
                }
707
708
                $iterator = $this->getIterator();
709
                while ($iterator->valid()) {
710
711
                    $ok = false;
712
                    if ($iterator->current() instanceof ActiveRecordSaveAllInterface) {
713
                        $ok = $iterator->current()->deleteFull(true);
714
                    } elseif (method_exists($iterator->current(), 'delete')) {
715
                        $ok = $iterator->current()->delete();
716
                    }
717
718
                    if (method_exists($iterator->current(), 'hasActionErrors')) {
719
                        if ($iterator->current()->hasActionErrors()) {
720
                            $this->mergeActionErrors($iterator->current()->getActionErrors());
721
                        }
722
                    }
723
724
                    if (method_exists($iterator->current(), 'hasActionWarnings')) {
725
                        if ($iterator->current()->hasActionWarnings()) {
726
                            $this->mergeActionWarnings($iterator->current()->getActionWarnings());
727
                        }
728
                    }
729
730
                    if (!$ok) {
731
                        $allOk = false;
732
                    }
733
734
                    $iterator->next();
735
                }
736
737
                if (!$hasParentModel) {
738
                    if ($allOk) {
739
                        $this->afterDeleteFullInternal();
740
                    } else {
741
                        $this->afterDeleteFullFailedInternal();
742
                    }
743
                }
744
745
            }
746
        }
747
748
        return $allOk;
749
    }
750
751
752
    /**
753
     * This method is called at the beginning of a deleteFull() request on a record array
754
     *
755
     * @param boolean $hasParentModel
756
     *        whether this method was called from the top level or by a parent
757
     *        If false, it means the method was called at the top level
758
     * @return boolean whether the deleteFull() method call should continue
759
     *        If false, deleteFull() will be cancelled.
760
     */
761
    public function beforeDeleteFullInternal($hasParentModel = false)
762
    {
763
        $this->clearActionErrors();
764
        $this->resetChildHasChanges();
765
766
        $canDeleteFull = true;
767
768
        if (!$hasParentModel) {
769
            //$event = new ModelEvent;
770
            //$this->trigger(self::EVENT_BEFORE_SAVE_ALL, $event);
771
            //$canSaveAll = $event->isValid;
772
        }
773
774
        if ($this->getReadOnly()) {
775
            // will be ignored during deleteFull()
776
        } elseif (!$this->getCanDelete()) {
777
            // will be ignored during deleteFull()
778
        } else {
779
780
            if ($canDeleteFull) {
781
782
                if ($this->count()) {
783
784
                    $iterator = $this->getIterator();
785
                    while ($iterator->valid()) {
786
787
                        $isReadOnly = false;
788
                        $canDelete = true;
789
                        if ($iterator->current() instanceof ActiveRecordReadOnlyInterface) {
790
                            $isReadOnly = $iterator->current()->getReadOnly();
791
                            $canDelete = $iterator->current()->getCanDelete();
792
                        }
793
794
                        if (!$isReadOnly && $canDelete) {
795
796
                            $this->setChildHasChanges($iterator->key());
797
                            if ($iterator->current() instanceof ActiveRecordSaveAllInterface) {
798
                                $this->setChildOldValues($iterator->key(), $iterator->current()->getResetDataForFailedSave());
799
                            } else {
800
                                $this->setChildOldValues(
801
                                    $iterator->key(),
802
                                    array(
803
                                        'new' => $iterator->current()->getIsNewRecord(),
804
                                        'oldValues' => $iterator->current()->getOldAttributes(),
805
                                        'current' => $iterator->current()->getAttributes()
806
                                    )
807
                                );
808
                            }
809
810
                            $canDeleteThis = true;
811
                            if ($iterator->current() instanceof ActiveRecordSaveAllInterface) {
812
                                $canDeleteThis = $iterator->current()->beforeDeleteFullInternal(true);
813
                                if (!$canDeleteThis) {
814
                                    if (method_exists($iterator->current(), 'hasActionErrors')) {
815
                                        if ($iterator->current()->hasActionErrors()) {
816
                                            $this->mergeActionErrors($iterator->current()->getActionErrors());
817
                                        }
818
                                    }
819
                                }
820
                            } elseif (method_exists($iterator->current(), 'beforeDeleteFull')) {
821
                                $canDeleteThis = $iterator->current()->beforeDeleteFull();
822
                                if (!$canDeleteThis) {
823
                                    $errors = $iterator->current()->getErrors();
824
                                    foreach ($errors as $errorField => $errorDescription) {
825
                                        $this->addActionError($errorDescription, 0, $errorField, Tools::getClassName($iterator->current()));
826
                                    }
827
                                }
828
                            }
829
830
                            if (!$canDeleteThis) {
831
                                $canDeleteFull = false;
832
                            }
833
834
                        }
835
836
                        $iterator->next();
837
                    }
838
                }
839
            }
840
        }
841
842
        if ($this->hasActionErrors()) {
843
            $canDeleteFull = false;
844
        } elseif (!$canDeleteFull) {
845
            $this->addActionError('beforeDeleteFullInternal checks failed');
846
        }
847
848
        if (!$canDeleteFull) {
849
            $this->resetChildHasChanges();
850
        }
851
852
        return $canDeleteFull;
853
    }
854
855
856
    /**
857
     * This method is called at the end of a successful deleteFull()
858
     *
859
     * @param boolean $hasParentModel
860
     *        whether this method was called from the top level or by a parent
861
     *        If false, it means the method was called at the top level
862
     */
863
    public function afterDeleteFullInternal($hasParentModel = false)
864
    {
865
        if ($this->getReadOnly()) {
866
            // will have been ignored during deleteFull()
867
        } elseif (!$this->getCanDelete()) {
868
            // will have been ignored during deleteFull()
869
        } else {
870
871
            if ($this->count()) {
872
873
                $iterator = $this->getIterator();
874
                while ($iterator->valid()) {
875
876
                    if ($this->getChildHasChanges($iterator->key())) {
877
                        if ($iterator->current() instanceof ActiveRecordSaveAllInterface) {
878
                            $iterator->current()->afterDeleteFullInternal(true);
879
                        } elseif ($iterator->current() instanceof YiiActiveRecord && method_exists($iterator->current(), 'afterDeleteFull')) {
880
                            $iterator->current()->afterDeleteFull();
0 ignored issues
show
Bug introduced by
The method afterDeleteFull() does not exist on yii\db\ActiveRecord. Did you maybe mean delete()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
881
                        }
882
                    }
883
884
                    $iterator->next();
885
                }
886
887
                $this->exchangeArray(array());
888
            }
889
        }
890
891
        $this->resetChildHasChanges();
892
893
        if (!$hasParentModel) {
894
            //$this->trigger(self::EVENT_AFTER_SAVE_ALL);
895
        }
896
897
    }
898
899
900
    /**
901
     * This method is called at the end of a failed deleteFull()
902
     *
903
     * @param boolean $hasParentModel
904
     *        whether this method was called from the top level or by a parent
905
     *        If false, it means the method was called at the top level
906
     */
907
    public function afterDeleteFullFailedInternal($hasParentModel = false)
908
    {
909
        if ($this->getReadOnly()) {
910
            // will have been ignored during deleteFull()
911
        } elseif (!$this->getCanDelete()) {
912
            // will have been ignored during deleteFull()
913
        } else {
914
915
            if ($this->count()) {
916
917
                $iterator = $this->getIterator();
918
                while ($iterator->valid()) {
919
920
                    if ($this->getChildHasChanges($iterator->key())) {
921
922
                        if ($iterator->current() instanceof ActiveRecordSaveAllInterface) {
923
                            $iterator->current()->resetOnFailedSave($this->getChildOldValues($iterator->key()));
924
                        } elseif ($iterator->current() instanceof YiiActiveRecord) {
925
                            $iterator->current()->setAttributes($this->getChildOldValues($iterator->key(), 'current'), false);
926
                            $iterator->current()->setIsNewRecord($this->getChildOldValues($iterator->key(), 'new'));
927
                            $tempValue = $this->getChildOldValues($iterator->key(), 'oldValues');
928
                            $iterator->current()->setOldAttributes($tempValue ? $tempValue : null);
929
                        }
930
931
                        if ($iterator->current() instanceof ActiveRecordSaveAllInterface) {
932
                            $iterator->current()->afterDeleteFullFailedInternal(true);
933
                        } elseif ($iterator->current() instanceof YiiActiveRecord && method_exists($iterator->current(), 'afterDeleteFullFailed')) {
934
                            $iterator->current()->afterDeleteFullFailed();
0 ignored issues
show
Bug introduced by
The method afterDeleteFullFailed() does not exist on yii\db\ActiveRecord. Did you maybe mean delete()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
935
                        }
936
                    }
937
938
                    $iterator->next();
939
                }
940
            }
941
        }
942
943
        $this->resetChildHasChanges();
944
945
        if (!$hasParentModel) {
946
            //$this->trigger(self::EVENT_AFTER_SAVE_ALL_FAILED);
947
        }
948
949
    }
950
951
952
    /**
953
     * Return all objects in the array as arrays but do not load children
954
     *
955
     * @return array
956
     */
957
    public function toArray(array $fields = [], array $expand = [], $recursive = true)
958
    {
959
        $data = array();
960
        if ($this->count()) {
961
            $iterator = $this->getIterator();
962
            while ($iterator->valid()) {
963
                if (method_exists($iterator->current(), 'toArray')) {
964
                    $data[$iterator->key()] = $iterator->current()->toArray($fields, $expand, $recursive);
965
                }
966
                $iterator->next();
967
            }
968
        }
969
        return $data;
970
    }
971
972
973
    /**
974
     * Return all objects in the array as arrays including their children
975
     * if applicable
976
     *
977
     * @param boolean $loadedOnly [OPTIONAL] only show populated relations default is false
978
     * @param boolean $excludeNewAndBlankRelations [OPTIONAL] exclude new blank records, default true
979
     * @return array
980
     */
981
    public function allToArray($loadedOnly=false, $excludeNewAndBlankRelations=true)
982
    {
983
        $data = array();
984
        if ($this->count()) {
985
            $iterator = $this->getIterator();
986
            while ($iterator->valid()) {
987
                $excludeFromArray = false;
988
                if ($excludeNewAndBlankRelations && $iterator->current()->getIsNewRecord()) {
989
                    if ($iterator->current() instanceof ActiveRecordSaveAllInterface) {
990
                        if (!$iterator->current()->hasChanges(true)) {
991
                            $excludeFromArray = true;
992
                        }
993
                    } elseif (method_exists($iterator->current(), 'getDirtyAttributes')) {
994
                        if (!$iterator->current()->getDirtyAttributes()) {
995
                            $excludeFromArray = true;
996
                        }
997
                    }
998
                }
999
                if ($excludeFromArray) {
1000
                    // exclude
1001
                } elseif (method_exists($iterator->current(), 'allToArray')) {
1002
                    $data[$iterator->key()] = $iterator->current()->allToArray($loadedOnly, $excludeNewAndBlankRelations);
1003
                } elseif (method_exists($iterator->current(), 'toArray')) {
1004
                    $data[$iterator->key()] = $iterator->current()->toArray(array(), array(), false);
1005
                }
1006
                $iterator->next();
1007
            }
1008
        }
1009
        return $data;
1010
    }
1011
1012
1013
    /**
1014
     * Set the default object class for use by $this->newElement()
1015
     *
1016
     * @param string $class
1017
     */
1018
    public function setDefaultObjectClass($class)
1019
    {
1020
        $this->defaultObjectClass = $class;
1021
    }
1022
1023
1024
    /**
1025
     * Set parent model
1026
     *
1027
     * @param ActiveRecord|YiiActiveRecord $parentModel
1028
     */
1029
    public function setParentModel($parentModel)
1030
    {
1031
        $this->parentModel = $parentModel;
1032
        if ($this->count()) {
1033
            $iterator = $this->getIterator();
1034
            while ($iterator->valid()) {
1035
                if ($iterator->current() instanceof ActiveRecordParentalInterface) {
1036
                    $iterator->current()->setParentModel($parentModel);
1037
                }
1038
                $iterator->next();
1039
            }
1040
        }
1041
    }
1042
1043
1044
    /**
1045
     * Set the read only value
1046
     *
1047
     * @param boolean $value [OPTIONAL] default true
1048
     */
1049
    public function setReadOnly($value=true)
1050
    {
1051
        $this->readOnly = $value;
1052
        if ($this->count()) {
1053
            $iterator = $this->getIterator();
1054
            while ($iterator->valid()) {
1055
                if ($iterator->current() instanceof ActiveRecordReadOnlyInterface) {
1056
                    $iterator->current()->setReadOnly($this->readOnly);
1057
                }
1058
                $iterator->next();
1059
            }
1060
        }
1061
    }
1062
1063
1064
    /**
1065
     * Set the can delete value
1066
     *
1067
     * @param boolean $value [OPTIONAL] default true
1068
     */
1069
    public function setCanDelete($value=true)
1070
    {
1071
        $this->canDelete = $value;
1072
        if ($this->count()) {
1073
            $iterator = $this->getIterator();
1074
            while ($iterator->valid()) {
1075
                if ($iterator->current() instanceof ActiveRecordReadOnlyInterface) {
1076
                    $iterator->current()->setCanDelete($this->canDelete);
1077
                }
1078
                $iterator->next();
1079
            }
1080
        }
1081
    }
1082
1083
1084
    /**
1085
     * Set if a new object should be created when an array element does not yes exist
1086
     *
1087
     * @param boolean $value [OPTIONAL] default true (which is also the default if the method is not used)
1088
     */
1089
    public function setAutoCreateObjectOnNewKey($value=true)
1090
    {
1091
        $this->autoCreateObjectOnNewKey = $value;
1092
    }
1093
1094
1095
    /**
1096
     * Set attribute in each of the models within the array
1097
     *
1098
     * @param string $attributeName
1099
     *        name of attribute to be updated
1100
     * @param mixed $value
1101
     *        value to be set
1102
     * @param boolean $onlyIfChanged
1103
     *        limit setting the value to records that have changes already
1104
     *        helps avoid saving new otherwise empty records to the db
1105
     * @param boolean $onlyIfNew
1106
     *        limit setting the value to new records only
1107
     */
1108
    public function setAttribute($attributeName, $value, $onlyIfChanged = false, $onlyIfNew = false)
1109
    {
1110
        if ($this->count()) {
1111
            $iterator = $this->getIterator();
1112
            while ($iterator->valid()) {
1113
                if (method_exists($iterator->current(), 'setAttribute')) {
1114
1115
                    if ($value == '__KEY__') {
1116
                        $value = $iterator->key();
1117
                    }
1118
1119
                    $hasChanges = true;
1120
1121
                    if ($onlyIfNew) {
1122
                        $hasChanges = $iterator->current()->getIsNewRecord();
1123
                    }
1124
1125
                    if ($hasChanges && $onlyIfChanged) {
1126
                        if ($iterator->current() instanceof ActiveRecordSaveAllInterface) {
1127
                            $hasChanges = $iterator->current()->hasChanges(true);
1128
                        } elseif (method_exists($iterator->current(), 'getDirtyAttributes')) {
1129
                            if (!$iterator->current()->getDirtyAttributes()) {
1130
                                $hasChanges = false;
1131
                            }
1132
                        }
1133
                    }
1134
1135
                    if ($hasChanges) {
1136
                        if ($iterator->current()->getAttribute($attributeName) != $value) {
1137
                            $iterator->current()->setAttribute($attributeName, $value);
1138
                        }
1139
                    }
1140
1141
                }
1142
                $iterator->next();
1143
            }
1144
        }
1145
    }
1146
1147
1148
    /**
1149
     * Determine if any of the models in the array have any unsaved changed
1150
     *
1151
     * @param boolean $checkRelations should changes in relations be checked as well
1152
     * @return boolean
1153
     */
1154
    public function hasChanges($checkRelations=false)
1155
    {
1156
        $hasChanges = false;
1157
        if ($this->count()) {
1158
            $iterator = $this->getIterator();
1159
            while (!$hasChanges && $iterator->valid()) {
1160
                if ($iterator->current() instanceof ActiveRecordSaveAllInterface) {
1161
                    $hasChanges = $iterator->current()->hasChanges($checkRelations);
1162
                } elseif (method_exists($iterator->current(), 'getDirtyAttributes')) {
1163
                    if ($iterator->current()->getDirtyAttributes()) {
1164
                        $hasChanges = true;
1165
                    }
1166
                }
1167
                $iterator->next();
1168
            }
1169
        }
1170
        return ($hasChanges ? true : false);
1171
    }
1172
1173
1174
    /**
1175
     * Call a function on each of the models in the array collection
1176
     *
1177
     * @param string $methodName
1178
     * @param mixed $parameters
1179
     * @param boolean $asArray
1180
     *        call method with $parameters as the first parameter rather than as one parameter per array element
1181
     */
1182
    public function callMethodOnEachObjectInArray($methodName, $parameters = false, $asArray = true)
1183
    {
1184
        if ($this->count()) {
1185
            $iterator = $this->getIterator();
1186
            while ($iterator->valid()) {
1187
1188
                if (method_exists($iterator->current(), $methodName)) {
1189
                    if (!$parameters || $parameters === null) {
1190
                        $iterator->current()->$methodName();
1191
                    } else {
1192
                        if ($asArray) {
1193
                            call_user_func(array(
1194
                                $iterator->current(),
1195
                                $methodName
1196
                            ), $parameters);
1197
                        } else {
1198
                            call_user_func_array(array(
1199
                                $iterator->current(),
1200
                                $methodName
1201
                            ), $parameters);
1202
                        }
1203
                    }
1204
                }
1205
                $iterator->next();
1206
            }
1207
        }
1208
    }
1209
1210
1211
    /**
1212
     * Call the debugTest method on all objects in the model map (used for testing)
1213
     *
1214
     * @param boolean $loadedOnly [OPTIONAL] only include populated relations default is false
1215
     * @param boolean $excludeNewAndBlankRelations [OPTIONAL] exclude new blank records, default true
1216
     * @return array
1217
     */
1218
    public function callDebugTestOnAll($loadedOnly=false, $excludeNewAndBlankRelations=true)
1219
    {
1220
        $data = array();
1221
        if ($this->count()) {
1222
            $iterator = $this->getIterator();
1223
            while ($iterator->valid()) {
1224
                $excludeFromArray = false;
1225
                if ($excludeNewAndBlankRelations && $iterator->current()->getIsNewRecord()) {
1226
                    if ($iterator->current() instanceof ActiveRecordSaveAllInterface) {
1227
                        if (!$iterator->current()->hasChanges(true)) {
1228
                            $excludeFromArray = true;
1229
                        }
1230
                    } elseif (method_exists($iterator->current(), 'getDirtyAttributes')) {
1231
                        if (!$iterator->current()->getDirtyAttributes()) {
1232
                            $excludeFromArray = true;
1233
                        }
1234
                    }
1235
                }
1236
                if ($excludeFromArray) {
1237
                    // exclude
1238
                } elseif (method_exists($iterator->current(), 'callDebugTestOnAll')) {
1239
                    $data[$iterator->key()] = $iterator->current()->callDebugTestOnAll($loadedOnly, $excludeNewAndBlankRelations);
1240
                } elseif (method_exists($iterator->current(), 'debugTest')) {
1241
                    $data[$iterator->key()] = $iterator->current()->debugTest();
1242
                }
1243
                $iterator->next();
1244
            }
1245
        }
1246
        return $data;
1247
    }
1248
1249
1250
    /**
1251
     * Allows use of array_* functions on this object array
1252
     * <code>
1253
     * <?php
1254
     * $yourObject->array_keys();
1255
     * ?>
1256
     * </code>
1257
     *
1258
     * @param string $func
1259
     * @param mixed[] $argv
1260
     * @throws ActiveRecordArrayException
1261
     * @return mixed
1262
     */
1263
    public function __call($func, $argv)
1264
    {
1265
        if (!is_callable($func) || substr($func, 0, 6) !== 'array_') {throw new ActiveRecordArrayException(__CLASS__ . '->' . $func . ' does not exist');}
1266
        return call_user_func_array($func, array_merge(array(
1267
            $this->getArrayCopy()
1268
        ), $argv));
1269
    }
1270
1271
1272
    /**
1273
     * Required to meet the needs of ActiveRecordSaveAllInterface but not used at this level
1274
     * @see ActiveRecordSaveAllInterface
1275
     */
1276
    public function save($runValidation = true, $attributes = null, $hasParentModel = false, $fromSaveAll = false)
1277
    {
1278
        return $this->saveAll($runValidation, $hasParentModel);
1279
    }
1280
1281
1282
    /**
1283
     * Required to meet the needs of ActiveRecordSaveAllInterface but not used at this level
1284
     * @see ActiveRecordSaveAllInterface
1285
     */
1286
    public function beforeSaveAll()
1287
    {
1288
        return true;
1289
    }
1290
1291
1292
    /**
1293
     * Required to meet the needs of ActiveRecordSaveAllInterface but not used at this level
1294
     * @see ActiveRecordSaveAllInterface
1295
     */
1296
    public function afterSaveAll()
1297
    {
1298
1299
    }
1300
1301
1302
    /**
1303
     * Required to meet the needs of ActiveRecordSaveAllInterface but not used at,
1304
     * this level, each object in the array will be processed by afterSaveAllFailedInternal()
1305
     */
1306
    public function afterSaveAllFailed()
1307
    {
1308
1309
    }
1310
1311
1312
    /**
1313
     * Obtain data required to reset current record to state before saveAll() was called in the event
1314
     * that saveAll() fails
1315
     * @return array array of data required to rollback the current model
1316
     */
1317
    public function getResetDataForFailedSave()
1318
    {
1319
1320
    }
1321
1322
1323
    /**
1324
     * Required to meet the needs of ActiveRecordSaveAllInterface but not used at this level
1325
     * @see ActiveRecordSaveAllInterface
1326
     */
1327
    public function resetOnFailedSave($data)
1328
    {
1329
1330
    }
1331
1332
    /**
1333
     * Required to meet the needs of ActiveRecordSaveAllInterface but not used at this level
1334
     * @see ActiveRecordSaveAllInterface
1335
     */
1336
    public function delete($hasParentModel = false, $fromDeleteFull = false)
1337
    {
1338
        return $this->deleteFull($hasParentModel);
1339
    }
1340
1341
    /**
1342
     * Required to meet the needs of ActiveRecordSaveAllInterface but not used at this level
1343
     * @see ActiveRecordSaveAllInterface
1344
     */
1345
    public function beforeDeleteFull()
1346
    {
1347
        return true;
1348
    }
1349
1350
1351
    /**
1352
     * Required to meet the needs of ActiveRecordSaveAllInterface but not used at this level
1353
     * @see ActiveRecordSaveAllInterface
1354
     */
1355
    public function afterDeleteFull()
1356
    {
1357
1358
    }
1359
1360
1361
    /**
1362
     * Required to meet the needs of ActiveRecordSaveAllInterface but not used at,
1363
     * this level, each object in the array will be processed by afterSaveAllFailedInternal()
1364
     */
1365
    public function afterDeleteFullFailed()
1366
    {
1367
1368
    }
1369
1370
}
1371