Completed
Push — master ( 084e1b...dd5a4f )
by Anton
08:30 queued 02:03
created

RecordEntity   F

Complexity

Total Complexity 92

Size/Duplication

Total Lines 884
Duplicated Lines 8.71 %

Coupling/Cohesion

Components 2
Dependencies 12

Importance

Changes 0
Metric Value
dl 77
loc 884
rs 2.9998
c 0
b 0
f 0
wmc 92
lcom 2
cbo 12

33 Methods

Rating   Name   Duplication   Size   Complexity  
A recordRole() 0 4 1
A primaryKey() 0 6 2
A isLoaded() 0 4 2
A isDeleted() 0 4 1
A getPivot() 0 4 1
A setFields() 0 20 4
B setField() 8 20 7
B relation() 0 29 4
D hasUpdates() 13 27 9
A flushUpdates() 0 10 3
A isValid() 0 6 2
A getErrors() 0 4 1
A __isset() 0 8 2
A __unset() 0 4 1
A __get() 0 9 2
A __set() 0 11 2
A __call() 0 9 2
A __debugInfo() 0 15 2
A sourceTable() 0 6 1
A stateCriteria() 0 9 2
C compileUpdates() 9 32 8
B validate() 24 24 4
A create() 12 12 1
A loadedState() 0 6 1
A ormSchema() 0 4 1
A isEmbedded() 0 6 1
B solidUpdate() 8 18 5
B validateRelations() 0 13 5
B __construct() 0 31 5
A solidState() 0 14 3
A isSolid() 0 4 1
A container() 0 8 2
A getField() 3 16 4

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like RecordEntity often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use RecordEntity, and based on these observations, apply Extract Interface, too.

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

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
494
                //We can skip setters for null values, but not accessors
495
                return $value;
496
            //}
497
        }
498
499
        return parent::getField($name, $default, $filter);
500
    }
501
502
    /**
503
     * Get or create record relation by it's name and pre-loaded (optional) set of data.
504
     *
505
     * @todo hasRelation?
506
     * @param string $name
507
     * @param mixed  $data
508
     * @param bool   $loaded
509
     * @return RelationInterface
510
     * @throws RelationException
511
     * @throws RecordException
512
     */
513
    public function relation($name, $data = null, $loaded = false)
514
    {
515
        if (array_key_exists($name, $this->relations)) {
516
            if (!is_object($this->relations[$name])) {
517
                $data = $this->relations[$name];
518
                unset($this->relations[$name]);
519
520
                //Loaded relation
521
                return $this->relation($name, $data, true);
522
            }
523
524
            //Already created
525
            return $this->relations[$name];
526
        }
527
528
529
        //Constructing relation
530
        if (!isset($this->ormSchema[ORM::M_RELATIONS][$name])) {
531
            throw new RecordException(
532
                "Undefined relation {$name} in record " . static::class . "."
533
            );
534
        }
535
536
        $relation = $this->ormSchema[ORM::M_RELATIONS][$name];
537
538
        return $this->relations[$name] = $this->orm->relation(
539
            $relation[ORM::R_TYPE], $this, $relation[ORM::R_DEFINITION], $data, $loaded
540
        );
541
    }
542
543
    /**
544
     * {@inheritdoc}
545
     *
546
     * @param string $field Specific field name to check for updates.
547
     */
548
    public function hasUpdates($field = null)
549
    {
550 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...
551
            if (!empty($this->updates)) {
552
                return true;
553
            }
554
555
            foreach ($this->fields as $field => $value) {
556
                if ($value instanceof RecordAccessorInterface && $value->hasUpdates()) {
557
                    return true;
558
                }
559
            }
560
561
            return false;
562
        }
563
564
        if (array_key_exists($field, $this->updates)) {
565
            return true;
566
        }
567
568
        $value = $this->getField($field);
569
        if ($value instanceof RecordAccessorInterface && $value->hasUpdates()) {
570
            return true;
571
        }
572
573
        return false;
574
    }
575
576
    /**
577
     * {@inheritdoc}
578
     */
579
    public function flushUpdates()
580
    {
581
        $this->updates = [];
582
583
        foreach ($this->fields as $value) {
584
            if ($value instanceof RecordAccessorInterface) {
585
                $value->flushUpdates();
586
            }
587
        }
588
    }
