Completed
Branch develop (c2aa4c)
by Anton
05:17
created

RecordEntity::solidState()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 14
Code Lines 8

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 14
rs 9.4286
cc 3
eloc 8
nc 3
nop 2
1
<?php
2
/**
3
 * Spiral Framework.
4
 *
5
 * @license   MIT
6
 * @author    Anton Titov (Wolfy-J)
7
 */
8
namespace Spiral\ORM;
9
10
use Spiral\Core\Exceptions\SugarException;
11
use Spiral\Core\Traits\SaturateTrait;
12
use Spiral\Database\Entities\Table;
13
use Spiral\Models\AccessorInterface;
14
use Spiral\Models\EntityInterface;
15
use Spiral\Models\Events\EntityEvent;
16
use Spiral\Models\Exceptions\AccessorExceptionInterface;
17
use Spiral\Models\SchematicEntity;
18
use Spiral\ORM\Exceptions\RecordException;
19
use Spiral\ORM\Exceptions\RelationException;
20
use Spiral\Validation\ValidatesInterface;
21
22
/**
23
 * Record is base data entity for ORM component, it used to describe related table schema,
24
 * filters, validations and relations to other records. You can count Record class as ActiveRecord
25
 * pattern. ORM component will automatically analyze existed Documents and create cached version of
26
 * their schema.
27
 *
28
 * @TODO: Add ability to set primary key manually, for example fpr uuid like fields.
29
 */
