Completed
Branch feature/pre-split (d91fae)
by Anton
05:19
created

Relation::hasErrors()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 5
Bugs 0 Features 0
Metric Value
cc 1
eloc 2
nc 1
nop 0
dl 0
loc 4
rs 10
c 5
b 0
f 0
1
<?php
2
/**
3
 * Spiral Framework.
4
 *
5
 * @license   MIT
6
 * @author    Anton Titov (Wolfy-J)
7
 */
8
9
namespace Spiral\ORM\Entities;
10
11
use Spiral\Core\Component;
12
use Spiral\Models\ActiveEntityInterface;
13
use Spiral\Models\EntityInterface;
14
use Spiral\Models\IdentifiedInterface;
15
use Spiral\ORM\Entities\Traits\AliasTrait;
16
use Spiral\ORM\Exceptions\RelationException;
17
use Spiral\ORM\ORM;
18
use Spiral\ORM\RecordEntity;
19
use Spiral\ORM\RecordInterface;
20
use Spiral\ORM\RelationInterface;
21
22
/**
23
 * Abstract implementation of ORM Relations, provides access to associated instances, use ORM entity
24
 * cache and record iterators. In additional, relation can be serialized into json, or iterated when
25
 * needed.
26
 *
27
 * This abstract implement built to work with ORM Record classes.
28
 */
29
abstract class Relation extends Component implements
30
    RelationInterface,
31
    \Countable,
32
    \IteratorAggregate,
33
    \JsonSerializable