589
590
    /**
591
     * {@inheritdoc}
592
     */
593
    public function isValid()
594
    {
595
        $this->validate();
596
597
        return empty($this->errors) && empty($this->nestedErrors);
598
    }
599
600
    /**
601
     * {@inheritdoc}
602
     */
603
    public function getErrors($reset = false)
604
    {
605
        return parent::getErrors($reset) + $this->nestedErrors;
606
    }
607
608
    /**
609
     * {@inheritdoc}
610
     */
611
    public function __isset($name)
612
    {
613
        if (isset($this->ormSchema[ORM::M_RELATIONS][$name])) {
614
            return !empty($this->relation($name)->getRelated());
615
        }
616
617
        return parent::__isset($name);
618
    }
619
620
    /**
621
     * {@inheritdoc}
622
     *
623
     * @throws RecordException
624
     */
625
    public function __unset($offset)
626
    {
627
        throw new FieldException("Records fields can not be unsetted.");
628
    }
629
630
    /**
631
     * {@inheritdoc}
632
     *
633
     * @see relation()
634
     */
635
    public function __get($offset)
636
    {
637
        if (isset($this->ormSchema[ORM::M_RELATIONS][$offset])) {
638
            //Bypassing call to relation
639
            return $this->relation($offset)->getRelated();
640
        }
641
642
        return $this->getField($offset, true);
643
    }
644
645
    /**
646
     * {@inheritdoc}
647
     *
648
     * @see relation()
649
     */
650
    public function __set($offset, $value)
651
    {
652
        if (isset($this->ormSchema[ORM::M_RELATIONS][$offset])) {
653
            //Bypassing call to relation
654
            $this->relation($offset)->associate($value);
655
656
            return;
657
        }
658
659
        $this->setField($offset, $value, true);
660
    }
661
662
    /**
663
     * Direct access to relation by it's name.
664
     *
665
     * @see relation()
666
     * @param string $method
667
     * @param array  $arguments
668
     * @return RelationInterface|mixed|AccessorInterface
669
     */
670
    public function __call($method, array $arguments)
671
    {
672
        if (isset($this->ormSchema[ORM::M_RELATIONS][$method])) {
673
            return $this->relation($method);
674
        }
675
676
        //See FIELD_FORMAT constant
677
        return parent::__call($method, $arguments);
678
    }
679
680
    /**
681
     * @return array
682
     */
683
    public function __debugInfo()
684
    {
685
        $info = [
686
            'table'     => $this->ormSchema[ORM::M_DB] . '/' . $this->ormSchema[ORM::M_TABLE],
687
            'pivotData' => $this->pivotData,
688
            'fields'    => $this->getFields(),
689
            'errors'    => $this->getErrors()
690
        ];
691
692
        if (empty($this->pivotData)) {
693
            unset($info['pivotData']);
694
        }
695
696
        return $info;
697
    }
698
699
    /**
700
     * Get associated Database\Table instance.
701
     *
702
     * @see save()
703
     * @see delete()
704
     * @return Table
705
     */
706
    protected function sourceTable()
707
    {
708
        return $this->orm->database($this->ormSchema[ORM::M_DB])->table(
709
            $this->ormSchema[ORM::M_TABLE]
710
        );
711
    }
712
713
    /**
714
     * Get WHERE array to be used to perform record data update or deletion. Usually will include
715
     * record primary key.
716
     *
717
     * @return array
718
     */
719
    protected function stateCriteria()
720
    {
721
        if (!empty($primaryKey = $this->ormSchema()[ORM::M_PRIMARY_KEY])) {
722
            return [$primaryKey => $this->primaryKey()];
723
        }
724
725
        //We have to serialize record data
726
        return $this->updates + $this->serializeData();
727
    }
728
729
    /**
730
     * {@inheritdoc}
731
     */
732
    protected function container()
733
    {
734
        if (empty($this->orm)) {
735
            return parent::container();
736
        }
737
738
        return $this->orm->container();
739
    }
740
741
    /**
742
     * Create set of fields to be sent to UPDATE statement.
743
     *
744
     * @internal
745
     * @todo make public, move to Record?
746
     * @todo create compileInsert twin?
747
     * @see save()
748
     * @return array
749
     */