30
class RecordEntity extends SchematicEntity implements RecordInterface
31
{
32
    /**
33
     * Static container fallback.
34
     */
35
    use SaturateTrait;
36
37
    /**
38
     * Field format declares how entity must process magic setters and getters. Available values:
39
     * camelCase, tableize.
40
     */
41
    const FIELD_FORMAT = 'tableize';
42
43
    /**
44
     * We are going to inherit parent validation rules, this will let spiral translator know about
45
     * it and merge i18n messages.
46
     *
47
     * @see TranslatorTrait
48
     */
49
    const I18N_INHERIT_MESSAGES = true;
50
51
    /**
52
     * ORM records are be divided by two sections: active and passive records. When record is active
53
     * ORM allowed to modify associated record table using declared schema and created relations.
54
     *
55
     * Passive records (ACTIVE_SCHEMA = false) however can only read table schema from database and
56
     * forbidden to do any schema modification either by record or by relations.
57
     *
58
     * You can use ACTIVE_SCHEMA = false in cases where you need to create an ActiveRecord for
59
     * existed table.
60
     *
61
     * @see RecordSchema
62
     * @see \Spiral\ORM\Entities\SchemaBuilder
63
     */
64
    const ACTIVE_SCHEMA = true;
65
66
    /**
67
     * Indication that record were deleted.
68
     */
69
    const DELETED = 900;
70
71
    /**
72
     * Default ORM relation types, see ORM configuration and documentation for more information,
73
     * i had to remove 200 lines of comments to make record little bit smaller.
74
     *
75
     * @see RelationSchemaInterface
76
     * @see RelationSchema
77
     */
78
    const HAS_ONE      = 101;
79
    const HAS_MANY     = 102;
80
    const BELONGS_TO   = 103;
81
    const MANY_TO_MANY = 104;
82
83
    /**
84
     * Morphed relation types are usually created by inversion or equivalent of primary relation
85
     * types.
86
     *
87
     * @see RelationSchemaInterface
88
     * @see RelationSchema
89
     * @see MorphedRelation
90
     */
91
    const BELONGS_TO_MORPHED = 108;
92
    const MANY_TO_MORPHED    = 109;
93
94
    /**
95
     * Constants used to declare relations in record schema, used in normalized relation schema.
96
     *
97
     * @see RelationSchemaInterface
98
     */
99
    const OUTER_KEY         = 901; //Outer key name
100
    const INNER_KEY         = 902; //Inner key name
101
    const MORPH_KEY         = 903; //Morph key name
102
    const PIVOT_TABLE       = 904; //Pivot table name
103
    const PIVOT_COLUMNS     = 905; //Pre-defined pivot table columns
104
    const PIVOT_DEFAULTS    = 906; //Pre-defined pivot table default values
105
    const THOUGHT_INNER_KEY = 907; //Pivot table options
106
    const THOUGHT_OUTER_KEY = 908; //Pivot table options
107
    const WHERE             = 909; //Where conditions
108
    const WHERE_PIVOT       = 910; //Where pivot conditions
109
110
    /**
111
     * Additional constants used to control relation schema behaviour.
112
     *
113
     * @see Record::$schema
114
     * @see RelationSchemaInterface
115
     */
116
    const INVERSE           = 1001; //Relation should be inverted to parent record
117
    const CONSTRAINT        = 1002; //Relation should create foreign keys (default)
118
    const CONSTRAINT_ACTION = 1003; //Default relation foreign key delete/update action (CASCADE)
119
    const CREATE_PIVOT      = 1004; //Many-to-Many should create pivot table automatically (default)
120
    const NULLABLE          = 1005; //Relation can be nullable (default)
121
    const CREATE_INDEXES    = 1006; //Indication that relation is allowed to create required indexes
122
    const MORPHED_ALIASES   = 1007; //Aliases for morphed sub-relations
123
124
    /**
125
     * Relations marked as embedded will be automatically saved/validated with parent model. In
126
     * addition such models data can be set using setFields method (only for ONE relations).
127
     *
128
     * @see setFields()
129
     * @see save()
130
     * @see validate()
131
     */
132
    const EMBEDDED_RELATION = 1008;
133
134
    /**
135
     * Constants used to declare indexes in record schema.
136
     *
137
     * @see Record::$indexes
138
     */
139
    const INDEX  = 1000;            //Default index type
140
    const UNIQUE = 2000;            //Unique index definition
141
142
    /**
143
     * Errors in relations and acessors.
144
     *
145
     * @var array
146
     */
147
    private $nestedErrors = [];
148
149
    /**
150
     * Indicates that record data were loaded from database (not recently created).
151
     *
152
     * @var bool
153
     */
154
    private $loaded = false;
155
156
    /**
157
     * Schema provided by ORM component.
158
     *
159
     * @var array
160
     */
161
    private $ormSchema = [];
162
163
    /**
164
     * SolidState will force record data to be saved as one big update set without any generating
165
     * separate update statements for changed columns.
166
     *
167
     * @var bool
168
     */
169
    private $solidState = false;
170
171
    /**
172
     * Populated when record loaded using many-to-many connection. Property will include every
173
     * column of connection row in pivot table.
174
     *
175
     * @see setContext()
176
     * @see getPivot();
177
     * @var array
178
     */
179
    private $pivotData = [];
180
181
    /**
182
     * Record field updates (changed values).
183
     *
184
     * @var array
185
     */
186
    private $updates = [];
187
188
    /**
189
     * Constructed and pre-cached set of record relations. Relation will be in a form of data array
190
     * to be created on demand.
191
     *
192
     * @see relation()
193
     * @see __call()
194
     * @see __set()
195
     * @see __get()
196
     * @var RelationInterface[]|array
197
     */
198
    protected $relations = [];
199
200
    /**
201
     * Table name (without database prefix) record associated to, RecordSchema will generate table
202
     * name automatically using class name, however i'm strongly recommend to declare table name
203
     * manually as it gives more readable code.
204
     *
205
     * @var string
206
     */
207
    protected $table = null;
208
209
    /**
210
     * Database name/id where record table located in. By default database will be used if nothing
211
     * else is specified.
212
     *
213
     * @var string|null
214
     */
215
    protected $database = null;
216
217
    /**
218
     * Set of indexes to be created for associated record table, indexes only created when record is
219
     * not abstract and has active schema set to true.
220
     *
221
     * Use constants INDEX and UNIQUE to describe indexes, you can also create compound indexes:
222
     * protected $indexes = [
223
     *      [self::UNIQUE, 'email'],
224
     *      [self::INDEX, 'board_id'],
225
     *      [self::INDEX, 'board_id', 'check_id']
226
     * ];
227
     *
228
     * @var array
229
     */
230
    protected $indexes = [];
231
232
    /**
233
     * Record relations and columns can be described in one place - record schema.
234
     * Attention: while defining table structure make sure that ACTIVE_SCHEMA constant is set to t
235
     * rue.
236
     *
237
     * Example:
238
     * protected $schema = [
239
     *      'id'        => 'primary',
240
     *      'name'      => 'string',
241
     *      'biography' => 'text'
242
     * ];
243
     *
244
     * You can pass additional options for some of your columns:
245
     * protected $schema = [
246
     *      'pinCode' => 'string(128)',         //String length
247
     *      'status'  => 'enum(active, hidden)', //Enum values
248
     *      'balance' => 'decimal(10, 2)'       //Decimal size and precision
249
     * ];
250
     *
251
     * Every created column will be stated as NOT NULL with forced default value, if you want to
252
     * have nullable columns, specify special data key: protected $schema = [
253
     *      'name'      => 'string, nullable'
254
     * ];
255
     *
256
     * You can easily combine table and relations definition in one schema:
257
     * protected $schema = [
258
     *
259
     *      //Table schema
260
     *      'id'          => 'bigPrimary',
261
     *      'name'        => 'string',
262
     *      'email'       => 'string',
263
     *      'phoneNumber' => 'string(32)',
264
     *
265
     *      //Relations
266
     *      'profile'     => [
267
     *          self::HAS_ONE => 'Records\Profile',
268
     *          self::INVERSE => 'user'
269
     *      ],
270
     *      'roles'       => [
271
     *          self::MANY_TO_MANY => 'Records\Role',
272
     *          self::INVERSE => 'users'
273
     *      ]
274
     * ];
275
     *
276
     * @var array
277
     */
278
    protected $schema = [];
279
280
    /**
281
     * Default field values.
282
     *
283
     * @var array
284
     */
285
    protected $defaults = [];
286
287
    /**
288
     * @invisible
289
     * @var ORM
290
     */
291
    protected $orm = null;
292
293
    /**
294
     * Due setContext() method and entity cache of ORM any custom initiation code in constructor
295
     * must not depends on database data.
296
     *
297
     * @see setContext
298
     * @param array      $data
299
     * @param bool|false $loaded
300
     * @param ORM|null   $orm
301
     * @param array      $ormSchema
302
     * @throws SugarException
303
     */
304
    public function __construct(
305
        array $data = [],
306
        $loaded = false,
307
        ORM $orm = null,
308
        array $ormSchema = []
309
    ) {
310
        $this->loaded = $loaded;
311
312
        //We can use global container as fallback if no default values were provided
313
        $this->orm = $this->saturate($orm, ORM::class);
314
315
        $this->ormSchema = !empty($ormSchema) ? $ormSchema : $this->orm->schema(static::class);
316
317
        if (isset($data[ORM::PIVOT_DATA])) {
318
            $this->pivotData = $data[ORM::PIVOT_DATA];
319
            unset($data[ORM::PIVOT_DATA]);
320
        }
321
322
        foreach (array_intersect_key($data,
323
            $this->ormSchema[ORM::M_RELATIONS]) as $name => $relation) {
324
            $this->relations[$name] = $relation;
325
            unset($data[$name]);
326
        }
327
328
        //Data is initiated as default values and non default
329
        parent::__construct($data + $this->ormSchema[ORM::M_COLUMNS], $this->ormSchema);
330
331
        if (!$this->isLoaded()) {
332
            //Non loaded records should be in solid state by default and require initial validation
333
            $this->solidState(true)->invalidate();
334
        }
335
    }
336
337
    /**
338
     * Record context must be updated in cases where single record instance can be accessed from
339
     * multiple places, context will not change record fields but might overwrite pivot data or
340
     * clarify loaded relations.
341
     *
342
     * @param array $context
343
     * @return $this
344
     */
345
    public function setContext(array $context)
346
    {
347
        //Mounting context pivot data
348
        $this->pivotData = isset($context[ORM::PIVOT_DATA]) ? $context[ORM::PIVOT_DATA] : [];
349
350
        $relations = array_intersect_key($context, $this->ormSchema[ORM::M_RELATIONS]);
351
        foreach ($relations as $name => $relation) {
352
            if (!isset($this->relations[$name]) || is_array($this->relations[$name])) {
353
                //Does not exists and never requested before
354
                $this->relations[$name] = $relation;
355
                continue;
356
            }
357
358
            //We have to reset relation state to update context
359
            $this->relations[$name]->reset($relation, true);
360
        }
361
362
        /**
363
         * We are not going to update record fields.
364
         */
365
366
        return $this;
367
    }
368
369
    /**
370
     * Change record solid state. SolidState will force record data to be saved as one big update
371
     * set without any generating separate update statements for changed columns.
372
     *
373
     * Attention, you have to carefully use forceUpdate flag with records without primary keys due
374
     * update criteria (WHERE condition) can not be easy constructed for records with primary key.
375
     *
376
     * @param bool $solidState
377
     * @param bool $forceUpdate Mark all fields as changed to force update later.
378
     * @return $this
379
     */
380
    public function solidState($solidState, $forceUpdate = false)
381
    {
382
        $this->solidState = $solidState;
383
384
        if ($forceUpdate) {
385
            if ($this->ormSchema[ORM::M_PRIMARY_KEY]) {
386
                $this->updates = $this->stateCriteria();
387
            } else {
388
                $this->updates = $this->ormSchema[ORM::M_COLUMNS];
389
            }
390
        }
391
392
        return $this;
393
    }
394
395
    /**
396
     * Is record is solid state?
397
     *
398
     * @see solidState()
399
     * @return bool
400
     */
401
    public function isSolid()
402
    {
403
        return $this->solidState;
404
    }
405
406
    /**
407
     * {@inheritdoc}
408
     */
409
    public function recordRole()
410
    {
411
        return $this->ormSchema[ORM::M_ROLE_NAME];
412
    }
413
414
    /**
415
     * {@inheritdoc}
416
     */
417
    public function primaryKey()
418
    {
419
        return isset($this->fields[$this->ormSchema[ORM::M_PRIMARY_KEY]])
420
            ? $this->fields[$this->ormSchema[ORM::M_PRIMARY_KEY]]
421
            : null;
422
    }
423
424
    /**
425
     * {@inheritdoc}
426
     */
427
    public function isLoaded()
428
    {
429
        return (bool)$this->loaded && !$this->isDeleted();
430
    }
431
432
    /**
433
     * {@inheritdoc}
434
     */
435
    public function isDeleted()
436
    {
437
        return $this->loaded === self::DELETED;
438
    }
439
440
    /**
441
     * Pivot data associated with record instance, populated only in cases when record loaded using
442
     * Many-to-Many relation.
443
     *
444
     * @return array
445
     */
446
    public function getPivot()
447
    {
448
        return $this->pivotData;
449
    }
450
451
    /**
452
     * {@inheritdoc}
453
     *
454
     * @see   $fillable
455
     * @see   $secured
456
     * @see   isFillable()
457
     * @param array|\Traversable $fields
458
     * @param bool               $all Fill all fields including non fillable.
459
     * @return $this
460
     * @throws AccessorExceptionInterface
461
     * @event setFields($fields)
462
     */
463
    public function setFields($fields = [], $all = false)
464
    {
465
        parent::setFields($fields, $all);
466
467
        foreach ($fields as $name => $nested) {
468
            //We can fill data of embedded of relations (usually HAS ONE)
469
            if ($this->isEmbedded($name)) {
470
                //Getting relation instance
471
                $relation = $this->relation($name);
472
473
                //Getting related object
474
                $related = $relation->getRelated();
475
                if ($related instanceof EntityInterface) {
476
                    $related->setFields($nested);
477
                }
478
            }
479
        }
480
481
        return $this;
482
    }
483
484
    /**
485
     * {@inheritdoc}
486
     *
487
     * Must track field updates. In addition Records will not allow to set unknown field.
488
     *
489
     * @throws RecordException
490
     */
491
    public function setField($name, $value, $filter = true)
492
    {
493
        if (!$this->hasField($name)) {
494
            throw new RecordException("Undefined field '{$name}' in '" . static::class . "'.");
495
        }
496
497
        //Original values with no filters
498
        $original = parent::getField($name, null, false);
0 ignored issues
show
Comprehensibility Bug introduced by
It seems like you call parent on a different method (getField() instead of setField()). Are you sure this is correct? If so, you might want to change this to $this->getField().

This check looks for a call to a parent method whose name is different than the method from which it is called.

Consider the following code:

class Daddy
{
    protected function getFirstName()
    {
        return "Eidur";
    }

    protected function getSurName()
    {
        return "Gudjohnsen";
    }
}

class Son
{
    public function getFirstName()
    {
        return parent::getSurname();
    }
}

The getFirstName() method in the Son calls the wrong method in the parent class.

Loading history...
499
        if ($value === null && in_array($name, $this->ormSchema[ORM::M_NULLABLE])) {
500
            //We must bypass setters and accessors when null value assigned to nullable column
501
            $this->setField($name, null, false);
502
        } else {
503
            parent::setField($name, $value, $filter);
504
        }
505
506 View Code Duplication
        if (!array_key_exists($name, $this->updates)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
507
            $this->updates[$name] = $original instanceof AccessorInterface
508
                ? $original->serializeData()
509
                : $original;
510
        }
511
    }
512
513
    /**
514
     * {@inheritdoc}
515
     *
516
     * Record will skip filtration for nullable fields.
517
     */
518
    public function getField($name, $default = null, $filter = true)
519
    {
520
        if (!$this) {
521
            throw new RecordException("Undefined field '{$name}' in '" . static::class . "'.");
522
        }
523
524
        $value = $this->fields[$name];
525
        if ($value === null && in_array($name, $this->ormSchema[ORM::M_NULLABLE])) {
526
            if (!isset($this->ormSchema[ORM::M_MUTATORS]['accessor'][$name])) {
527
                //We can skip setters for null values, but not accessors
528
                return $value;
529
            }
530
        }
531
532
        return parent::getField($name, $default, $filter);
533
    }
534
535
    /**
536
     * Get or create record relation by it's name and pre-loaded (optional) set of data.
537
     *
538
     * @param string $name
539
     * @param mixed  $data
540
     * @param bool   $loaded
541
     * @return RelationInterface
542
     * @throws RelationException
543
     * @throws RecordException
544
     */
545
    public function relation($name, $data = null, $loaded = false)
546
    {
547
        if (array_key_exists($name, $this->relations)) {
548
            if (!is_object($this->relations[$name])) {
549
                $data = $this->relations[$name];
550
                unset($this->relations[$name]);
551
552
                //Loaded relation
553
                return $this->relation($name, $data, true);
554
            }
555
556
            //Already created
557
            return $this->relations[$name];
558
        }
559
560
561
        //Constructing relation
562
        if (!isset($this->ormSchema[ORM::M_RELATIONS][$name])) {
563
            throw new RecordException(
564
                "Undefined relation {$name} in record " . static::class . "."
565
            );
566
        }
567
568
        $relation = $this->ormSchema[ORM::M_RELATIONS][$name];
569
570
        return $this->relations[$name] = $this->orm->relation(
571
            $relation[ORM::R_TYPE], $this, $relation[ORM::R_DEFINITION], $data, $loaded
572
        );
573
    }
574
575
    /**
576
     * {@inheritdoc}
577
     *
578
     * @param string $field Specific field name to check for updates.
579
     */
580
    public function hasUpdates($field = null)
581
    {
582 View Code Duplication
        if (empty($field)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
583
            if (!empty($this->updates)) {
584
                return true;
585
            }
586
587
            foreach ($this->fields as $field => $value) {
588
                if ($value instanceof RecordAccessorInterface && $value->hasUpdates()) {
589
                    return true;
590
                }
591
            }
592
593
            return false;
594
        }
595
596
        if (array_key_exists($field, $this->updates)) {
597
            return true;
598
        }
599
600
        $value = $this->getField($field);
601
        if ($value instanceof RecordAccessorInterface && $value->hasUpdates()) {
602
            return true;
603
        }
604
605
        return false;
606
    }
607
608
    /**
609
     * {@inheritdoc}
610
     */
611
    public function flushUpdates()
612
    {
613
        $this->updates = [];
614
615
        foreach ($this->fields as $value) {
616
            if ($value instanceof RecordAccessorInterface) {
617
                $value->flushUpdates();
618
            }
619
        }
620
    }
621
622
    /**
623
     * {@inheritdoc}
624
     */
625
    public function isValid()
626
    {
627
        $this->validate();
628
629
        return empty($this->errors) && empty($this->nestedErrors);
630
    }
631
632
    /**
633
     * {@inheritdoc}
634
     */
635
    public function getErrors($reset = false)
636
    {
637
        return parent::getErrors($reset) + $this->nestedErrors;
638
    }
639
640
    /**
641
     * {@inheritdoc}
642
     */
643
    public function __isset($name)
644
    {
645
        if (isset($this->ormSchema[ORM::M_RELATIONS][$name])) {
646
            return !empty($this->relation($name)->getRelated());
647
        }
648
649
        return parent::__isset($name);
650
    }
651
652
    /**
653
     * {@inheritdoc}
654
     *
655
     * @throws RecordException
656
     */
657
    public function __unset($offset)
658
    {
659
        throw new RecordException("Records fields can not be unsetted.");
660
    }
661
662
    /**
663
     * {@inheritdoc}
664
     *
665
     * @see relation()
666
     */
667
    public function __get($offset)
668
    {
669
        if (isset($this->ormSchema[ORM::M_RELATIONS][$offset])) {
670
            //Bypassing call to relation
671
            return $this->relation($offset)->getRelated();
672
        }
673
674
        return $this->getField($offset, true);
675
    }
676
677
    /**
678
     * {@inheritdoc}
679
     *
680
     * @see relation()
681
     */
682
    public function __set($offset, $value)
683
    {
684
        if (isset($this->ormSchema[ORM::M_RELATIONS][$offset])) {
685
            //Bypassing call to relation
686
            $this->relation($offset)->associate($value);
687
688
            return;
689
        }
690
691
        $this->setField($offset, $value, true);
692
    }
693
694
    /**
695
     * Direct access to relation by it's name.
696
     *
697
     * @see relation()
698
     * @param string $method
699
     * @param array  $arguments
700
     * @return RelationInterface|mixed|AccessorInterface
701
     */
702
    public function __call($method, array $arguments)
703
    {
704
        if (isset($this->ormSchema[ORM::M_RELATIONS][$method])) {
705
            return $this->relation($method);
706
        }
707
708
        //See FIELD_FORMAT constant
709
        return parent::__call($method, $arguments);
710
    }
711
712
    /**
713
     * @return Object
714
     */
715
    public function __debugInfo()
716
    {
717
        $info = [
718
            'table'     => $this->ormSchema[ORM::M_DB] . '/' . $this->ormSchema[ORM::M_TABLE],
719
            'pivotData' => $this->pivotData,
720
            'fields'    => $this->getFields(),
721
            'errors'    => $this->getErrors()
722
        ];
723
724
        if (empty($this->pivotData)) {
725
            unset($info['pivotData']);
726
        }
727
728
        return (object)$info;
729
    }
730
731
    /**
732
     * Get associated Database\Table instance.
733
     *
734
     * @see save()
735
     * @see delete()
736
     * @return Table
737
     */
738
    protected function sourceTable()
739
    {
740
        return $this->orm->database($this->ormSchema[ORM::M_DB])->table(
741
            $this->ormSchema[ORM::M_TABLE]
742
        );
743
    }
744
745
    /**
746
     * Get WHERE array to be used to perform record data update or deletion. Usually will include
747
     * record primary key.
748
     *
749
     * @return array
750
     */
751
    protected function stateCriteria()
752
    {
753
        if (!empty($primaryKey = $this->ormSchema()[ORM::M_PRIMARY_KEY])) {
754
            return [$primaryKey => $this->primaryKey()];
755
        }
756
757
        //We have to serialize record data
758
        return $this->updates + $this->serializeData();
759
    }
760
761
    /**
762
     * {@inheritdoc}
763
     */
764
    protected function container()
765
    {
766
        if (empty($this->orm)) {
767
            return parent::container();
768
        }
769
770
        return $this->orm->container();
1 ignored issue
show
Bug introduced by
The method container() cannot be called from this context as it is declared protected in class Spiral\Core\Component.

This check looks for access to methods that are not accessible from the current context.

If you need to make a method accessible to another context you can raise its visibility level in the defining class.

Loading history...
771
    }
772
773
    /**
774
     * Create set of fields to be sent to UPDATE statement.
775
     *
776
     * @see save()
777
     * @return array
778
     */
779
    protected function compileUpdates()
780
    {
781
        if (!$this->hasUpdates() && !$this->isSolid()) {
782
            return [];
783
        }
784
785
        if ($this->isSolid()) {
786
            return $this->solidUpdate();
787
        }
788
789
        $updates = [];
790
        foreach ($this->fields as $field => $value) {
791 View Code Duplication
            if ($value instanceof RecordAccessorInterface) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
792
                if ($value->hasUpdates()) {
793
                    $updates[$field] = $value->compileUpdates($field);
794
                    continue;
795
                }
796
797
                //Will be handled as normal update if needed
798
                $value = $value->serializeData();
799
            }
800
801
            if (array_key_exists($field, $this->updates)) {
802
                $updates[$field] = $value;
803
            }
804
        }
805
806
        //Primary key should not present in update set
807
        unset($updates[$this->ormSchema[ORM::M_PRIMARY_KEY]]);
808
809
        return $updates;
810
    }
