Completed
Branch feature/pre-split (af0512)
by Anton
03:32
created

RecordEntity   C

Complexity

Total Complexity 61

Size/Duplication

Total Lines 650
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 13

Importance

Changes 0
Metric Value
dl 0
loc 650
rs 5.5667
c 0
b 0
f 0
wmc 61
lcom 1
cbo 13

22 Methods

Rating   Name   Duplication   Size   Complexity  
B __construct() 0 25 3
A isLoaded() 0 4 1
A getState() 0 4 1
A getField() 0 10 2
A setField() 0 12 2
A hasField() 0 8 2
A __unset() 0 14 3
D hasUpdates() 0 31 9
B queueSave() 0 24 6
A queueDelete() 0 9 3
A __debugInfo() 0 9 1
A createAccessor() 0 9 1
A iocContainer() 0 9 2
A setState() 0 4 1
A prepareInsert() 0 21 1
A prepareUpdate() 0 22 1
A prepareDelete() 0 20 1
A stateCriteria() 0 16 3
D compileUpdates() 0 38 10
A flushUpdates() 0 10 3
A registerChange() 0 11 3
A assertField() 0 10 2

How to fix   Complexity   

Complex Class

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
 * components
4
 *
5
 * @author    Wolfy-J
6
 */
7
namespace Spiral\ORM;
8
9
use Spiral\Core\Component;
10
use Spiral\Core\Traits\SaturateTrait;
11
use Spiral\Models\AccessorInterface;
12
use Spiral\Models\SchematicEntity;
13
use Spiral\Models\Traits\SolidableTrait;
14
use Spiral\ORM\Commands\DeleteCommand;
15
use Spiral\ORM\Commands\InsertCommand;
16
use Spiral\ORM\Commands\NullCommand;
17
use Spiral\ORM\Commands\UpdateCommand;
18
use Spiral\ORM\Entities\RelationBucket;
19
use Spiral\ORM\Events\RecordEvent;
20
use Spiral\ORM\Exceptions\FieldException;
21
22
/**
23
 * Provides ActiveRecord-less abstraction for carried data with ability to automatically apply
24
 * setters, getters, generate update, insert and delete sequences and access nested relations.
25
 *
26
 * Class implementations statically analyzed to define DB schema.
27
 *
28
 * @see RecordEntity::SCHEMA
29
 *
30
 * Potentially requires split for StateWatcher.
31
 */
