Completed
Pull Request — master (#11)
by Klochok
04:23
created

Collection::insert()   D

Complexity

Conditions 9
Paths 24

Size

Total Lines 32
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 90

Importance

Changes 0
Metric Value
dl 0
loc 32
ccs 0
cts 28
cp 0
rs 4.909
c 0
b 0
f 0
cc 9
eloc 21
nc 24
nop 3
crap 90
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
     * Perform query options
85
     * @var array
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
     * Sets the model of the collection.
117
     * @param ActiveRecord|array $model if the model is an instance of [[Model]] - sets it, otherwise - creates the model
118
     * using given options array
119
     * @return object|ActiveRecord
120
     */
121
    public function setModel($model)
122
    {
123
        if ($model instanceof Model) {
124
            $this->model = $model;
125
        } else {
126
            $this->model = Yii::createObject($model);
127
        }
128
129
        $model = $this->model;
130
        $this->updateFormName();
131
132
        if (empty($this->getScenario())) {
133
            $this->setScenario($model->scenario);
134
        }
135
136
        return $this->model;
137
    }
138
139
    /**
140
     * Returns the [[model]].
141
     * @return ActiveRecord
142
     */
143
    public function getModel()
144
    {
145
        return $this->model;
146
    }
147
148
    public function getIds()
149
    {
150
        $ids = [];
151
        foreach ($this->models as $model) {
152
            $ids[] = $model->getPrimaryKey();
153
        }
154
155
        return $ids;
156
    }
157
158
    /**
159
     * @return ActiveRecord[] models
160
     */
161
    public function getModels()
162
    {
163
        return $this->models;
164
    }
165
166
    /**
167
     * Sets the scenario of the default model.
168
     * @param $value string scenario
169
     */
170
    public function setScenario($value)
171
    {
172
        $this->modelOptions['scenario'] = $value;
173
    }
174
175
    /**
176
     * Gets the scenario the default model.
177
     * @return string the scenario
178
     */
179
    public function getScenario()
180
    {
181
        return $this->modelOptions['scenario'];
182
    }
183
184
    /**
185
     * Updates [[formName]] from the current [[model]].
186
     * @return string the form name
187
     */
188
    public function updateFormName()
189
    {
190
        if (!($this->model instanceof Model)) {
191
            throw new InvalidCallException('The model should be set first');
192
        }
193
194
        return $this->formName = $this->model->formName();
195
    }
196
197
    /**
198
     * We can load data from 3 different structures:.
199
     * 1) POST: [
200
     *     'ModelName' => [
201
     *         'attribute1' => 'value1',
202
     *         'attribute2' => 'value2'
203
     *     ]
204
     * ]
205
     * 2) POST: [
206
     *      'ModelName' => [
207
     *          1   => [
208
     *              'attribute1' => 'value1',
209
     *              'attribute2' => 'value2'
210
     *          ],
211
     *          2   => [
212
     *              ...
213
     *          ]
214
     *      ]
215
     * }
216
     * 3) foreach ($selection as $id) {
217
     *      $res[$id] = [reset($model->primaryKey()) => $id];
218
     *    }.
219
     * @param array|callable $data - the data to be proceeded.
220
     *                             If is callable - gets arguments:
221
     *                             - model
222
     *                             - fromName
223
     * @throws InvalidConfigException
224
     * @return Collection
225
     */
226
    public function load($data = null)
227
    {
228
        $models    = [];
229
        $finalData = [];
230
231
        if ($data === null) {
232
            $data = Yii::$app->request->post();
233
234
            if (isset($data[$this->formName])) {
235
                $data = $data[$this->formName];
236
237
                $is_batch = true;
238
                foreach ($data as $k => $v) {
239
                    if (!is_array($v)) {
240
                        $is_batch = false;
241
                        break;
242
                    }
243
                }
244
245
                if (!$is_batch) {
246
                    $data = [$data];
247
                }
248
            } elseif ($data['selection']) {
249
                $res = [];
250
                foreach ($data['selection'] as $id) {
251
                    $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...
252
                }
253
                $data = $res;
254
            }
255
        } elseif ($data instanceof Closure) {
256
            $data = call_user_func($data, $this->model, $this->formName);
257
        }
258
259
        foreach ($data as $key => $value) {
260
            if ($this->loadFormatter instanceof Closure) {
261
                $item = call_user_func($this->loadFormatter, $this->model, $key, $value);
262
                $key  = $item[0];
263
            } else {
264
                $item = [$key, $value];
265
            }
266
            $options      = ArrayHelper::merge(['class' => $this->model->className()], $this->modelOptions);
267
            $models[$key] = Yii::createObject($options);
268
269
            $finalData[$this->formName][$key] = $item[1];
270
        }
271
        $this->model->loadMultiple($models, $finalData);
272
273
        return $this->set($models);
274
    }
275
276
    /**
277
     * Sets the array of AR models to the collection.
278
     * @param array|ActiveRecord $models - array of AR Models or a single model
279
     * @return $this
280
     */
281
    public function set($models)
282
    {
283
        if ($models instanceof ActiveRecord) {
284
            $models = [$models];
285
        }
286
287
        $first = reset($models);
288
        if ($first === false) {
289
            return $this;
290
        }
291
        $this->first = $first;
292
293
        $this->formName = $first->formName();
294
        $this->model    = $this->setModel($first);
295
        $this->models   = $models;
296
297
        if ($this->checkConsistency && !$this->isConsistent()) {
298
            throw new InvalidValueException('Models are not objects of same class or not follow same operation');
299
        }
300
301
        return $this;
302
    }
303
304
    /**
305
     * Saves the current collection.
306
     * This method will call [[insert()]] or [[update()]].
307
     * @param bool  $runValidation whether to perform validation before saving the collection
308
     * @param array $attributes    list of attribute names that need to be saved. Defaults to null,
309
     *                             meaning all attributes that are loaded will be saved. If the scenario is specified, will use only
310
     *                             fields from the scenario
311
     * @param array $options       the array of options that will be passed to [[insert]] or [[update]] methods to override
312
     *                             model parameters
313
     * @return bool whether the saving succeeds
314
     */
315
    public function save($runValidation = true, $attributes = null, $options = [])
316
    {
317
        if ($this->isEmpty()) {
318
            throw new InvalidCallException('Collection is empty, nothing to save');
319
        }
320
        $options = array_merge($options, $this->queryOptions);
321
322
        if ($this->first->getIsNewRecord()) {
323
            return $this->insert($runValidation, $attributes, $options);
324
        } else {
325
            return $this->update($runValidation, $attributes, $options);
326
        }
327
    }
328
329
    public function insert($runValidation = true, $attributes = null, array $options = [])
330
    {
331
        if (!$attributes) {
332
            $attributes = $this->attributes ?: $this->first->activeAttributes();
333
        }
334
        if ($runValidation && !$this->validate($attributes)) {
335
            return false;
336
        }
337
        if (!$this->beforeSave(true)) {
338
            return false;
339
        }
340
341
        $data    = $this->collectData($attributes);
342
        $results = $this->isBatch($options) ? $this->first->batchQuery('create', $data, $options) : $this->first->query('create', reset($data), $options);
343
        $pk      = $this->first->primaryKey()[0];
344
        foreach ($this->models as $key => $model) {
345
            $values = &$data[$key];
346
            $result = &$results[$key];
347
348
            $model->{$pk} = $result['id'];
349
            if ($pk !== 'id') {
350
                $values[$pk] = $result['id'];
351
            }
352
            $changedAttributes = array_fill_keys(array_keys($values), null);
353
            $model->setOldAttributes($values);
354
            $model->afterSave(true, $changedAttributes);
355
        }
356
357
        $this->afterSave();
358
359
        return true;
360
    }
361
362
    public function update($runValidation = true, $attributes = null, array $options = [])
363
    {
364
        if (!$attributes) {
365
            $attributes = $this->attributes ?: $this->first->activeAttributes();
366
        }
367
        if ($runValidation && !$this->validate($attributes)) {
368
            return false;
369
        }
370
        if (!$this->beforeSave()) {
371
            return false;
372
        }
373
374
        $data    = $this->collectData($attributes);
375
        $results = $this->isBatch($options) ? $this->first->batchQuery('update', $data, $options) : $this->first->query('update', reset($data), $options);
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...
376
377
        foreach ($this->models as $key => $model) {
378
            $changedAttributes = [];
379
            $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...
380
            foreach ($values as $name => $value) {
381
                $changedAttributes[$name] = $model->getOldAttribute($name);
382
                $model->setOldAttribute($name, $value);
383
            }
384
            $model->afterSave(false, $changedAttributes);
385
        }
386
387
        $this->afterSave();
388
389
        return true;
390
    }
391
392
    public function delete()
393
    {
394
        if (!$this->beforeDelete()) {
395
            return false;
396
        }
397
398
        $data    = $this->collectData();
399
        $results = $this->first->batchQuery('delete', $data);
400
401
        $this->afterDelete();
402
403
        return $results;
404
    }
405
406
    /**
407
     * Collects data from the stored models.
408
     * @param string|array $attributes list of attributes names
409
     * @return array
410
     */
411
    public function collectData($attributes = null)
412
    {
413
        $data = [];
414
        foreach ($this->models as $model) {
415
            if ($this->dataCollector instanceof Closure) {
416
                list($key, $row) = call_user_func($this->dataCollector, $model, $this);
417
            } else {
418
                $key = $model->getPrimaryKey();
419
                $row = $model->getAttributes($attributes);
0 ignored issues
show
Bug introduced by
It seems like $attributes defined by parameter $attributes on line 411 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...
420
            }
421
422
            if ($key) {
423
                $data[$key] = $row;
424
            } else {
425
                $data[] = $row;
426
            }
427
        }
428
429
        return $data;
430
    }
431
432
    /**
433
     * Whether one of models has an error.
434
     * @return bool
435
     */
436
    public function hasErrors()
437
    {
438
        foreach ($this->models as $model) {
439
            if ($model->hasErrors()) {
440
                return true;
441
            }
442
        }
443
444
        return false;
445
    }
446
447
    /**
448
     * Returns the first error of the collection.
449
     * @return bool|mixed
450
     */
451
    public function getFirstError()
452
    {
453
        foreach ($this->models as $model) {
454
            if ($model->hasErrors()) {
455
                $errors = $model->getFirstErrors();
456
457
                return array_shift($errors);
458
            }
459
        }
460
461
        return false;
462
    }
463
464
    public function count()
465
    {
466
        return is_array($this->models) ? count($this->models) : 0;
467
    }
468
469
    public function validate($attributes = null)
470
    {
471
        if (!$this->beforeValidate()) {
472
            return false;
473
        }
474
475
        if (!$this->first->validateMultiple($this->models, $attributes)) {
476
            return false;
477
        }
478
479
        $this->afterValidate();
480
481
        return true;
482
    }
483
484
    public function beforeValidate()
485
    {
486
        $event = new ModelEvent();
487
        $this->triggerAll(self::EVENT_BEFORE_VALIDATE, $event);
488
489
        return $event->isValid;
490
    }
491
492
    public function afterValidate()
493
    {
494
        $event = new ModelEvent();
495
496
        $this->triggerAll(self::EVENT_AFTER_VALIDATE, $event);
497
498
        return $event->isValid;
499
    }
500
501
    public function beforeSave($insert = false)
502
    {
503
        $event = new ModelEvent();
504
        if ($this->isEmpty()) {
505
            $event->isValid = false;
506
        }
507
        $this->triggerAll($insert ? self::EVENT_BEFORE_INSERT : self::EVENT_BEFORE_UPDATE, $event);
508
509
        return $event->isValid;
510
    }
511
512
    public function afterSave()
513
    {
514
        $this->triggerAll(self::EVENT_AFTER_SAVE);
515
    }
516
517
    public function beforeLoad()
518
    {
519
        $event = new ModelEvent();
520
        $this->trigger(self::EVENT_BEFORE_LOAD, $event);
521
522
        return $event->isValid;
523
    }
524
525
    public function afterLoad()
526
    {
527
        $this->trigger(self::EVENT_AFTER_LOAD);
528
    }
529
530
    public function beforeDelete()
531
    {
532
        $event = new ModelEvent();
533
        $this->trigger(self::EVENT_BEFORE_DELETE, $event);
534
535
        return $event->isValid;
536
    }
537
538
    public function afterDelete()
539
    {
540
        $this->trigger(self::EVENT_AFTER_DELETE);
541
    }
542
543
    /**
544
     * Iterates over all of the models and triggers some event.
545
     * @param string     $name  the event name
546
     * @param ModelEvent $event
547
     * @return bool whether is valid
548
     */
549
    public function triggerModels($name, ModelEvent $event = null)
550
    {
551
        if ($event === null) {
552
            $event = new ModelEvent();
553
        }
554
        foreach ($this->models as $model) {
555
            $model->trigger($name, $event);
556
        }
557
558
        return $event->isValid;
559
    }
560
561
    /**
562
     * Calls [[triggerModels()]], then calls [[trigger()]].
563
     * @param string     $name  the event name
564
     * @param ModelEvent $event
565
     * @return bool whether is valid
566
     */
567
    public function triggerAll($name, ModelEvent $event = null)
568
    {
569
        if ($event === null) {
570
            $event = new ModelEvent();
571
        }
572
        if ($this->triggerModels($name, $event)) {
573
            $this->trigger($name, $event);
574
        }
575
576
        return $event->isValid;
577
    }
578
579
    public function isConsistent()
580
    {
581
        $new       = $this->first->getIsNewRecord();
582
        $className = $this->first->className();
583
        foreach ($this->models as $model) {
584
            if ($new !== $model->getIsNewRecord() || $className !== $model->className()) {
585
                return false;
586
            }
587
        }
588
589
        return true;
590
    }
591
592
    public function isEmpty()
593
    {
594
        return empty($this->models);
595
    }
596
597
    /**
598
     * @param array $options
599
     * @return bool
600
     */
601
    public function isBatch($options = [])
602
    {
603
        if (isset($options['batch'])) {
604
           return (bool)$options['batch'];
605
        }
606
607
        return  true;
608
    }
609
}
610