1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* components |
4
|
|
|
* |
5
|
|
|
* @author Wolfy-J |
6
|
|
|
*/ |
7
|
|
|
namespace Spiral\ORM; |
8
|
|
|
|
9
|
|
|
use Spiral\Core\Traits\SaturateTrait; |
10
|
|
|
use Spiral\Models\Traits\SolidableTrait; |
11
|
|
|
use Spiral\ORM\Commands\DeleteCommand; |
12
|
|
|
use Spiral\ORM\Commands\InsertCommand; |
13
|
|
|
use Spiral\ORM\Commands\NullCommand; |
14
|
|
|
use Spiral\ORM\Commands\UpdateCommand; |
15
|
|
|
use Spiral\ORM\Entities\RelationMap; |
16
|
|
|
use Spiral\ORM\Events\RecordEvent; |
17
|
|
|
use Spiral\ORM\Exceptions\RecordException; |
18
|
|
|
use Spiral\ORM\Exceptions\RelationException; |
19
|
|
|
|
20
|
|
|
/** |
21
|
|
|
* Provides ActiveRecord-less abstraction for carried data with ability to automatically apply |
22
|
|
|
* setters, getters, generate update, insert and delete sequences and access nested relations. |
23
|
|
|
* |
24
|
|
|
* Class implementations statically analyzed to define DB schema. |
25
|
|
|
* |
26
|
|
|
* @see RecordEntity::SCHEMA |
27
|
|
|
*/ |
28
|
|
|
abstract class RecordEntity extends AbstractRecord implements RecordInterface |
29
|
|
|
{ |
30
|
|
|
use SaturateTrait, SolidableTrait; |
31
|
|
|
|
32
|
|
|
/* |
33
|
|
|
* Begin set of behaviour and description constants. |
34
|
|
|
* ================================================ |
35
|
|
|
*/ |
36
|
|
|
|
37
|
|
|
/** |
38
|
|
|
* Default ORM relation types, see ORM configuration and documentation for more information. |
39
|
|
|
* |
40
|
|
|
* @see RelationSchemaInterface |
41
|
|
|
* @see RelationSchema |
42
|
|
|
*/ |
43
|
|
|
const HAS_ONE = 101; |
44
|
|
|
const HAS_MANY = 102; |
45
|
|
|
const BELONGS_TO = 103; |
46
|
|
|
const MANY_TO_MANY = 104; |
47
|
|
|
|
48
|
|
|
/** |
49
|
|
|
* Morphed relation types are usually created by inversion or equivalent of primary relation |
50
|
|
|
* types. |
51
|
|
|
* |
52
|
|
|
* @see RelationSchemaInterface |
53
|
|
|
* @see RelationSchema |
54
|
|
|
* @see MorphedRelation |
55
|
|
|
*/ |
56
|
|
|
const BELONGS_TO_MORPHED = 108; |
57
|
|
|
const MANY_TO_MORPHED = 109; |
58
|
|
|
|
59
|
|
|
/** |
60
|
|
|
* Constants used to declare relations in record schema, used in normalized relation schema. |
61
|
|
|
* |
62
|
|
|
* @see RelationSchemaInterface |
63
|
|
|
*/ |
64
|
|
|
const OUTER_KEY = 901; //Outer key name |
65
|
|
|
const INNER_KEY = 902; //Inner key name |
66
|
|
|
const MORPH_KEY = 903; //Morph key name |
67
|
|
|
const PIVOT_TABLE = 904; //Pivot table name |
68
|
|
|
const PIVOT_COLUMNS = 905; //Pre-defined pivot table columns |
69
|
|
|
const PIVOT_DEFAULTS = 906; //Pre-defined pivot table default values |
70
|
|
|
const THOUGHT_INNER_KEY = 907; //Pivot table options |
71
|
|
|
const THOUGHT_OUTER_KEY = 908; //Pivot table options |
72
|
|
|
const WHERE = 909; //Where conditions |
73
|
|
|
const WHERE_PIVOT = 910; //Where pivot conditions |
74
|
|
|
|
75
|
|
|
/** |
76
|
|
|
* Additional constants used to control relation schema behaviour. |
77
|
|
|
* |
78
|
|
|
* @see RecordEntity::SCHEMA |
79
|
|
|
* @see RelationSchemaInterface |
80
|
|
|
*/ |
81
|
|
|
const INVERSE = 1001; //Relation should be inverted to parent record |
82
|
|
|
const CREATE_CONSTRAINT = 1002; //Relation should create foreign keys (default) |
83
|
|
|
const CONSTRAINT_ACTION = 1003; //Default relation foreign key delete/update action (CASCADE) |
84
|
|
|
const CREATE_PIVOT = 1004; //Many-to-Many should create pivot table automatically (default) |
85
|
|
|
const NULLABLE = 1005; //Relation can be nullable (default) |
86
|
|
|
const CREATE_INDEXES = 1006; //Indication that relation is allowed to create required indexes |
87
|
|
|
const MORPHED_ALIASES = 1007; //Aliases for morphed sub-relations |
88
|
|
|
|
89
|
|
|
/** |
90
|
|
|
* Set of columns to be used in relation (attention, make sure that loaded records are set as |
91
|
|
|
* NON SOLID if you planning to modify their data). |
92
|
|
|
*/ |
93
|
|
|
const RELATION_COLUMNS = 1009; |
94
|
|
|
|
95
|
|
|
/** |
96
|
|
|
* Constants used to declare indexes in record schema. |
97
|
|
|
* |
98
|
|
|
* @see Record::INDEXES |
99
|
|
|
*/ |
100
|
|
|
const INDEX = 1000; //Default index type |
101
|
|
|
const UNIQUE = 2000; //Unique index definition |
102
|
|
|
|
103
|
|
|
/* |
104
|
|
|
* ================================================ |
105
|
|
|
* End set of behaviour and description constants. |
106
|
|
|
*/ |
107
|
|
|
|
108
|
|
|
/** |
109
|
|
|
* Model behaviour configurations. |
110
|
|
|
*/ |
111
|
|
|
const SECURED = '*'; |
112
|
|
|
const HIDDEN = []; |
113
|
|
|
const FILLABLE = []; |
114
|
|
|
const SETTERS = []; |
115
|
|
|
const GETTERS = []; |
116
|
|
|
const ACCESSORS = []; |
117
|
|
|
|
118
|
|
|
/** |
119
|
|
|
* Record relations and columns can be described in one place - record schema. |
120
|
|
|
* Attention: while defining table structure make sure that ACTIVE_SCHEMA constant is set to t |
121
|
|
|
* rue. |
122
|
|
|
* |
123
|
|
|
* Example: |
124
|
|
|
* const SCHEMA = [ |
125
|
|
|
* 'id' => 'primary', |
126
|
|
|
* 'name' => 'string', |
127
|
|
|
* 'biography' => 'text' |
128
|
|
|
* ]; |
129
|
|
|
* |
130
|
|
|
* You can pass additional options for some of your columns: |
131
|
|
|
* const SCHEMA = [ |
132
|
|
|
* 'pinCode' => 'string(128)', //String length |
133
|
|
|
* 'status' => 'enum(active, hidden)', //Enum values |
134
|
|
|
* 'balance' => 'decimal(10, 2)' //Decimal size and precision |
135
|
|
|
* ]; |
136
|
|
|
* |
137
|
|
|
* Every created column will be stated as NOT NULL with forced default value, if you want to |
138
|
|
|
* have nullable columns, specify special data key: protected $schema = [ |
139
|
|
|
* 'name' => 'string, nullable' |
140
|
|
|
* ]; |
141
|
|
|
* |
142
|
|
|
* You can easily combine table and relations definition in one schema: |
143
|
|
|
* const SCHEMA = [ |
144
|
|
|
* 'id' => 'bigPrimary', |
145
|
|
|
* 'name' => 'string', |
146
|
|
|
* 'email' => 'string', |
147
|
|
|
* 'phoneNumber' => 'string(32)', |
148
|
|
|
* |
149
|
|
|
* //Relations |
150
|
|
|
* 'profile' => [ |
151
|
|
|
* self::HAS_ONE => 'Records\Profile', |
152
|
|
|
* self::INVERSE => 'user' |
153
|
|
|
* ], |
154
|
|
|
* 'roles' => [ |
155
|
|
|
* self::MANY_TO_MANY => 'Records\Role', |
156
|
|
|
* self::INVERSE => 'users' |
157
|
|
|
* ] |
158
|
|
|
* ]; |
159
|
|
|
* |
160
|
|
|
* @var array |
161
|
|
|
*/ |
162
|
|
|
const SCHEMA = []; |
163
|
|
|
|
164
|
|
|
/** |
165
|
|
|
* Default field values. |
166
|
|
|
* |
167
|
|
|
* @var array |
168
|
|
|
*/ |
169
|
|
|
const DEFAULTS = []; |
170
|
|
|
|
171
|
|
|
/** |
172
|
|
|
* Set of indexes to be created for associated record table, indexes only created when record is |
173
|
|
|
* not abstract and has active schema set to true. |
174
|
|
|
* |
175
|
|
|
* Use constants INDEX and UNIQUE to describe indexes, you can also create compound indexes: |
176
|
|
|
* const INDEXES = [ |
177
|
|
|
* [self::UNIQUE, 'email'], |
178
|
|
|
* [self::INDEX, 'board_id'], |
179
|
|
|
* [self::INDEX, 'board_id', 'check_id'] |
180
|
|
|
* ]; |
181
|
|
|
* |
182
|
|
|
* @var array |
183
|
|
|
*/ |
184
|
|
|
const INDEXES = []; |
185
|
|
|
|
186
|
|
|
/** |
187
|
|
|
* Record state. |
188
|
|
|
* |
189
|
|
|
* @var int |
190
|
|
|
*/ |
191
|
|
|
private $state; |
192
|
|
|
|
193
|
|
|
/** |
194
|
|
|
* Points to last queued insert command for this entity, required to properly handle multiple |
195
|
|
|
* entity updates inside one transaction. |
196
|
|
|
* |
197
|
|
|
* @var InsertCommand |
198
|
|
|
*/ |
199
|
|
|
private $firstInsert = null; |
200
|
|
|
|
201
|
|
|
/** |
202
|
|
|
* Initiate entity inside or outside of ORM scope using given fields and state. |
203
|
|
|
* |
204
|
|
|
* @param array $data |
205
|
|
|
* @param int $state |
206
|
|
|
* @param ORMInterface|null $orm |
207
|
|
|
*/ |
208
|
|
|
public function __construct( |
209
|
|
|
array $data = [], |
210
|
|
|
int $state = ORMInterface::STATE_NEW, |
211
|
|
|
ORMInterface $orm = null |
212
|
|
|
) { |
213
|
|
|
//We can use global container as fallback if no default values were provided |
214
|
|
|
$orm = $this->saturate($orm, ORMInterface::class); |
215
|
|
|
|
216
|
|
|
$this->state = $state; |
217
|
|
|
|
218
|
|
|
//Non loaded records should be in solid state by default |
219
|
|
|
$this->solidState($this->state == ORMInterface::STATE_NEW); |
220
|
|
|
|
221
|
|
|
parent::__construct($orm, $data, new RelationMap($this, $orm)); |
222
|
|
|
} |
223
|
|
|
|
224
|
|
|
/** |
225
|
|
|
* Check if entity been loaded (non new). |
226
|
|
|
* |
227
|
|
|
* @return bool |
228
|
|
|
*/ |
229
|
|
|
public function isLoaded(): bool |
230
|
|
|
{ |
231
|
|
|
return $this->getState() != ORMInterface::STATE_NEW |
232
|
|
|
&& $this->getState() != ORMInterface::STATE_DELETED |
233
|
|
|
&& $this->getState() != ORMInterface::STATE_SCHEDULED_DELETE; |
234
|
|
|
} |
235
|
|
|
|
236
|
|
|
/** |
237
|
|
|
* Current model state. |
238
|
|
|
* |
239
|
|
|
* @return int |
240
|
|
|
*/ |
241
|
|
|
public function getState(): int |
242
|
|
|
{ |
243
|
|
|
return $this->state; |
244
|
|
|
} |
245
|
|
|
|
246
|
|
|
/** |
247
|
|
|
* {@inheritdoc} |
248
|
|
|
* |
249
|
|
|
* @param bool $queueRelations |
250
|
|
|
* |
251
|
|
|
* @throws RecordException |
252
|
|
|
* @throws RelationException |
253
|
|
|
*/ |
254
|
|
|
public function queueStore(bool $queueRelations = true): ContextualCommandInterface |
255
|
|
|
{ |
256
|
|
|
if (!$this->isLoaded()) { |
257
|
|
|
$command = $this->prepareInsert(); |
258
|
|
|
} else { |
259
|
|
|
$command = $this->prepareUpdate(); |
260
|
|
|
} |
261
|
|
|
|
262
|
|
|
//Reset all tracked entity changes |
263
|
|
|
$this->flushChanges(); |
264
|
|
|
|
265
|
|
|
//Relation commands |
266
|
|
|
if ($queueRelations) { |
267
|
|
|
//Queue relations before and after parent command (if needed) |
268
|
|
|
return $this->relations->queueRelations($command); |
269
|
|
|
} |
270
|
|
|
|
271
|
|
|
return $command; |
272
|
|
|
} |
273
|
|
|
|
274
|
|
|
/** |
275
|
|
|
* {@inheritdoc} |
276
|
|
|
* |
277
|
|
|
* @throws RecordException |
278
|
|
|
* @throws RelationException |
279
|
|
|
*/ |
280
|
|
|
public function queueDelete(): CommandInterface |
281
|
|
|
{ |
282
|
|
|
if (!$this->isLoaded()) { |
283
|
|
|
//Nothing to do |
284
|
|
|
return new NullCommand(); |
285
|
|
|
} |
286
|
|
|
|
287
|
|
|
return $this->prepareDelete(); |
288
|
|
|
} |
289
|
|
|
|
290
|
|
|
/** |
291
|
|
|
* @return InsertCommand |
292
|
|
|
*/ |
293
|
|
|
private function prepareInsert(): InsertCommand |
294
|
|
|
{ |
295
|
|
|
$data = $this->packValue(); |
296
|
|
|
unset($data[$this->primaryColumn()]); |
297
|
|
|
|
298
|
|
|
$command = new InsertCommand($this->orm->table(static::class), $data); |
299
|
|
|
|
300
|
|
|
//Entity indicates it's own status |
301
|
|
|
$this->state = ORMInterface::STATE_SCHEDULED_INSERT; |
302
|
|
|
$this->dispatch('insert', new RecordEvent($this, $command)); |
|
|
|
|
303
|
|
|
|
304
|
|
|
//Executed when transaction successfully completed |
305
|
|
|
$command->onComplete(function (InsertCommand $command) { |
306
|
|
|
$this->handleInsert($command); |
307
|
|
|
}); |
308
|
|
|
|
309
|
|
|
$command->onRollBack(function () { |
310
|
|
|
//Flushing existed insert command to prevent collisions |
311
|
|
|
$this->firstInsert = null; |
312
|
|
|
}); |
313
|
|
|
|
314
|
|
|
//Keep reference to the last insert command |
315
|
|
|
return $this->firstInsert = $command; |
316
|
|
|
} |
317
|
|
|
|
318
|
|
|
/** |
319
|
|
|
* @return UpdateCommand |
320
|
|
|
*/ |
321
|
|
|
private function prepareUpdate(): UpdateCommand |
322
|
|
|
{ |
323
|
|
|
$command = new UpdateCommand( |
324
|
|
|
$this->orm->table(static::class), |
325
|
|
|
[$this->primaryColumn() => $this->primaryKey()], |
326
|
|
|
$this->packChanges(true) |
327
|
|
|
); |
328
|
|
|
|
329
|
|
|
if (!empty($this->firstInsert)) { |
330
|
|
|
$this->firstInsert->onExecute(function (InsertCommand $insert) use ($command) { |
331
|
|
|
//Sync primary key values |
332
|
|
|
$command->setWhere([$this->primaryColumn() => $insert->getInsertID()]); |
333
|
|
|
}); |
334
|
|
|
} |
335
|
|
|
|
336
|
|
|
//Entity indicates it's own status |
337
|
|
|
$this->state = ORMInterface::STATE_SCHEDULED_UPDATE; |
338
|
|
|
$this->dispatch('update', new RecordEvent($this)); |
339
|
|
|
|
340
|
|
|
//Executed when transaction successfully completed |
341
|
|
|
$command->onComplete(function (UpdateCommand $command) { |
342
|
|
|
$this->handleUpdate($command); |
343
|
|
|
}); |
344
|
|
|
|
345
|
|
|
return $command; |
346
|
|
|
} |
347
|
|
|
|
348
|
|
|
/** |
349
|
|
|
* @return DeleteCommand |
350
|
|
|
*/ |
351
|
|
|
private function prepareDelete(): DeleteCommand |
352
|
|
|
{ |
353
|
|
|
$command = new DeleteCommand( |
354
|
|
|
$this->orm->table(static::class), |
355
|
|
|
[$this->primaryColumn() => $this->primaryKey()] |
356
|
|
|
); |
357
|
|
|
|
358
|
|
|
if (!empty($this->firstInsert)) { |
359
|
|
|
$this->firstInsert->onExecute(function (InsertCommand $insert) use ($command) { |
360
|
|
|
//Sync primary key values |
361
|
|
|
$command->setWhere([$this->primaryColumn() => $insert->getInsertID()]); |
362
|
|
|
}); |
363
|
|
|
} |
364
|
|
|
|
365
|
|
|
//Entity indicates it's own status |
366
|
|
|
$this->state = ORMInterface::STATE_SCHEDULED_DELETE; |
367
|
|
|
$this->dispatch('delete', new RecordEvent($this)); |
368
|
|
|
|
369
|
|
|
//Executed when transaction successfully completed |
370
|
|
|
$command->onComplete(function (DeleteCommand $command) { |
371
|
|
|
$this->handleDelete($command); |
372
|
|
|
}); |
373
|
|
|
|
374
|
|
|
return $command; |
375
|
|
|
} |
376
|
|
|
|
377
|
|
|
/** |
378
|
|
|
* Handle result of insert command. |
379
|
|
|
* |
380
|
|
|
* @param InsertCommand $command |
381
|
|
|
*/ |
382
|
|
|
private function handleInsert(InsertCommand $command) |
383
|
|
|
{ |
384
|
|
|
//Flushing reference to last insert command |
385
|
|
|
$this->firstInsert = null; |
386
|
|
|
|
387
|
|
|
//Mounting PK |
388
|
|
|
$this->setField($this->primaryColumn(), $command->getInsertID(), true, false); |
389
|
|
|
|
390
|
|
|
//Once command executed we will know some information about it's context (for exampled added FKs) |
391
|
|
|
foreach ($command->getContext() as $name => $value) { |
392
|
|
|
$this->setField($name, $value, true, false); |
393
|
|
|
} |
394
|
|
|
|
395
|
|
|
$this->state = ORMInterface::STATE_LOADED; |
396
|
|
|
$this->dispatch('created', new RecordEvent($this)); |
397
|
|
|
} |
398
|
|
|
|
399
|
|
|
/** |
400
|
|
|
* Handle result of update command. |
401
|
|
|
* |
402
|
|
|
* @param UpdateCommand $command |
403
|
|
|
*/ |
404
|
|
|
private function handleUpdate(UpdateCommand $command) |
405
|
|
|
{ |
406
|
|
|
//Once command executed we will know some information about it's context (for exampled added FKs) |
407
|
|
|
foreach ($command->getContext() as $name => $value) { |
408
|
|
|
$this->setField($name, $value, true, false); |
409
|
|
|
} |
410
|
|
|
|
411
|
|
|
$this->state = ORMInterface::STATE_LOADED; |
412
|
|
|
$this->dispatch('updated', new RecordEvent($this)); |
413
|
|
|
} |
414
|
|
|
|
415
|
|
|
/** |
416
|
|
|
* Handle result of delete command. |
417
|
|
|
* |
418
|
|
|
* @param DeleteCommand $command |
419
|
|
|
*/ |
420
|
|
|
private function handleDelete(DeleteCommand $command) |
421
|
|
|
{ |
422
|
|
|
$this->state = ORMInterface::STATE_DELETED; |
423
|
|
|
$this->dispatch('deleted', new RecordEvent($this)); |
424
|
|
|
} |
425
|
|
|
} |
426
|
|
|
|
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.