32
abstract class RecordEntity extends SchematicEntity implements RecordInterface
33
{
34
    use SaturateTrait, SolidableTrait;
35
36
    /*
37
     * Begin set of behaviour and description constants.
38
     * ================================================
39
     */
40
41
    /**
42
     * Set of schema sections needed to describe entity behaviour.
43
     */
44
    const SH_PRIMARIES = 0;
45
    const SH_DEFAULTS  = 1;
46
    const SH_RELATIONS = 6;
47
48
    /**
49
     * Default ORM relation types, see ORM configuration and documentation for more information.
50
     *
51
     * @see RelationSchemaInterface
52
     * @see RelationSchema
53
     */
54
    const HAS_ONE      = 101;
55
    const HAS_MANY     = 102;
56
    const BELONGS_TO   = 103;
57
    const MANY_TO_MANY = 104;
58
59
    /**
60
     * Morphed relation types are usually created by inversion or equivalent of primary relation
61
     * types.
62
     *
63
     * @see RelationSchemaInterface
64
     * @see RelationSchema
65
     * @see MorphedRelation
66
     */
67
    const BELONGS_TO_MORPHED = 108;
68
    const MANY_TO_MORPHED    = 109;
69
70
    /**
71
     * Constants used to declare relations in record schema, used in normalized relation schema.
72
     *
73
     * @see RelationSchemaInterface
74
     */
75
    const OUTER_KEY         = 901; //Outer key name
76
    const INNER_KEY         = 902; //Inner key name
77
    const MORPH_KEY         = 903; //Morph key name
78
    const PIVOT_TABLE       = 904; //Pivot table name
79
    const PIVOT_COLUMNS     = 905; //Pre-defined pivot table columns
80
    const PIVOT_DEFAULTS    = 906; //Pre-defined pivot table default values
81
    const THOUGHT_INNER_KEY = 907; //Pivot table options
82
    const THOUGHT_OUTER_KEY = 908; //Pivot table options
83
    const WHERE             = 909; //Where conditions
84
    const WHERE_PIVOT       = 910; //Where pivot conditions
85
86
    /**
87
     * Additional constants used to control relation schema behaviour.
88
     *
89
     * @see RecordEntity::SCHEMA
90
     * @see RelationSchemaInterface
91
     */
92
    const INVERSE           = 1001; //Relation should be inverted to parent record
93
    const CREATE_CONSTRAINT = 1002; //Relation should create foreign keys (default)
94
    const CONSTRAINT_ACTION = 1003; //Default relation foreign key delete/update action (CASCADE)
95
    const CREATE_PIVOT      = 1004; //Many-to-Many should create pivot table automatically (default)
96
    const NULLABLE          = 1005; //Relation can be nullable (default)
97
    const CREATE_INDEXES    = 1006; //Indication that relation is allowed to create required indexes
98
    const MORPHED_ALIASES   = 1007; //Aliases for morphed sub-relations
99
100
    /**
101
     * Set of columns to be used in relation (attention, make sure that loaded records are set as
102
     * NON SOLID if you planning to modify their data).
103
     */
104
    const RELATION_COLUMNS = 1009;
105
106
    /**
107
     * Constants used to declare indexes in record schema.
108
     *
109
     * @see Record::INDEXES
110
     */
111
    const INDEX  = 1000;            //Default index type
112
    const UNIQUE = 2000;            //Unique index definition
113
114
    /*
115
     * ================================================
116
     * End set of behaviour and description constants.
117
     */
118
119
    /**
120
     * Model behaviour configurations.
121
     */
122
    const SECURED   = '*';
123
    const HIDDEN    = [];
124
    const FILLABLE  = [];
125
    const SETTERS   = [];
126
    const GETTERS   = [];
127
    const ACCESSORS = [];
128
129
    /**
130
     * Record relations and columns can be described in one place - record schema.
131
     * Attention: while defining table structure make sure that ACTIVE_SCHEMA constant is set to t
132
     * rue.
133
     *
134
     * Example:
135
     * const SCHEMA = [
136
     *      'id'        => 'primary',
137
     *      'name'      => 'string',
138
     *      'biography' => 'text'
139
     * ];
140
     *
141
     * You can pass additional options for some of your columns:
142
     * const SCHEMA = [
143
     *      'pinCode' => 'string(128)',         //String length
144
     *      'status'  => 'enum(active, hidden)', //Enum values
145
     *      'balance' => 'decimal(10, 2)'       //Decimal size and precision
146
     * ];
147
     *
148
     * Every created column will be stated as NOT NULL with forced default value, if you want to
149
     * have nullable columns, specify special data key: protected $schema = [
150
     *      'name'      => 'string, nullable'
151
     * ];
152
     *
153
     * You can easily combine table and relations definition in one schema:
154
     * const SCHEMA = [
155
     *      'id'          => 'bigPrimary',
156
     *      'name'        => 'string',
157
     *      'email'       => 'string',
158
     *      'phoneNumber' => 'string(32)',
159
     *
160
     *      //Relations
161
     *      'profile'     => [
162
     *          self::HAS_ONE => 'Records\Profile',
163
     *          self::INVERSE => 'user'
164
     *      ],
165
     *      'roles'       => [
166
     *          self::MANY_TO_MANY => 'Records\Role',
167
     *          self::INVERSE => 'users'
168
     *      ]
169
     * ];
170
     *
171
     * @var array
172
     */
173
    const SCHEMA = [];
174
175
    /**
176
     * Default field values.
177
     *
178
     * @var array
179
     */
180
    const DEFAULTS = [];
181
182
    /**
183
     * Set of indexes to be created for associated record table, indexes only created when record is
184
     * not abstract and has active schema set to true.
185
     *
186
     * Use constants INDEX and UNIQUE to describe indexes, you can also create compound indexes:
187
     * const INDEXES = [
188
     *      [self::UNIQUE, 'email'],
189
     *      [self::INDEX, 'board_id'],
190
     *      [self::INDEX, 'board_id', 'check_id']
191
     * ];
192
     *
193
     * @var array
194
     */
195
    const INDEXES = [];
196
197
    /**
198
     * Record behaviour definition.
199
     *
200
     * @var array
201
     */
202
    private $recordSchema = [];
203
204
    /**
205
     * Record state.
206
     *
207
     * @var int
208
     */
209
    private $state;
210
211
    /**
212
     * Record field updates (changed values). This array contain set of initial property values if
213
     * any of them changed.
214
     *
215
     * @var array
216
     */
217
    private $changes = [];
218
219
    /**
220
     * AssociatedRelation bucket. Manages declared record relations.
221
     *
222
     * @var RelationBucket
223
     */
224
    private $relations;
225
226
    /**
227
     * Parent ORM instance, responsible for relation initialization and lazy loading operations.
228
     *
229
     * @invisible
230
     * @var ORMInterface
231
     */
232
    protected $orm;
233
234
    /**
235
     * Initiate entity inside or outside of ORM scope using given fields and state.
236
     *
237
     * @param array             $data
238
     * @param int               $state
239
     * @param ORMInterface|null $orm
240
     * @param array|null        $schema
241
     */
242
    public function __construct(
243
        array $data = [],
244
        int $state = ORMInterface::STATE_NEW,
245
        ORMInterface $orm = null,
246
        array $schema = null
247
    ) {//We can use global container as fallback if no default values were provided
248
        $this->orm = $this->saturate($orm, ORMInterface::class);
249
250
        //Use supplied schema or fetch one from ORM
251
        $this->recordSchema = !empty($schema) ? $schema : $this->orm->define(
252
            static::class,
253
            ORMInterface::R_SCHEMA
254
        );
255
256
        $this->state = $state;
257
        if ($this->state == ORMInterface::STATE_NEW) {
258
            //Non loaded records should be in solid state by default
259
            $this->solidState(true);
260
        }
261
262
        $this->relations = new RelationBucket($this, $this->orm);
0 ignored issues
show
Unused Code introduced by
The call to RelationBucket::__construct() has too many arguments starting with $this.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
263
        $this->relations->extractRelations($data);
264
265
        parent::__construct($data + $this->recordSchema[self::SH_DEFAULTS], $this->recordSchema);
266
    }
267
268
    /**
269
     * Check if entity been loaded (non new).
270
     *
271
     * @return bool
272
     */
273
    public function isLoaded(): bool
274
    {
275
        return $this->state != ORMInterface::STATE_NEW;
276
    }
277
278
    /**
279
     * Current model state.
280
     *
281
     * @return int
282
     */
283
    public function getState(): int
284
    {
285
        return $this->state;
286
    }
287
288
    /**
289
     * {@inheritdoc}
290
     */
291
    public function getField(string $name, $default = null, bool $filter = true)
292
    {
293
        if ($this->relations->exists($name)) {
0 ignored issues
show
Bug introduced by
The method exists() does not seem to exist on object<Spiral\ORM\Entities\RelationBucket>.

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...
294
            return $this->relations->get($name);
0 ignored issues
show
Bug introduced by
The method get() does not seem to exist on object<Spiral\ORM\Entities\RelationBucket>.

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...
295
        }
296
297
        $this->assertField($name);
298
299
        return parent::getField($name, $default, $filter);
300
    }
301
302
    /**
303
     * {@inheritdoc}
304
     *
305
     * Tracks field changes.
306
     */
307
    public function setField(string $name, $value, bool $filter = true)
308
    {
309
        if ($this->relations->exists($name)) {
0 ignored issues
show
Bug introduced by
The method exists() does not seem to exist on object<Spiral\ORM\Entities\RelationBucket>.

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...
310
            //Would not work with relations which do not represent singular entities
311
            return $this->relations->set($name, $value);
0 ignored issues
show
Bug introduced by
The method set() does not seem to exist on object<Spiral\ORM\Entities\RelationBucket>.

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...
312
        }
313
314
        $this->assertField($name);
315
        $this->registerChange($name);
316
317
        parent::setField($name, $value, $filter);
318
    }
319
320
    /**
321
     * {@inheritdoc}
322
     */
323
    public function hasField(string $name): bool
324
    {
325
        if ($this->relations->exists($name)) {
0 ignored issues
show
Bug introduced by
The method exists() does not seem to exist on object<Spiral\ORM\Entities\RelationBucket>.

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...
326
            return true;
327
        }
328
329
        return parent::__isset($name);
0 ignored issues
show
Comprehensibility Bug introduced by
It seems like you call parent on a different method (__isset() instead of hasField()). Are you sure this is correct? If so, you might want to change this to $this->__isset().

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...
330
    }
331
332
    /**
333
     * {@inheritdoc}
334
     *
335
     * @throws FieldException
336
     */
337
    public function __unset($offset)
338
    {
339
        if ($this->relations->exists($offset)) {
0 ignored issues
show
Bug introduced by
The method exists() does not seem to exist on object<Spiral\ORM\Entities\RelationBucket>.

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...
340
            $this->relations->delete($offset);
0 ignored issues
show
Bug introduced by
The method delete() does not seem to exist on object<Spiral\ORM\Entities\RelationBucket>.

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...
341
342
            return;
343
        }
344
345
        if (!$this->isNullable($offset)) {
346
            throw new FieldException("Unable to unset not nullable field '{$offset}'");
347
        }
348
349
        $this->setField($offset, null, false);
350
    }
351
352
    /**
353
     * {@inheritdoc}
354
     *
355
     * Method does not check updates in nested relation, but only in primary record.
356
     *
357
     * @param string $field Check once specific field changes.
358
     */
359
    public function hasUpdates(string $field = null): bool
360
    {
361
        //Check updates for specific field
362
        if (!empty($field)) {
363
            if (array_key_exists($field, $this->changes)) {
364
                return true;
365
            }
366
367
            //Do not force accessor creation
368
            $value = $this->getField($field, null, false);
369
            if ($value instanceof RecordAccessorInterface && $value->hasUpdates()) {
370
                return true;
371
            }
372
373
            return false;
374
        }
375
376
        if (!empty($this->changes)) {
377
            return true;
378
        }
379
380
        //Do not force accessor creation
381
        foreach ($this->getFields(false) as $value) {
382
            //Checking all fields for changes (handled internally)
383
            if ($value instanceof RecordAccessorInterface && $value->hasUpdates()) {
384
                return true;
385
            }
386
        }
387
388
        return false;
389
    }
390
391
    /**
392
     * {@inheritdoc}
393
     *
394
     * @param bool $queueRelations
395
     */
396
    public function queueSave(bool $queueRelations = true): CommandInterface
397
    {
398
        if ($this->state == ORMInterface::STATE_READONLY) {
399
            //Nothing to do on readonly entities
400
            return new NullCommand();
401
        }
402
403
        if (!$this->isLoaded()) {
404
            $command = $this->prepareInsert();
405
        } else {
406
            if ($this->hasUpdates() || $this->solidState) {
407
                $command = $this->prepareUpdate();
408
            } else {
409
                $command = new NullCommand();
410
            }
411
        }
412
413
        //Relation commands
414
        if ($queueRelations) {
0 ignored issues
show
Unused Code introduced by
This if statement is empty and can be removed.

This check looks for the bodies of if statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These if bodies can be removed. If you have an empty if but statements in the else branch, consider inverting the condition.

if (rand(1, 6) > 3) {
//print "Check failed";
} else {
    print "Check succeeded";
}

could be turned into

if (rand(1, 6) <= 3) {
    print "Check succeeded";
}

This is much more concise to read.

Loading history...
415
            //This is magical part
416
        }
417
418
        return $command;
419
    }
420
421
    /**
422
     * {@inheritdoc}
423
     */
424
    public function queueDelete(): CommandInterface
425
    {
426
        if ($this->state == ORMInterface::STATE_READONLY || !$this->isLoaded()) {
427
            //Nothing to do
428
            return new NullCommand();
429
        }
430
431
        return $this->prepareDelete();
432
    }
433
434
    /**
435
     * @return array
436
     */
437
    public function __debugInfo()
438
    {
439
        return [
440
            'database'  => $this->orm->define(static::class, ORMInterface::R_DATABASE),
441
            'table'     => $this->orm->define(static::class, ORMInterface::R_TABLE),
442
            'fields'    => $this->getFields(),
443
            'relations' => $this->relations
444
        ];
445
    }
446
447
    /**
448
     * {@inheritdoc}
449
     *
450
     * DocumentEntity will pass ODM instance as part of accessor context.
451
     *
452
     * @see CompositionDefinition
453
     */
454
    protected function createAccessor(
455
        $accessor,
456
        string $name,
457
        $value,
458
        array $context = []
459
    ): AccessorInterface {
460
        //Giving ORM as context
461
        return parent::createAccessor($accessor, $name, $value, $context + ['orm' => $this->orm]);
462
    }
463
464
    /**
465
     * {@inheritdoc}
466
     */
467
    protected function iocContainer()
468
    {
469
        if ($this->orm instanceof Component) {
470
            //Forwarding IoC scope to parent ORM instance
471
            return $this->orm->iocContainer();
472
        }
473
474
        return parent::iocContainer();
475
    }
476
477
    /*
478
     * Code below used to generate transaction commands.
479
     */
480
481
    /**
482
     * Change object state.
483
     *
484
     * @param int $state
485
     */
486
    private function setState(int $state)
487
    {
488
        $this->state = $state;
489
    }
490
491
    /**
492
     * @return InsertCommand
493
     */
494
    private function prepareInsert(): InsertCommand
495
    {
496
        //Entity indicates it's own status
497
        $this->setState(ORMInterface::STATE_SCHEDULED_INSERT);
498
        $this->dispatch('insert', new RecordEvent($this));
499
500
        $command = new InsertCommand(
501
            $this->packValue(),
0 ignored issues
show
Unused Code introduced by
The call to InsertCommand::__construct() has too many arguments starting with $this->packValue().

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
502
            $this->orm->define(static::class, ORMInterface::R_DATABASE),
503
            $this->orm->define(static::class, ORMInterface::R_TABLE)
504
        );
505
506
        //Executed when transaction successfully completed
507
        $command->onComplete(function () {
0 ignored issues
show
Bug introduced by
The method onComplete() does not seem to exist on object<Spiral\ORM\Commands\InsertCommand>.

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...
508
            $this->setState(ORMInterface::STATE_LOADED);
509
            $this->flushUpdates();
510
            $this->dispatch('created', new RecordEvent($this));
511
        });
512
513
        return $command;
514
    }
515
516
    /**
517
     * @return UpdateCommand
518
     */
519
    private function prepareUpdate(): UpdateCommand
520
    {
521
        //Entity indicates it's own status
522
        $this->setState(ORMInterface::STATE_SCHEDULED_UPDATE);
523
        $this->dispatch('update', new RecordEvent($this));
524
525
        $command = new UpdateCommand(
526
            $this->stateCriteria(),
0 ignored issues
show
Unused Code introduced by
The call to UpdateCommand::__construct() has too many arguments starting with $this->stateCriteria().

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
527
            $this->compileUpdates(true),
528
            $this->orm->define(static::class, ORMInterface::R_DATABASE),
529
            $this->orm->define(static::class, ORMInterface::R_TABLE)
530
        );
531
532
        //Executed when transaction successfully completed
533
        $command->onComplete(function () {
0 ignored issues
show
Bug introduced by
The method onComplete() does not seem to exist on object<Spiral\ORM\Commands\UpdateCommand>.

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...
534
            $this->setState(ORMInterface::STATE_LOADED);
535
            $this->flushUpdates();
536
            $this->dispatch('updated', new RecordEvent($this));
537
        });
538
539
        return $command;
540
    }
541
542
    /**
543
     * @return DeleteCommand
544
     */
545
    private function prepareDelete(): DeleteCommand
546
    {
547
        //Entity indicates it's own status
548
        $this->setState(ORMInterface::STATE_SCHEDULED_DELETE);
549
        $this->dispatch('delete', new RecordEvent($this));
550
551
        $command = new DeleteCommand(
552
            $this->stateCriteria(),
0 ignored issues
show
Unused Code introduced by
The call to DeleteCommand::__construct() has too many arguments starting with $this->stateCriteria().

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
553
            $this->orm->define(static::class, ORMInterface::R_DATABASE),
554
            $this->orm->define(static::class, ORMInterface::R_TABLE)
555
        );
556
557
        //Executed when transaction successfully completed
558
        $command->onComplete(function () {
0 ignored issues
show
Bug introduced by
The method onComplete() does not seem to exist on object<Spiral\ORM\Commands\DeleteCommand>.

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...
559
            $this->setState(ORMInterface::STATE_DELETED);
560
            $this->dispatch('deleted', new RecordEvent($this));
561
        });
562
563
        return $command;
564
    }
565
566
    /**
567
     * Get WHERE array to be used to perform record data update or deletion. Usually will include
568
     * record primary key.
569
     *
570
     * Usually just [ID => value] array.
571
     *
572
     * @return array
573
     */
574
    private function stateCriteria()
575
    {
576
        if (!empty($primaryKey = $this->recordSchema[self::SH_PRIMARIES])) {
577
578
            //Set of primary keys
579
            $state = [];
580
            foreach ($primaryKey as $key) {
581
                $state[$key] = $this->getField($key);
582
            }
583
584
            return $state;
585
        }
586
587
        //Use entity data as where definition
588
        return $this->changes + $this->packValue();
589
    }
590
591
    /**
592
     * Create set of fields to be sent to UPDATE statement.
593
     *
594
     * @param bool $skipPrimaries Remove primary keys from update statement.
595
     *
596
     * @return array
597
     */
598
    private function compileUpdates(bool $skipPrimaries = false): array
599
    {
600
        if (!$this->hasUpdates() && !$this->isSolid()) {
601
            return [];
602
        }
603
604
        if ($this->isSolid()) {
605
            //Solid records always saved as one chunk of data
606
            return $this->packValue();
607
        }
608
609
        $updates = [];
610
        foreach ($this->getFields(false) as $field => $value) {
611
            if (
612
                $skipPrimaries
613
                && in_array($field, $this->recordSchema[self::SH_PRIMARIES])
614
            ) {
615
                continue;
616
            }
617
618
            //Handled by sub-accessor
619
            if ($value instanceof RecordAccessorInterface) {
620
                if ($value->hasUpdates()) {
621
                    $updates[$field] = $value->compileUpdates($field);
622
                    continue;
623
                }
624
625
                $value = $value->packValue();
626
            }
627
628
            //Field change registered
629
            if (array_key_exists($field, $this->changes)) {
630
                $updates[$field] = $value;
631
            }
632
        }
633
634
        return $updates;
635
    }
636
637
    /**
638
     * Indicate that all updates done, reset dirty state.
639
     */
640
    private function flushUpdates()
641
    {
642
        $this->changes = [];
643
644
        foreach ($this->getFields(false) as $field => $value) {
645
            if ($value instanceof RecordAccessorInterface) {
646
                $value->flushUpdates();
647
            }
648
        }
649
    }
650
651
    /**
652
     * @param string $name
653
     */
654
    private function registerChange(string $name)
655
    {
656
        $original = $this->getField($name, null, false);
657
658
        if (!array_key_exists($name, $this->changes)) {
659
            //Let's keep track of how field looked before first change
660
            $this->changes[$name] = $original instanceof AccessorInterface
661
                ? $original->packValue()
662
                : $original;
663
        }
664
    }
665
666
    /**
667
     * @param string $name
668
     *
669
     * @throws FieldException
670
     */
671
    private function assertField(string $name)
672
    {
673
        if (!$this->hasField($name)) {
674
            throw new FieldException(sprintf(
675
                "No such property '%s' in '%s', check schema being relevant",
676
                $name,
677
                get_called_class()
678
            ));
679
        }
680
    }
681
}