811
812
    /**
813
     * {@inheritdoc}
814
     *
815
     * Will validate every loaded and embedded relation.
816
     */
817 View Code Duplication
    protected function validate($reset = false)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
818
    {
819
        $this->nestedErrors = [];
820
821
        //Validating all compositions/accessors
822
        foreach ($this->fields as $field => $value) {
823
            //Ensuring value state
824
            $value = $this->getField($field);
825
            if (!$value instanceof ValidatesInterface) {
826
                continue;
827
            }
828
829
            if (!$value->isValid()) {
830
                $this->nestedErrors[$field] = $value->getErrors($reset);
831
            }
832
        }
833
834
        //We have to validate some relations before saving them
835
        $this->validateRelations($reset);
836
837
        parent::validate($reset);
838
839
        return empty($this->errors + $this->nestedErrors);
840
    }
841
842
    /**
843
     * {@inheritdoc}
844
     *
845
     * @see   Component::staticContainer()
846
     * @param array $fields Record fields to set, will be passed thought filters.
847
     * @param ORM   $orm    ORM component, global container will be called if not instance provided.
848
     * @event created()
849
     */
850 View Code Duplication
    public static function create($fields = [], ORM $orm = null)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
851
    {
852
        /**
853
         * @var RecordEntity $record
854
         */
855
        $record = new static([], false, $orm);
856
857
        //Forcing validation (empty set of fields is not valid set of fields)
858
        $record->setFields($fields)->dispatch('created', new EntityEvent($record));
859
860
        return $record;
861
    }