750
    protected function compileUpdates()
751
    {
752
        if (!$this->hasUpdates() && !$this->isSolid()) {
753
            return [];
754
        }
755
756
        if ($this->isSolid()) {
757
            return $this->solidUpdate();
758
        }
759
760
        $updates = [];
761
        foreach ($this->fields as $field => $value) {
762 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...
763
                if ($value->hasUpdates()) {
764
                    $updates[$field] = $value->compileUpdates($field);
765
                    continue;
766
                }
767
768
                //Will be handled as normal update if needed
769
                $value = $value->serializeData();
770
            }
771
772
            if (array_key_exists($field, $this->updates)) {
773
                $updates[$field] = $value;
774
            }
775
        }
776
777
        //Primary key should not present in update set
778
        unset($updates[$this->ormSchema[ORM::M_PRIMARY_KEY]]);
779
780
        return $updates;
781
    }
782
783
    /**
784
     * {@inheritdoc}
785
     *
786
     * Will validate every loaded and embedded relation.
787
     */
788 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...
789
    {
790
        $this->nestedErrors = [];
791
792
        //Validating all compositions/accessors
793
        foreach ($this->fields as $field => $value) {
794
            //Ensuring value state
795
            $value = $this->getField($field);
796
            if (!$value instanceof ValidatesInterface) {
797
                continue;
798
            }
799
800
            if (!$value->isValid()) {
801
                $this->nestedErrors[$field] = $value->getErrors($reset);
802
            }
803
        }
804
805
        //We have to validate some relations before saving them
806
        $this->validateRelations($reset);
807
808
        parent::validate($reset);
809
810
        return empty($this->errors + $this->nestedErrors);
811
    }
812
813
    /**
814
     * {@inheritdoc}
815
     *
816
     * @see   Component::staticContainer()
817
     * @param array $fields Record fields to set, will be passed thought filters.
818
     * @param ORM   $orm    ORM component, global container will be called if not instance provided.
819
     * @event created()
820
     */
821 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...
822
    {
823
        /**
824
         * @var RecordEntity $record
825
         */
826
        $record = new static([], false, $orm);
827
828
        //Forcing validation (empty set of fields is not valid set of fields)
829
        $record->setFields($fields)->dispatch('created', new EntityEvent($record));
830
831
        return $record;
832
    }
833
834
    /**
835
     * Change record loaded state.
836
     *
837
     * @param bool|mixed $loader
838
     * @return $this
839
     */
840
    protected function loadedState($loader)
841
    {
842
        $this->loaded = $loader;
843
844
        return $this;
845
    }
846
847
    /**
848
     * Related and cached ORM schema.
849
     *
850
     * @internal
851
     * @return array
852
     */
853
    protected function ormSchema()
854
    {
855
        return $this->ormSchema;
856
    }
857
858
    /**
859
     * Check if relation is embedded.
860
     *
861
     * @internal
862
     * @param string $relation
863
     * @return bool
864
     */
865
    protected function isEmbedded($relation)
866
    {
867
        return !empty(
868
        $this->ormSchema[ORM::M_RELATIONS][$relation][ORM::R_DEFINITION][self::EMBEDDED_RELATION]
869
        );
870
    }
871
872
    /**
873
     * Full structure update.
874
     *
875
     * @return array
876
     */
877
    private function solidUpdate()
878
    {
879
        $updates = [];
880
        foreach ($this->fields as $field => $value) {
881 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...
882
                if ($value->hasUpdates()) {
883
                    $updates[$field] = $value->compileUpdates($field);
884
                } else {
885
                    $updates[$field] = $value->serializeData();
886
                }
887
                continue;
888
            }
889
890
            $updates[$field] = $value;
891
        }
892
893
        return $updates;
894
    }
895
896
    /**
897
     * Validate embedded relations.
898
     *
899
     * @param bool $reset
900
     */
901
    private function validateRelations($reset)
902
    {
903
        foreach ($this->relations as $name => $relation) {
904
            if (!$relation instanceof ValidatesInterface) {
905
                //Never constructed
906
                continue;
907
            }
908
909
            if ($this->isEmbedded($name) && !$relation->isValid()) {
910
                $this->nestedErrors[$name] = $relation->getErrors($reset);
911
            }
912
        }
913
    }
914
}
915