Completed
Push — master ( 028c16...c9b957 )
by Andrii
18s
created

Collection::afterLoad()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
ccs 0
cts 3
cp 0
cc 1
eloc 2
nc 1
nop 0
crap 2
1
<?php
2
/**
3
 * ActiveRecord for API
4
 *
5
 * @link      https://github.com/hiqdev/yii2-hiart
6
 * @package   yii2-hiart
7
 * @license   BSD-3-Clause
8
 * @copyright Copyright (c) 2015-2017, HiQDev (http://hiqdev.com/)
9
 */
10
11
namespace hiqdev\hiart;
12
13
use Closure;
14
use Yii;
15
use yii\base\Component;
16
use yii\base\InvalidCallException;
17
use yii\base\InvalidConfigException;
18
use yii\base\InvalidValueException;
19
use yii\base\Model;
20
use yii\base\ModelEvent;
21
use yii\helpers\ArrayHelper;
22
23
/**
24
 * Class Collection manages the collection of the models.
25
 *
26
 * @var ActiveRecord[] the array of models in the collection
27
 */
28
class Collection extends Component
29
{
30
    const EVENT_BEFORE_INSERT   = 'beforeInsert';
31
    const EVENT_BEFORE_UPDATE   = 'beforeUpdate';
32
    const EVENT_BEFORE_VALIDATE = 'beforeValidate';
33
    const EVENT_AFTER_VALIDATE  = 'afterValidate';
34
    const EVENT_AFTER_SAVE      = 'afterSave';
35
    const EVENT_BEFORE_LOAD     = 'beforeLoad';
36
    const EVENT_AFTER_LOAD      = 'afterLoad';
37
    const EVENT_BEFORE_DELETE   = 'beforeDelete';
38
    const EVENT_AFTER_DELETE    = 'afterDelete';
39
40
    /**
41
     * @var boolean Whether to check, that all [[$models]] are instance of the same class
42
     * @see isConsistent
43
     */
44
    public $checkConsistency = true;
45
46
    /**
47
     * @var ActiveRecord[] array of models
48
     */
49
    protected $models = [];
50
51
    /**
52
     * @var string the name of the form. Sets automatically on [[set()]]
53
     * @see set()
54
     */
55
    public $formName;
56
57
    /**
58
     * @var callable the function to format loaded data. Gets three attributes:
59
     *               - model (instance of operating model)
60
     *               - key   - the key of the loaded item
61
     *               - value - the value of the loaded item
62
     * Should return array, where the first item is the new key, and the second - a new value. Example:
63
     * ```
64
     * return [$key, $value];
65
     * ```
66
     */
67
    public $loadFormatter;
68
69
    /**
70
     * @var ActiveRecord the template model instance. May be set manually by [[setModel()]] or
71
     * automatically on [[set()]] call
72
     * @see setModel()
73
     * @see set()
74
     */
75
    protected $model;
76
77
    /**
78
     * @var array options that will be passed to the new model when loading data in [[load]]
79
     * @see load()
80
     */
81
    public $modelOptions = [];
82
83
    /**
84
     * @var array Options that will be passed to [[ActiveRecord::query()]] method as third argument.
85
     * @see ActiveRecord::query()
86
     */
87
    public $queryOptions = [];
88
89
    /**
90
     * @var ActiveRecord the first model of the set. Fills automatically by [[set()]]
91
     * @see set()
92
     */
93
    public $first;
94
95
    /**
96
     * @var array the model's attributes that will be saved
97
     */
98
    public $attributes;
99
100
    /**
101
     * @var Closure a closure that will used to collect data from [[models]] before saving.
102
     * Signature:
103
     * ```php
104
     * function ($model, $collection)
105
     * ```
106
     *
107
     * Method must return array of two elements:
108
     *  - 0: key of the model in resulting array
109
     *  - 1: corresponding value
110
     *
111
     * @see collectData
112
     */
113
    public $dataCollector;
114
115
116
    public function init()
117
    {
118
        if (!isset($this->queryOptions['batch'])) {
119
            $this->queryOptions['batch'] = true;
120
        }
121
    }
122
123
    /**
124
     * Sets the model of the collection.
125
     * @param ActiveRecord|array $model if the model is an instance of [[Model]] - sets it, otherwise - creates the model
126
     * using given options array
127
     * @return object|ActiveRecord
128
     */
129
    public function setModel($model)
130
    {
131
        if ($model instanceof Model) {
132
            $this->model = $model;
133
        } else {
134
            $this->model = Yii::createObject($model);
135
        }
136
137
        $model = $this->model;
138
        $this->updateFormName();
139
140
        if (empty($this->getScenario())) {
141
            $this->setScenario($model->scenario);
142
        }
143
144
        return $this->model;
145
    }
146
147
    /**
148
     * Returns the [[model]].
149
     * @return ActiveRecord
150
     */
151
    public function getModel()
152
    {
153
        return $this->model;
154
    }
155
156
    public function getIds()
157
    {
158
        $ids = [];
159
        foreach ($this->models as $model) {
160
            $ids[] = $model->getPrimaryKey();
161
        }
162
163
        return $ids;
164
    }
165
166
    /**
167
     * @return ActiveRecord[] models
168
     */
169
    public function getModels()
170
    {
171
        return $this->models;
172
    }
173
174
    /**
175
     * Sets the scenario of the default model.
176
     * @param $value string scenario
177
     */
178
    public function setScenario($value)
179
    {
180
        $this->modelOptions['scenario'] = $value;
181
    }
182
183
    /**
184
     * Gets the scenario the default model.
185
     * @return string the scenario
186
     */
187
    public function getScenario()
188
    {
189
        return $this->modelOptions['scenario'];
190
    }
191
192
    /**
193
     * Updates [[formName]] from the current [[model]].
194
     * @return string the form name
195
     */
196
    public function updateFormName()
197
    {
198
        if (!($this->model instanceof Model)) {
199
            throw new InvalidCallException('The model should be set first');
200
        }
201
202
        return $this->formName = $this->model->formName();
203
    }
204
205
    /**
206
     * We can load data from 3 different structures:.
207
     * 1) POST: [
208
     *     'ModelName' => [
209
     *         'attribute1' => 'value1',
210
     *         'attribute2' => 'value2'
211
     *     ]
212
     * ]
213
     * 2) POST: [
214
     *      'ModelName' => [
215
     *          1   => [
216
     *              'attribute1' => 'value1',
217
     *              'attribute2' => 'value2'
218
     *          ],
219
     *          2   => [
220
     *              ...
221
     *          ]
222
     *      ]
223
     * }
224
     * 3) foreach ($selection as $id) {
225
     *      $res[$id] = [reset($model->primaryKey()) => $id];
226
     *    }.
227
     * @param array|callable $data - the data to be proceeded.
228
     *                             If is callable - gets arguments:
229
     *                             - model
230
     *                             - fromName
231
     * @throws InvalidConfigException
232
     * @return Collection
233
     */
234
    public function load($data = null)
235
    {
236
        $models    = [];
237
        $finalData = [];
238
239
        if ($data === null) {
240
            $data = Yii::$app->request->post();
241
242
            if (isset($data[$this->formName])) {
243
                $data = $data[$this->formName];
244
245
                $is_batch = true;
246
                foreach ($data as $k => $v) {
247
                    if (!is_array($v)) {
248
                        $is_batch = false;
249
                        break;
250
                    }
251
                }
252
253
                if (!$is_batch) {
254
                    $data = [$data];
255
                }
256
            } elseif ($data['selection']) {
257
                $res = [];
258
                foreach ($data['selection'] as $id) {
259
                    $res[$id] = [reset($this->model->primaryKey()) => $id];
0 ignored issues
show
Bug introduced by
$this->model->primaryKey() cannot be passed to reset() as the parameter $array expects a reference.
Loading history...
260
                }
261
                $data = $res;
262
            }
263
        } elseif ($data instanceof Closure) {
264
            $data = call_user_func($data, $this->model, $this->formName);
265
        }
266
267
        foreach ($data as $key => $value) {
268
            if ($this->loadFormatter instanceof Closure) {
269
                $item = call_user_func($this->loadFormatter, $this->model, $key, $value);
270
                $key  = $item[0];
271
            } else {
272
                $item = [$key, $value];
273
            }
274
            $options      = ArrayHelper::merge(['class' => $this->model->className()], $this->modelOptions);
275
            $models[$key] = Yii::createObject($options);
276
277
            $finalData[$this->formName][$key] = $item[1];
278
        }
279
        $this->model->loadMultiple($models, $finalData);
280
281
        return $this->set($models);
282
    }
283
284
    /**
285
     * Sets the array of AR models to the collection.
286
     * @param array|ActiveRecord $models - array of AR Models or a single model
287
     * @return $this
288
     */
289
    public function set($models)
290
    {
291
        if ($models instanceof ActiveRecord) {
292
            $models = [$models];
293
        }
294
295
        $first = reset($models);
296
        if ($first === false) {
297
            return $this;
298
        }
299
        $this->first = $first;
300
301
        $this->formName = $first->formName();
302
        $this->model    = $this->setModel($first);
303
        $this->models   = $models;
304
305
        if ($this->checkConsistency && !$this->isConsistent()) {
306
            throw new InvalidValueException('Models are not objects of same class or not follow same operation');
307
        }
308
309
        return $this;
310
    }
311
312
    /**
313
     * Saves the current collection.
314
     * This method will call [[insert()]] or [[update()]].
315
     * @param bool  $runValidation whether to perform validation before saving the collection
316
     * @param array $attributes    list of attribute names that need to be saved. Defaults to null,
317
     *                             meaning all attributes that are loaded will be saved. If the scenario is specified, will use only
318
     *                             fields from the scenario
319
     * @param array $options       the array of options that will be passed to [[insert]] or [[update]] methods to override
320
     *                             model parameters
321
     * @return bool whether the saving succeeds
322
     */
323
    public function save($runValidation = true, $attributes = null, $options = [])
324
    {
325
        if ($this->isEmpty()) {
326
            throw new InvalidCallException('Collection is empty, nothing to save');
327
        }
328
        $options = array_merge($this->queryOptions, $options);
329
330
        if ($this->first->getIsNewRecord()) {
331
            return $this->insert($runValidation, $attributes, $options);
332
        } else {
333
            return $this->update($runValidation, $attributes, $options);
334
        }
335
    }
336
337
    public function insert($runValidation = true, $attributes = null, array $queryOptions = [])
338
    {
339
        if (!$attributes) {
340
            $attributes = $this->attributes ?: $this->first->activeAttributes();
341
        }
342
        if ($runValidation && !$this->validate($attributes)) {
343
            return false;
344
        }
345
        if (!$this->beforeSave(true)) {
346
            return false;
347
        }
348
349
        $data    = $this->collectData($attributes);
350
        $results = $this->first->batchQuery('create', $data, $queryOptions);
351
        $pk      = $this->first->primaryKey()[0];
352
        foreach ($this->models as $key => $model) {
353
            $values = &$data[$key];
354
            $result = &$results[$key];
355
356
            $model->{$pk} = $result['id'];
357
            if ($pk !== 'id') {
358
                $values[$pk] = $result['id'];
359
            }
360
            $changedAttributes = array_fill_keys(array_keys($values), null);
361
            $model->setOldAttributes($values);
362
            $model->afterSave(true, $changedAttributes);
363
        }
364
365
        $this->afterSave();
366
367
        return true;
368
    }
369
370
    public function update($runValidation = true, $attributes = null, array $queryOptions = [])
371
    {
372
        if (!$attributes) {
373
            $attributes = $this->attributes ?: $this->first->activeAttributes();
374
        }
375
        if ($runValidation && !$this->validate($attributes)) {
376
            return false;
377
        }
378
        if (!$this->beforeSave()) {
379
            return false;
380
        }
381
382
        $data    = $this->collectData($attributes);
383
        $results = $this->first->batchQuery('update', $data, $queryOptions);
0 ignored issues
show
Unused Code introduced by
$results is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
384
385
        foreach ($this->models as $key => $model) {
386
            $changedAttributes = [];
387
            $values            = array_key_exists($key, $data) ? $data[$key] : $data[$model->id]; /// XXX not good
0 ignored issues
show
Documentation introduced by
The property id does not exist on object<hiqdev\hiart\ActiveRecord>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
388
            foreach ($values as $name => $value) {
389
                $changedAttributes[$name] = $model->getOldAttribute($name);
390
                $model->setOldAttribute($name, $value);
391
            }
392
            $model->afterSave(false, $changedAttributes);
393
        }
394
395
        $this->afterSave();
396
397
        return true;
398
    }
399
400
    public function delete()
401
    {
402
        if (!$this->beforeDelete()) {
403
            return false;
404
        }
405
406
        $data    = $this->collectData();
407
        $results = $this->first->batchQuery('delete', $data);
408
409
        $this->afterDelete();
410
411
        return $results;
412
    }
413
414
    /**
415
     * Collects data from the stored models.
416
     * @param string|array $attributes list of attributes names
417
     * @return array
418
     */
419
    public function collectData($attributes = null)
420
    {
421
        $data = [];
422
        foreach ($this->models as $model) {
423
            if ($this->dataCollector instanceof Closure) {
424
                list($key, $row) = call_user_func($this->dataCollector, $model, $this);
425
            } else {
426
                $key = $model->getPrimaryKey();
427
                $row = $model->getAttributes($attributes);
0 ignored issues
show
Bug introduced by
It seems like $attributes defined by parameter $attributes on line 419 can also be of type string; however, yii\base\Model::getAttributes() does only seem to accept array|null, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
428
            }
429
430
            if ($key) {
431
                $data[$key] = $row;
432
            } else {
433
                $data[] = $row;
434
            }
435
        }
436
437
        return $data;
438
    }
439
440
    /**
441
     * Whether one of models has an error.
442
     * @return bool
443
     */
444
    public function hasErrors()
445
    {
446
        foreach ($this->models as $model) {
447
            if ($model->hasErrors()) {
448
                return true;
449
            }
450
        }
451
452
        return false;
453
    }
454
455
    /**
456
     * Returns the first error of the collection.
457
     * @return bool|mixed
458
     */
459
    public function getFirstError()
460
    {
461
        foreach ($this->models as $model) {
462
            if ($model->hasErrors()) {
463
                $errors = $model->getFirstErrors();
464
465
                return array_shift($errors);
466
            }
467
        }
468
469
        return false;
470
    }
471
472
    public function count()
473
    {
474
        return is_array($this->models) ? count($this->models) : 0;
475
    }
476
477
    public function validate($attributes = null)
478
    {
479
        if (!$this->beforeValidate()) {
480
            return false;
481
        }
482
483
        if (!$this->first->validateMultiple($this->models, $attributes)) {
484
            return false;
485
        }
486
487
        $this->afterValidate();
488
489
        return true;
490
    }
491
492
    public function beforeValidate()
493
    {
494
        $event = new ModelEvent();
495
        $this->triggerAll(self::EVENT_BEFORE_VALIDATE, $event);
496
497
        return $event->isValid;
498
    }
499
500
    public function afterValidate()
501
    {
502
        $event = new ModelEvent();
503
504
        $this->triggerAll(self::EVENT_AFTER_VALIDATE, $event);
505
506
        return $event->isValid;
507
    }
508
509
    public function beforeSave($insert = false)
510
    {
511
        $event = new ModelEvent();
512
        if ($this->isEmpty()) {
513
            $event->isValid = false;
514
        }
515
        $this->triggerAll($insert ? self::EVENT_BEFORE_INSERT : self::EVENT_BEFORE_UPDATE, $event);
516
517
        return $event->isValid;
518
    }
519
520
    public function afterSave()
521
    {
522
        $this->triggerAll(self::EVENT_AFTER_SAVE);
523
    }
524
525
    public function beforeLoad()
526
    {
527
        $event = new ModelEvent();
528
        $this->trigger(self::EVENT_BEFORE_LOAD, $event);
529
530
        return $event->isValid;
531
    }
532
533
    public function afterLoad()
534
    {
535
        $this->trigger(self::EVENT_AFTER_LOAD);
536
    }
537
538
    public function beforeDelete()
539
    {
540
        $event = new ModelEvent();
541
        $this->trigger(self::EVENT_BEFORE_DELETE, $event);
542
543
        return $event->isValid;
544
    }
545
546
    public function afterDelete()
547
    {
548
        $this->trigger(self::EVENT_AFTER_DELETE);
549
    }
550
551
    /**
552
     * Iterates over all of the models and triggers some event.
553
     * @param string     $name  the event name
554
     * @param ModelEvent $event
555
     * @return bool whether is valid
556
     */
557
    public function triggerModels($name, ModelEvent $event = null)
558
    {
559
        if ($event === null) {
560
            $event = new ModelEvent();
561
        }
562
        foreach ($this->models as $model) {
563
            $model->trigger($name, $event);
564
        }
565
566
        return $event->isValid;
567
    }
568
569
    /**
570
     * Calls [[triggerModels()]], then calls [[trigger()]].
571
     * @param string     $name  the event name
572
     * @param ModelEvent $event
573
     * @return bool whether is valid
574
     */
575
    public function triggerAll($name, ModelEvent $event = null)
576
    {
577
        if ($event === null) {
578
            $event = new ModelEvent();
579
        }
580
        if ($this->triggerModels($name, $event)) {
581
            $this->trigger($name, $event);
582
        }
583
584
        return $event->isValid;
585
    }
586
587
    public function isConsistent()
588
    {
589
        $new       = $this->first->getIsNewRecord();
590
        $className = $this->first->className();
591
        foreach ($this->models as $model) {
592
            if ($new !== $model->getIsNewRecord() || $className !== $model->className()) {
593
                return false;
594
            }
595
        }
596
597
        return true;
598
    }
599
600
    public function isEmpty()
601
    {
602
        return empty($this->models);
603
    }
604
}
605