862
863
    /**
864
     * Change record loaded state.
865
     *
866
     * @param bool|mixed $loader
867
     * @return $this
868
     */
869
    protected function loadedState($loader)
870
    {
871
        $this->loaded = $loader;
872
873
        return $this;
874
    }
875
876
    /**
877
     * Related and cached ORM schema.
878
     *
879
     * @return array
880
     */
881
    protected function ormSchema()
882
    {
883
        return $this->ormSchema;
884
    }
885
886
    /**
887
     * Check if relation is embedded.
888
     *
889
     * @param string $relation
890
     * @return bool
891
     */
892
    protected function isEmbedded($relation)
893
    {
894
        return !empty(
895
        $this->ormSchema[ORM::M_RELATIONS][$relation][ORM::R_DEFINITION][self::EMBEDDED_RELATION]
896
        );
897
    }
898
899
    /**
900
     * Full structure update.
901
     *
902
     * @return array
903
     */
904
    private function solidUpdate()
905
    {
906
        $updates = [];
907
        foreach ($this->fields as $field => $value) {
908 View Code Duplication
            if ($value instanceof RecordAccessorInterface && $value->hasUpdates()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
909
                if ($value->hasUpdates()) {
910
                    $updates[$field] = $value->compileUpdates($field);
911
                } else {
912
                    $updates[$field] = $value->serializeData();
913
                }
914
                continue;
915
            }
916
917
            $updates[$field] = $value;
918
        }
919
920
        return $updates;
921
    }
922
923
    /**
924
     * Validate embedded relations.
925
     *
926
     * @param bool $reset
927
     */
928
    private function validateRelations($reset)
929
    {
930
        foreach ($this->relations as $name => $relation) {
931
            if (!$relation instanceof ValidatesInterface) {
932
                //Never constructed
933
                continue;
934
            }
935
936
            if ($this->isEmbedded($name) && !$relation->isValid()) {
937
                $this->nestedErrors[$name] = $relation->getErrors($reset);
938
            }
939
        }
940
    }
941
}
942