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); |
|
|
|
|
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)) { |
|
|
|
|
294
|
|
|
return $this->relations->get($name); |
|
|
|
|
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)) { |
|
|
|
|
310
|
|
|
//Would not work with relations which do not represent singular entities |
311
|
|
|
return $this->relations->set($name, $value); |
|
|
|
|
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)) { |
|
|
|
|
326
|
|
|
return true; |
327
|
|
|
} |
328
|
|
|
|
329
|
|
|
return parent::__isset($name); |
|
|
|
|
330
|
|
|
} |
331
|
|
|
|
332
|
|
|
/** |
333
|
|
|
* {@inheritdoc} |
334
|
|
|
* |
335
|
|
|
* @throws FieldException |
336
|
|
|
*/ |
337
|
|
|
public function __unset($offset) |
338
|
|
|
{ |
339
|
|
|
if ($this->relations->exists($offset)) { |
|
|
|
|
340
|
|
|
$this->relations->delete($offset); |
|
|
|
|
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) { |
|
|
|
|
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(), |
|
|
|
|
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 () { |
|
|
|
|
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(), |
|
|
|
|
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 () { |
|
|
|
|
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(), |
|
|
|
|
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 () { |
|
|
|
|
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
|
|
|
} |
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.