34
{
35
    /*
36
     * {@} table aliases.
37
     */
38
    use AliasTrait;
39
40
    /**
41
     * Relation type, required to fetch record class from relation definition.
42
     */
43
    const RELATION_TYPE = null;
44
45
    /**
46
     * Indication that relation represent multiple records (HAS_MANY relations).
47
     */
48
    const MULTIPLE = false;
49
50
    /**
51
     * Indication that relation data has been loaded from databases.
52
     *
53
     * @var bool
54
     */
55
    protected $loaded = false;
56
57
    /**
58
     * Pre-loaded relation data, can be loaded while parent record, or later. Real data instance
59
     * will be constructed on demand and will keep it pre-loaded context between calls.
60
     *
61
     * @see Record::setContext()
62
     *
63
     * @var array|null
64
     */
65
    protected $data = [];
66
67
    /**
68
     * Instance of constructed EntityInterface of RecordIterator.
69
     *
70
     * @invisible
71
     *
72
     * @var mixed|EntityInterface|RecordIterator
73
     */
74
    protected $instance = null;
75
76
    /**
77
     * Parent Record caused relation to be created.
78
     *
79
     * @var RecordInterface
80
     */
81
    protected $parent = null;
82
83
    /**
84
     * Relation definition fetched from ORM schema. Must already be normalized by RelationSchema.
85
     *
86
     * @invisible
87
     *
88
     * @var array
89
     */
90
    protected $definition = [];
91
92
    /**
93
     * @invisible
94
     *
95
     * @var ORM
96
     */
97
    protected $orm = null;
98
99
    /**
100
     * @param ORM             $orm
101
     * @param RecordInterface $parent
102
     * @param array           $definition Relation definition, must be normalized by relation
103
     *                                    schema.
104
     * @param mixed           $data       Pre-loaded relation data.
105
     * @param bool            $loaded     Indication that relation data has been loaded.
106
     */
107
    public function __construct(
108
        ORM $orm,
109
        RecordInterface $parent,
110
        array $definition,
111
        $data = null,
112
        $loaded = false
113
    ) {
114
        $this->orm = $orm;
115
        $this->parent = $parent;
116
        $this->definition = $definition;
117
        $this->data = $data;
118
        $this->loaded = $loaded;
119
    }
120
121
    /**
122
     * {@inheritdoc}
123
     */
124
    public function isLoaded()
125
    {
126
        return $this->loaded;
127
    }
128
129
    /**
130
     * {@inheritdoc}
131
     *
132
     * Relation will automatically create related record if relation is not nullable. Usually
133
     * applied for has one relations ($user->profile).
134
     */
135
    public function getRelated()
136
    {
137
        if (!empty($this->instance)) {
138
            //RecordIterator will update context automatically
139
            return $this->instance;
140
        }
141
142
        if (!$this->isLoaded()) {
143
            //Loading data if not already loaded
144
            $this->loadData();
145
        }
146
147
        if (empty($this->data)) {
148
            if (
149
                array_key_exists(RecordEntity::NULLABLE, $this->definition)
150
                && !$this->definition[RecordEntity::NULLABLE]
151
                && !static::MULTIPLE
152
            ) {
153
                //Not nullable relations must always return requested instance
154
                return $this->instance = $this->emptyRecord();
155
            }
156
157
            //Can not be loaded, let's use empty iterator
158
            return static::MULTIPLE ? $this->createIterator() : null;
159
        }
160
161
        if (static::MULTIPLE) {
162
            $instance = $this->createIterator();
163
        } else {
164
            $instance = $this->createRecord();
165
        }
166
167
        return $this->instance = $instance;
168
    }
169
170
    /**
171
     * {@inheritdoc}
172
     */
173
    public function associate(EntityInterface $related = null)
174
    {
175
        if (static::MULTIPLE) {
176
            throw new RelationException(
177
                'Unable to associate relation data (relation represent multiple records).'
178
            );
179
        }
180
181
        //Simplification for morphed relations
182
        if (!is_array($allowed = $this->definition[static::RELATION_TYPE])) {
183
            $allowed = [$allowed];
184
        }
185
186
        if (!is_object($related) || !in_array(get_class($related), $allowed)) {
187
            $allowed = implode("', '", $allowed);
188
189
            throw new RelationException(
190
                "Only instances of '{$allowed}' can be assigned to this relation."
191
            );
192
        }
193
194
        //Entity caching
195
        $this->instance = $related;
196
        $this->loaded = true;
197
        $this->data = [];
198
    }
199
200
    /**
201
     * {@inheritdoc}
202
     */
203
    public function saveRelated($validate = true)
204
    {
205
        if (empty($instance = $this->getRelated())) {
206
            //Nothing to save
207
            return true;
208
        }
209
210
        if (static::MULTIPLE) {
211
            /**
212
             * @var RecordIterator|EntityInterface[]
213
             */
214
            foreach ($instance as $record) {
0 ignored issues
show
Bug introduced by
The expression $instance of type object<Spiral\Models\EntityInterface> is not traversable.
Loading history...
215
                if (!$this->saveEntity($record, $validate)) {
216
                    return false;
217
                }
218
            }
219
220
            return true;
221
        }
222
223
        return $this->saveEntity($instance, $validate);
224
    }
225
226
    /**
227
     * {@inheritdoc}
228
     */
229
    public function reset(array $data = [], $loaded = false)
230
    {
231
        if ($loaded && !empty($this->data) && $this->data == $data) {
232
            //Nothing to do, context is the same
233
            return;
234
        }
235
236
        if (!$loaded || !($this->instance instanceof EntityInterface)) {
237
            //Flushing instance
238
            $this->instance = null;
239
        }
240
241
        $this->data = $data;
242
        $this->loaded = $loaded;
243
    }
244
245
    /**
246
     * {@inheritdoc}
247
     */
248
    public function isValid()
249
    {
250
        $related = $this->getRelated();
251
        if (!static::MULTIPLE) {
252
            if ($related instanceof EntityInterface) {
253
                return $related->isValid();
254
            }
255
256
            return true;
257
        }
258
259
        /*
260
         * @var RecordIterator|EntityInterface[] $data
261
         */
262
        $hasErrors = false;
263
        foreach ($related as $entity) {
0 ignored issues
show
Bug introduced by
The expression $related of type null|object<Spiral\Models\EntityInterface> is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
264
            if (!$entity->isValid()) {
265
                $hasErrors = true;
266
            }
267
        }
268
269
        return !$hasErrors;
270
    }
271
272
    /**
273
     * {@inheritdoc}
274
     */
275
    public function hasErrors()
276
    {
277
        return !$this->isValid();
278
    }
279
280
    /**
281
     * List of errors associated with parent field, every field must have only one error assigned.
282
     *
283
     * @param bool $reset Clean errors after receiving every message.
284
     *
285
     * @return array
286
     */
287
    public function getErrors($reset = false)
288
    {
289
        $related = $this->getRelated();
290
291
        if (!static::MULTIPLE) {
292
            if ($related instanceof EntityInterface) {
293
                return $related->getErrors($reset);
294
            }
295
296
            return [];
297
        }
298
299
        /*
300
         * @var RecordIterator|EntityInterface[] $data
301
         */
302
        $errors = [];
303
        foreach ($related as $position => $record) {
0 ignored issues
show
Bug introduced by
The expression $related of type null|object<Spiral\Models\EntityInterface> is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
304
            if (!$record->isValid()) {
305
                $errors[$position] = $record->getErrors($reset);
306
            }
307
        }
308
309
        return !empty($errors);
310
    }
311
312
    /**
313
     * Get selector associated with relation.
314
     *
315
     * @param array $where
316
     *
317
     * @return RecordSelector
318
     */
319
    public function find(array $where = [])
320
    {
321
        return $this->createSelector()->where($where);
322
    }
323
324
    /**
325
     * {@inheritdoc}
326
     *
327
     * Use getRelation() method to count pre-loaded data.
328
     *
329
     * @return int
330
     */
331
    public function count()
332
    {
333
        return $this->createSelector()->count();
334
    }
335
336
    /**
337
     * Perform iterator on pre-loaded data. Use relation selector to iterate thought custom relation
338
     * query.
339
     *
340
     * @return RecordEntity|RecordEntity[]|RecordIterator
341
     */
342
    public function getIterator()
343
    {
344
        return $this->getRelated();
345
    }
346
347
    /**
348
     * Bypassing call to created selector.
349
     *
350
     * @param string $method
351
     * @param array  $arguments
352
     *
353
     * @return mixed
354
     */
355
    public function __call($method, array $arguments)
356
    {
357
        return call_user_func_array([$this->createSelector(), $method], $arguments);
358
    }
359
360
    /**
361
     * {@inheritdoc}
362
     */
363
    public function jsonSerialize()
364
    {
365
        return $this->getRelated();
366
    }
367
368
    /**
369
     * {@inheritdoc}
370
     */
371
    protected function container()
372
    {
373
        return $this->orm->container();
374
    }
375
376
    /**
377
     * Class name of outer record.
378
     *
379
     * @return string
380
     */
381
    protected function getClass()
382
    {
383
        return $this->definition[static::RELATION_TYPE];
384
    }
385
386
    /**
387
     * Mount relation keys to parent or children records to ensure their connection. Method called
388
     * when record requests relation save.
389
     *
390
     * @param EntityInterface $record
391
     *
392
     * @return EntityInterface
393
     */
394
    abstract protected function mountRelation(EntityInterface $record);
395
396
    /**
397
     * Convert pre-loaded relation data to record iterator record.
398
     *
399
     * @return RecordIterator
400
     */
401
    protected function createIterator()
402
    {
403
        return new RecordIterator($this->orm, $this->getClass(), (array)$this->data);
0 ignored issues
show
Documentation introduced by
$this->orm is of type object<Spiral\ORM\ORM>, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
Documentation introduced by
(array) $this->data is of type array, but the function expects a object<Spiral\ORM\ORMInterface>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
404
    }
405
406
    /**
407
     * Convert pre-loaded relation data to active record record.
408
     *
409
     * @return RecordEntity
410
     */
411
    protected function createRecord()
412
    {
413
        return $this->orm->record($this->getClass(), (array)$this->data);
414
    }
415
416
    /**
417
     * Create empty record to be associated with non nullable relation.
418
     *
419
     * @return RecordEntity
420
     */
421
    protected function emptyRecord()
422
    {
423
        $record = $this->orm->record($this->getClass(), []);
424
        $this->associate($record);
425
426
        return $record;
427
    }
428
429
    /**
430
     * Load relation data based on created selector.
431
     *
432
     * @return array|null
433
     */
434
    protected function loadData()
435
    {
436
        if (!$this->isLoadable()) {
437
            //Nothing to load for unloaded parents
438
            return;
439
        }
440
441
        $this->loaded = true;
442
        if (static::MULTIPLE) {
443
            return $this->data = $this->createSelector()->fetchData();
444
        }
445
446
        $data = $this->createSelector()->fetchData();
447
        if (isset($data[0])) {
448
            return $this->data = $data[0];
449
        }
450
451
        return;
452
    }
453
454
    /**
455
     * Internal ORM relation method used to create valid selector used to pre-load relation data or
456
     * create custom query based on relation options.
457
     *
458
     * Must be redeclarated in child implementations.
459
     *
460
     * @return RecordSelector
461
     */
462
    protected function createSelector()
463
    {
464
        return $this->orm->selector($this->getClass());
465
    }
466
467
    /**
468
     * Loadable when parent is loaded as well.
469
     *
470
     * @return bool
471
     */
472
    protected function isLoadable()
473
    {
474
        return $this->parent->isLoaded();
475
    }
476
477
    /**
478
     * Save simple related entity.
479
     *
480
     * @param EntityInterface $entity
481
     * @param bool            $validate
482
     *
483
     * @return bool|void
484
     */
485
    private function saveEntity(EntityInterface $entity, $validate)
486
    {
487
        if ($entity instanceof RecordInterface && $entity->isDeleted()) {
488
            return true;
489
        }
490
491
        if (!$entity instanceof ActiveEntityInterface) {
492
            throw new RelationException('Unable to save non active entity.');
493
        }
494
495
        $this->mountRelation($entity);
496
        if (!$entity->save($validate)) {
497
            return false;
498
        }
499
500
        if ($entity instanceof IdentifiedInterface) {
501
            $this->orm->cache()->remember($entity);
0 ignored issues
show
Bug introduced by
The method cache() does not seem to exist on object<Spiral\ORM\ORM>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
502
        }
503
504
        return true;
505
    }
506
}
507