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\Component; |
11
|
|
|
use Spiral\Core\Exceptions\SugarException; |
12
|
|
|
use Spiral\Core\Traits\SaturateTrait; |
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
|
|
|
* RecordEntity 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
|
|
|
* Configuration properties: |
30
|
|
|
* - schema |
31
|
|
|
* - defaults |
32
|
|
|
* - secured (* by default) |
33
|
|
|
* - fillable |
34
|
|
|
* - validates |
35
|
|
|
* - database |
36
|
|
|
* - table |
37
|
|
|
* - indexes |
38
|
|
|
* |
39
|
|
|
* @todo: Add ability to set primary key manually, for example fpr uuid like fields. |
40
|
|
|
*/ |
41
|
|
|
class RecordEntity extends SchematicEntity implements RecordInterface |
42
|
|
|
{ |
43
|
|
|
use SaturateTrait; |
44
|
|
|
|
45
|
|
|
/** |
46
|
|
|
* We are going to inherit parent validation rules, this will let spiral translator know about |
47
|
|
|
* it and merge i18n messages. |
48
|
|
|
* |
49
|
|
|
* @see TranslatorTrait |
50
|
|
|
*/ |
51
|
|
|
const I18N_INHERIT_MESSAGES = true; |
52
|
|
|
|
53
|
|
|
/** |
54
|
|
|
* Field format declares how entity must process magic setters and getters. Available values: |
55
|
|
|
* camelCase, tableize. |
56
|
|
|
*/ |
57
|
|
|
const FIELD_FORMAT = 'tableize'; |
58
|
|
|
|
59
|
|
|
/** |
60
|
|
|
* ORM records are be divided by two sections: active and passive records. When record is active |
61
|
|
|
* ORM allowed to modify associated record table using declared schema and created relations. |
62
|
|
|
* |
63
|
|
|
* Passive records (ACTIVE_SCHEMA = false) however can only read table schema from database and |
64
|
|
|
* forbidden to do any schema modification either by record or by relations. |
65
|
|
|
* |
66
|
|
|
* You can use ACTIVE_SCHEMA = false in cases where you need to create an ActiveRecord for |
67
|
|
|
* existed table. |
68
|
|
|
* |
69
|
|
|
* @see RecordSchema |
70
|
|
|
* @see \Spiral\ORM\Entities\SchemaBuilder |
71
|
|
|
*/ |
72
|
|
|
const ACTIVE_SCHEMA = true; |
73
|
|
|
|
74
|
|
|
/** |
75
|
|
|
* Indication that record were deleted. |
76
|
|
|
*/ |
77
|
|
|
const DELETED = 900; |
78
|
|
|
|
79
|
|
|
/** |
80
|
|
|
* Default ORM relation types, see ORM configuration and documentation for more information, |
81
|
|
|
* i had to remove 200 lines of comments to make record little bit smaller. |
82
|
|
|
* |
83
|
|
|
* @see RelationSchemaInterface |
84
|
|
|
* @see RelationSchema |
85
|
|
|
*/ |
86
|
|
|
const HAS_ONE = 101; |
87
|
|
|
const HAS_MANY = 102; |
88
|
|
|
const BELONGS_TO = 103; |
89
|
|
|
const MANY_TO_MANY = 104; |
90
|
|
|
|
91
|
|
|
/** |
92
|
|
|
* Morphed relation types are usually created by inversion or equivalent of primary relation |
93
|
|
|
* types. |
94
|
|
|
* |
95
|
|
|
* @see RelationSchemaInterface |
96
|
|
|
* @see RelationSchema |
97
|
|
|
* @see MorphedRelation |
98
|
|
|
*/ |
99
|
|
|
const BELONGS_TO_MORPHED = 108; |
100
|
|
|
const MANY_TO_MORPHED = 109; |
101
|
|
|
|
102
|
|
|
/** |
103
|
|
|
* Constants used to declare relations in record schema, used in normalized relation schema. |
104
|
|
|
* |
105
|
|
|
* @see RelationSchemaInterface |
106
|
|
|
*/ |
107
|
|
|
const OUTER_KEY = 901; //Outer key name |
108
|
|
|
const INNER_KEY = 902; //Inner key name |
109
|
|
|
const MORPH_KEY = 903; //Morph key name |
110
|
|
|
const PIVOT_TABLE = 904; //Pivot table name |
111
|
|
|
const PIVOT_COLUMNS = 905; //Pre-defined pivot table columns |
112
|
|
|
const PIVOT_DEFAULTS = 906; //Pre-defined pivot table default values |
113
|
|
|
const THOUGHT_INNER_KEY = 907; //Pivot table options |
114
|
|
|
const THOUGHT_OUTER_KEY = 908; //Pivot table options |
115
|
|
|
const WHERE = 909; //Where conditions |
116
|
|
|
const WHERE_PIVOT = 910; //Where pivot conditions |
117
|
|
|
|
118
|
|
|
/** |
119
|
|
|
* Additional constants used to control relation schema behaviour. |
120
|
|
|
* |
121
|
|
|
* @see Record::$schema |
122
|
|
|
* @see RelationSchemaInterface |
123
|
|
|
*/ |
124
|
|
|
const INVERSE = 1001; //Relation should be inverted to parent record |
125
|
|
|
const CONSTRAINT = 1002; //Relation should create foreign keys (default) |
126
|
|
|
const CONSTRAINT_ACTION = 1003; //Default relation foreign key delete/update action (CASCADE) |
127
|
|
|
const CREATE_PIVOT = 1004; //Many-to-Many should create pivot table automatically (default) |
128
|
|
|
const NULLABLE = 1005; //Relation can be nullable (default) |
129
|
|
|
const CREATE_INDEXES = 1006; //Indication that relation is allowed to create required indexes |
130
|
|
|
const MORPHED_ALIASES = 1007; //Aliases for morphed sub-relations |
131
|
|
|
|
132
|
|
|
/** |
133
|
|
|
* Relations marked as embedded will be automatically saved/validated with parent model. In |
134
|
|
|
* addition such models data can be set using setFields method (only for ONE relations). |
135
|
|
|
* |
136
|
|
|
* @see setFields() |
137
|
|
|
* @see save() |
138
|
|
|
* @see validate() |
139
|
|
|
*/ |
140
|
|
|
const EMBEDDED_RELATION = 1008; |
141
|
|
|
|
142
|
|
|
/** |
143
|
|
|
* Constants used to declare indexes in record schema. |
144
|
|
|
* |
145
|
|
|
* @see Record::$indexes |
146
|
|
|
*/ |
147
|
|
|
const INDEX = 1000; //Default index type |
148
|
|
|
const UNIQUE = 2000; //Unique index definition |
149
|
|
|
|
150
|
|
|
/** |
151
|
|
|
* Indicates that record data were loaded from database (not recently created). |
152
|
|
|
* |
153
|
|
|
* @var bool |
154
|
|
|
*/ |
155
|
|
|
private $recordState = false; |
156
|
|
|
|
157
|
|
|
/** |
158
|
|
|
* Errors in relations and accessors. |
159
|
|
|
* |
160
|
|
|
* @var array |
161
|
|
|
*/ |
162
|
|
|
private $innerErrors = []; |
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
|
|
|
* Record field updates (changed values). |
174
|
|
|
* |
175
|
|
|
* @var array |
176
|
|
|
*/ |
177
|
|
|
private $updates = []; |
178
|
|
|
|
179
|
|
|
/** |
180
|
|
|
* Constructed and pre-cached set of record relations. Relation will be in a form of data array |
181
|
|
|
* to be created on demand. |
182
|
|
|
* |
183
|
|
|
* @see relation() |
184
|
|
|
* @see __call() |
185
|
|
|
* @see __set() |
186
|
|
|
* @see __get() |
187
|
|
|
* |
188
|
|
|
* @var RelationInterface[]|array |
189
|
|
|
*/ |
190
|
|
|
protected $relations = []; |
191
|
|
|
|
192
|
|
|
/** |
193
|
|
|
* @invisible |
194
|
|
|
* |
195
|
|
|
* @var ORMInterface|ORM |
196
|
|
|
*/ |
197
|
|
|
protected $orm = null; |
198
|
|
|
|
199
|
|
|
/** |
200
|
|
|
* Schema provided by ORM component. |
201
|
|
|
* |
202
|
|
|
* @var array |
203
|
|
|
*/ |
204
|
|
|
protected $ormSchema = []; |
205
|
|
|
|
206
|
|
|
/** |
207
|
|
|
* {@inheritdoc} |
208
|
|
|
* |
209
|
|
|
* @param null|array $schema |
210
|
|
|
* |
211
|
|
|
* @throws SugarException |
212
|
|
|
*/ |
213
|
|
|
public function __construct( |
214
|
|
|
array $data = [], |
215
|
|
|
$loaded = false, |
216
|
|
|
ORMInterface $orm = null, |
217
|
|
|
array $schema = [] |
218
|
|
|
) { |
219
|
|
|
$this->recordState = (bool)$loaded; |
220
|
|
|
|
221
|
|
|
//We can use global container as fallback if no default values were provided |
222
|
|
|
$this->orm = $this->saturate($orm, ORMInterface::class); |
223
|
|
|
$this->ormSchema = !empty($schema) ? $schema : $this->orm->schema(static::class); |
224
|
|
|
|
225
|
|
|
$this->extractRelations($data); |
226
|
|
|
|
227
|
|
|
if (!$this->isLoaded()) { |
228
|
|
|
//Non loaded records should be in solid state by default and require initial validation |
229
|
|
|
$this->solidState(true)->invalidate(); |
230
|
|
|
} |
231
|
|
|
|
232
|
|
|
parent::__construct($data + $this->ormSchema[ORMInterface::M_COLUMNS], $this->ormSchema); |
233
|
|
|
} |
234
|
|
|
|
235
|
|
|
/** |
236
|
|
|
* Change record solid state. SolidState will force record data to be saved as one big update |
237
|
|
|
* set without any generating separate update statements for changed columns. |
238
|
|
|
* |
239
|
|
|
* Attention, you have to carefully use forceUpdate flag with records without primary keys due |
240
|
|
|
* update criteria (WHERE condition) can not be easy constructed for records with primary key. |
241
|
|
|
* |
242
|
|
|
* @param bool $solidState |
243
|
|
|
* @param bool $forceUpdate Mark all fields as changed to force update later. |
244
|
|
|
* |
245
|
|
|
* @return $this |
246
|
|
|
*/ |
247
|
|
|
public function solidState($solidState, $forceUpdate = false) |
248
|
|
|
{ |
249
|
|
|
$this->solidState = $solidState; |
250
|
|
|
|
251
|
|
|
if ($forceUpdate) { |
252
|
|
|
if (!empty($this->ormSchema[ORMInterface::M_PRIMARY_KEY])) { |
253
|
|
|
$this->updates = $this->stateCriteria(); |
254
|
|
|
} else { |
255
|
|
|
$this->updates = $this->ormSchema[ORMInterface::M_COLUMNS]; |
256
|
|
|
} |
257
|
|
|
} |
258
|
|
|
|
259
|
|
|
return $this; |
260
|
|
|
} |
261
|
|
|
|
262
|
|
|
/** |
263
|
|
|
* Is record is solid state? |
264
|
|
|
* |
265
|
|
|
* @see solidState() |
266
|
|
|
* |
267
|
|
|
* @return bool |
268
|
|
|
*/ |
269
|
|
|
public function isSolid() |
270
|
|
|
{ |
271
|
|
|
return $this->solidState; |
272
|
|
|
} |
273
|
|
|
|
274
|
|
|
/** |
275
|
|
|
* {@inheritdoc} |
276
|
|
|
*/ |
277
|
|
|
public function recordRole() |
278
|
|
|
{ |
279
|
|
|
return $this->ormSchema[ORMInterface::M_ROLE_NAME]; |
280
|
|
|
} |
281
|
|
|
|
282
|
|
|
/** |
283
|
|
|
* {@inheritdoc} |
284
|
|
|
*/ |
285
|
|
|
public function isLoaded() |
286
|
|
|
{ |
287
|
|
|
return (bool)$this->recordState && !$this->isDeleted(); |
288
|
|
|
} |
289
|
|
|
|
290
|
|
|
/** |
291
|
|
|
* {@inheritdoc} |
292
|
|
|
*/ |
293
|
|
|
public function isDeleted() |
294
|
|
|
{ |
295
|
|
|
return $this->recordState === self::DELETED; |
296
|
|
|
} |
297
|
|
|
|
298
|
|
|
/** |
299
|
|
|
* {@inheritdoc} |
300
|
|
|
*/ |
301
|
|
|
public function primaryKey() |
302
|
|
|
{ |
303
|
|
|
if (!$this->hasField(ORMInterface::M_PRIMARY_KEY)) { |
304
|
|
|
throw new RecordException("Record does not have determinated primary key"); |
305
|
|
|
} |
306
|
|
|
|
307
|
|
|
return $this->getField('_id', null, false); |
308
|
|
|
} |
309
|
|
|
|
310
|
|
|
/** |
311
|
|
|
* {@inheritdoc} |
312
|
|
|
* |
313
|
|
|
* @see $fillable |
314
|
|
|
* @see $secured |
315
|
|
|
* @see isFillable() |
316
|
|
|
* |
317
|
|
|
* @param array|\Traversable $fields |
318
|
|
|
* @param bool $all Fill all fields including non fillable. |
319
|
|
|
* |
320
|
|
|
* @return $this |
321
|
|
|
* |
322
|
|
|
* @throws AccessorExceptionInterface |
323
|
|
|
*/ |
324
|
|
|
public function setFields($fields = [], $all = false) |
325
|
|
|
{ |
326
|
|
|
parent::setFields($fields, $all); |
327
|
|
|
|
328
|
|
|
foreach ($fields as $name => $nested) { |
329
|
|
|
|
330
|
|
|
//We can fill data of embedded of relations (usually HAS ONE) |
331
|
|
|
if ($this->embeddedRelation($name) && !empty($relation = $this->relation($name))) { |
332
|
|
|
//Getting related object |
333
|
|
|
$related = $relation->getRelated(); |
334
|
|
|
|
335
|
|
|
if ($related instanceof EntityInterface) { |
336
|
|
|
$related->setFields($nested); |
337
|
|
|
} |
338
|
|
|
} |
339
|
|
|
} |
340
|
|
|
|
341
|
|
|
return $this; |
342
|
|
|
} |
343
|
|
|
|
344
|
|
|
/** |
345
|
|
|
* {@inheritdoc} |
346
|
|
|
* |
347
|
|
|
* Must track field updates. |
348
|
|
|
*/ |
349
|
|
|
public function setField($name, $value, $filter = true) |
350
|
|
|
{ |
351
|
|
|
if (!$this->hasField($name)) { |
352
|
|
|
throw new FieldException("Undefined field '{$name}' in '" . static::class . "'"); |
353
|
|
|
} |
354
|
|
|
|
355
|
|
|
//Original field value |
356
|
|
|
$original = $this->getField($name, null, false); |
357
|
|
|
|
358
|
|
|
if (is_null($value) && in_array($name, $this->ormSchema[ORM::M_NULLABLE])) { |
359
|
|
|
//Bypassing filter for nullable values |
360
|
|
|
parent::setField($name, null, false); |
361
|
|
|
} else { |
362
|
|
|
parent::setField($name, $value, $filter); |
363
|
|
|
} |
364
|
|
|
|
365
|
|
View Code Duplication |
if (!array_key_exists($name, $this->updates)) { |
|
|
|
|
366
|
|
|
$this->updates[$name] = $original instanceof AccessorInterface |
367
|
|
|
? $original->serializeData() |
368
|
|
|
: $original; |
369
|
|
|
} |
370
|
|
|
} |
371
|
|
|
|
372
|
|
|
/** |
373
|
|
|
* {@inheritdoc} |
374
|
|
|
* |
375
|
|
|
* Record will skip filtration for nullable fields. |
376
|
|
|
*/ |
377
|
|
|
public function getField($name, $default = null, $filter = true) |
378
|
|
|
{ |
379
|
|
|
if (!$this->hasField($name)) { |
380
|
|
|
throw new FieldException("Undefined field '{$name}' in '" . static::class . "'"); |
381
|
|
|
} |
382
|
|
|
|
383
|
|
|
$value = parent::getField($name, $default, false); |
384
|
|
|
if ($value === null && in_array($name, $this->ormSchema[ORM::M_NULLABLE])) { |
385
|
|
|
return $value; |
386
|
|
|
} |
387
|
|
|
|
388
|
|
|
return parent::getField($name, $default, $filter); |
389
|
|
|
} |
390
|
|
|
|
391
|
|
|
/** |
392
|
|
|
* {@inheritdoc} |
393
|
|
|
* |
394
|
|
|
* @see relation() |
395
|
|
|
*/ |
396
|
|
|
public function __get($offset) |
397
|
|
|
{ |
398
|
|
|
if (isset($this->ormSchema[ORMInterface::M_RELATIONS][$offset])) { |
399
|
|
|
//Bypassing call to relation |
400
|
|
|
return $this->relation($offset)->getRelated(); |
401
|
|
|
} |
402
|
|
|
|
403
|
|
|
return $this->getField($offset, true); |
404
|
|
|
} |
405
|
|
|
|
406
|
|
|
/** |
407
|
|
|
* {@inheritdoc} |
408
|
|
|
* |
409
|
|
|
* @see relation() |
410
|
|
|
*/ |
411
|
|
|
public function __set($offset, $value) |
412
|
|
|
{ |
413
|
|
|
if (isset($this->ormSchema[ORMInterface::M_RELATIONS][$offset])) { |
414
|
|
|
//Bypassing call to relation |
415
|
|
|
$this->relation($offset)->associate($value); |
416
|
|
|
|
417
|
|
|
return; |
418
|
|
|
} |
419
|
|
|
|
420
|
|
|
$this->setField($offset, $value, true); |
421
|
|
|
} |
422
|
|
|
|
423
|
|
|
/** |
424
|
|
|
* {@inheritdoc} |
425
|
|
|
* |
426
|
|
|
* @throws FieldException |
427
|
|
|
*/ |
428
|
|
|
public function __unset($offset) |
429
|
|
|
{ |
430
|
|
|
throw new FieldException('Records fields can not be unsetted'); |
431
|
|
|
} |
432
|
|
|
|
433
|
|
|
/** |
434
|
|
|
* {@inheritdoc} |
435
|
|
|
*/ |
436
|
|
|
public function __isset($name) |
437
|
|
|
{ |
438
|
|
|
if (isset($this->ormSchema[ORMInterface::M_RELATIONS][$name])) { |
439
|
|
|
return !empty($this->relation($name)->getRelated()); |
440
|
|
|
} |
441
|
|
|
|
442
|
|
|
return parent::__isset($name); |
443
|
|
|
} |
444
|
|
|
|
445
|
|
|
/** |
446
|
|
|
* Direct access to relation by it's name. |
447
|
|
|
* |
448
|
|
|
* @see relation() |
449
|
|
|
* |
450
|
|
|
* @param string $method |
451
|
|
|
* @param array $arguments |
452
|
|
|
* |
453
|
|
|
* @return RelationInterface|mixed|AccessorInterface |
454
|
|
|
*/ |
455
|
|
|
public function __call($method, array $arguments) |
456
|
|
|
{ |
457
|
|
|
if (isset($this->ormSchema[ORMInterface::M_RELATIONS][$method])) { |
458
|
|
|
return $this->relation($method); |
459
|
|
|
} |
460
|
|
|
|
461
|
|
|
//See FIELD_FORMAT constant |
462
|
|
|
return parent::__call($method, $arguments); |
463
|
|
|
} |
464
|
|
|
|
465
|
|
|
/** |
466
|
|
|
* Get or create record relation by it's name and pre-loaded (optional) set of data. |
467
|
|
|
* |
468
|
|
|
* @param string $name |
469
|
|
|
* |
470
|
|
|
* @return RelationInterface |
471
|
|
|
* |
472
|
|
|
* @throws RelationException |
473
|
|
|
* @throws RecordException |
474
|
|
|
*/ |
475
|
|
|
public function relation($name) |
476
|
|
|
{ |
477
|
|
|
if (array_key_exists($name, $this->relations)) { |
478
|
|
|
if ($this->relations[$name] instanceof RelationInterface) { |
479
|
|
|
return $this->relations[$name]; |
480
|
|
|
} |
481
|
|
|
|
482
|
|
|
//Been pre-loaded |
483
|
|
|
return $this->initiateRelation($name, $this->relations[$name], true); |
484
|
|
|
} |
485
|
|
|
|
486
|
|
|
//Initiating empty relation object |
487
|
|
|
return $this->initiateRelation($name, null, false); |
488
|
|
|
} |
489
|
|
|
|
490
|
|
|
/** |
491
|
|
|
* {@inheritdoc} |
492
|
|
|
* |
493
|
|
|
* @param string $field Specific field name to check for updates. |
494
|
|
|
*/ |
495
|
|
|
public function hasUpdates($field = null) |
496
|
|
|
{ |
497
|
|
View Code Duplication |
if (empty($field)) { |
|
|
|
|
498
|
|
|
if (!empty($this->updates)) { |
499
|
|
|
return true; |
500
|
|
|
} |
501
|
|
|
|
502
|
|
|
foreach ($this->getFields(false) as $field => $value) { |
503
|
|
|
if ($value instanceof RecordAccessorInterface && $value->hasUpdates()) { |
504
|
|
|
return true; |
505
|
|
|
} |
506
|
|
|
} |
507
|
|
|
|
508
|
|
|
return false; |
509
|
|
|
} |
510
|
|
|
|
511
|
|
|
if (array_key_exists($field, $this->updates)) { |
512
|
|
|
return true; |
513
|
|
|
} |
514
|
|
|
|
515
|
|
|
$value = $this->getField($field); |
516
|
|
|
if ($value instanceof RecordAccessorInterface && $value->hasUpdates()) { |
517
|
|
|
return true; |
518
|
|
|
} |
519
|
|
|
|
520
|
|
|
return false; |
521
|
|
|
} |
522
|
|
|
|
523
|
|
|
/** |
524
|
|
|
* {@inheritdoc} |
525
|
|
|
*/ |
526
|
|
|
public function flushUpdates() |
527
|
|
|
{ |
528
|
|
|
$this->updates = []; |
529
|
|
|
|
530
|
|
|
foreach ($this->getFields(false) as $value) { |
531
|
|
|
if ($value instanceof RecordAccessorInterface) { |
532
|
|
|
$value->flushUpdates(); |
533
|
|
|
} |
534
|
|
|
} |
535
|
|
|
} |
536
|
|
|
|
537
|
|
|
/** |
538
|
|
|
* Create set of fields to be sent to UPDATE statement. |
539
|
|
|
* |
540
|
|
|
* @return array |
541
|
|
|
*/ |
542
|
|
|
public function compileUpdates() |
543
|
|
|
{ |
544
|
|
|
if (!$this->hasUpdates() && !$this->isSolid()) { |
545
|
|
|
return []; |
546
|
|
|
} |
547
|
|
|
|
548
|
|
|
if ($this->isSolid()) { |
549
|
|
|
return $this->serializeData(); |
550
|
|
|
} |
551
|
|
|
|
552
|
|
|
$updates = []; |
553
|
|
|
foreach ($this->getFields(false) as $field => $value) { |
554
|
|
|
if ($field == $this->ormSchema[ORMInterface::M_PRIMARY_KEY]) { |
555
|
|
|
continue; |
556
|
|
|
} |
557
|
|
|
|
558
|
|
|
if ($value instanceof RecordAccessorInterface) { |
559
|
|
|
if ($value->hasUpdates()) { |
560
|
|
|
$updates[$field] = $value->compileUpdates($field); |
561
|
|
|
continue; |
562
|
|
|
} |
563
|
|
|
|
564
|
|
|
//Will be handled as normal update if needed |
565
|
|
|
$value = $value->serializeData(); |
566
|
|
|
} |
567
|
|
|
|
568
|
|
|
if (array_key_exists($field, $this->updates)) { |
569
|
|
|
$updates[$field] = $value; |
570
|
|
|
} |
571
|
|
|
} |
572
|
|
|
|
573
|
|
|
return $updates; |
574
|
|
|
} |
575
|
|
|
|
576
|
|
|
/** |
577
|
|
|
* @return array |
578
|
|
|
*/ |
579
|
|
|
public function __debugInfo() |
580
|
|
|
{ |
581
|
|
|
$info = [ |
582
|
|
|
'table' => $this->ormSchema[ORMInterface::M_DB] . '/' . $this->ormSchema[ORMInterface::M_TABLE], |
583
|
|
|
'fields' => $this->getFields(), |
584
|
|
|
'errors' => $this->getErrors(), |
585
|
|
|
]; |
586
|
|
|
|
587
|
|
|
return $info; |
588
|
|
|
} |
589
|
|
|
|
590
|
|
|
/** |
591
|
|
|
* {@inheritdoc} |
592
|
|
|
*/ |
593
|
|
|
public function isValid() |
594
|
|
|
{ |
595
|
|
|
return parent::isValid() && empty($this->innerErrors); |
596
|
|
|
} |
597
|
|
|
|
598
|
|
|
/** |
599
|
|
|
* {@inheritdoc} |
600
|
|
|
*/ |
601
|
|
|
public function getErrors($reset = false) |
602
|
|
|
{ |
603
|
|
|
return parent::getErrors($reset) + $this->innerErrors; |
604
|
|
|
} |
605
|
|
|
|
606
|
|
|
/** |
607
|
|
|
* Change record loaded state. |
608
|
|
|
* |
609
|
|
|
* @param bool|mixed $state |
610
|
|
|
* |
611
|
|
|
* @return $this |
612
|
|
|
*/ |
613
|
|
|
protected function loadedState($state) |
614
|
|
|
{ |
615
|
|
|
$this->recordState = $state; |
616
|
|
|
|
617
|
|
|
return $this; |
618
|
|
|
} |
619
|
|
|
|
620
|
|
|
/** |
621
|
|
|
* {@inheritdoc} |
622
|
|
|
* |
623
|
|
|
* Will validate every embedded relation. |
624
|
|
|
* |
625
|
|
|
* @param bool $reset |
626
|
|
|
* |
627
|
|
|
* @throws RecordException |
628
|
|
|
*/ |
629
|
|
View Code Duplication |
protected function validate($reset = false) |
|
|
|
|
630
|
|
|
{ |
631
|
|
|
$this->innerErrors = []; |
632
|
|
|
|
633
|
|
|
foreach ($this->relations as $name => $relation) { |
634
|
|
|
if (!$relation instanceof ValidatesInterface) { |
635
|
|
|
//Never constructed |
636
|
|
|
continue; |
637
|
|
|
} |
638
|
|
|
|
639
|
|
|
if ($this->embeddedRelation($name) && !$relation->isValid()) { |
640
|
|
|
$this->innerErrors[$name] = $relation->getErrors($reset); |
641
|
|
|
} |
642
|
|
|
} |
643
|
|
|
|
644
|
|
|
parent::validate($reset); |
645
|
|
|
|
646
|
|
|
return $this->hasErrors() && empty($this->innerErrors); |
647
|
|
|
} |
648
|
|
|
|
649
|
|
|
/** |
650
|
|
|
* Get WHERE array to be used to perform record data update or deletion. Usually will include |
651
|
|
|
* record primary key. |
652
|
|
|
* |
653
|
|
|
* @return array |
654
|
|
|
*/ |
655
|
|
|
protected function stateCriteria() |
656
|
|
|
{ |
657
|
|
|
if (!empty($primaryKey = $this->ormSchema[ORMInterface::M_PRIMARY_KEY])) { |
658
|
|
|
return [$primaryKey => $this->primaryKey()]; |
659
|
|
|
} |
660
|
|
|
|
661
|
|
|
//We have to serialize record data |
662
|
|
|
return $this->updates + $this->serializeData(); |
663
|
|
|
} |
664
|
|
|
|
665
|
|
|
/** |
666
|
|
|
* {@inheritdoc} |
667
|
|
|
*/ |
668
|
|
|
protected function container() |
669
|
|
|
{ |
670
|
|
|
if (empty($this->orm) || !$this->orm instanceof Component) { |
671
|
|
|
return parent::container(); |
672
|
|
|
} |
673
|
|
|
|
674
|
|
|
return $this->orm->container(); |
675
|
|
|
} |
676
|
|
|
|
677
|
|
|
/** |
678
|
|
|
* Check if relation is embedded. |
679
|
|
|
* |
680
|
|
|
* @internal |
681
|
|
|
* |
682
|
|
|
* @param string $relation |
683
|
|
|
* |
684
|
|
|
* @return bool |
685
|
|
|
*/ |
686
|
|
|
protected function embeddedRelation($relation) |
687
|
|
|
{ |
688
|
|
|
return !empty($this->ormSchema[ORMInterface::M_RELATIONS][$relation][ORMInterface::R_DEFINITION][self::EMBEDDED_RELATION]); |
689
|
|
|
} |
690
|
|
|
|
691
|
|
|
/** |
692
|
|
|
* @param array $data |
693
|
|
|
*/ |
694
|
|
|
private function extractRelations(array &$data) |
695
|
|
|
{ |
696
|
|
|
$relations = array_intersect_key($data, $this->ormSchema[ORMInterface::M_RELATIONS]); |
697
|
|
|
|
698
|
|
|
foreach ($relations as $name => $relation) { |
699
|
|
|
$this->relations[$name] = $relation; |
700
|
|
|
unset($data[$name]); |
701
|
|
|
} |
702
|
|
|
} |
703
|
|
|
|
704
|
|
|
/** |
705
|
|
|
* @param string $name |
706
|
|
|
* @param array|null $data |
707
|
|
|
* @param bool $loaded |
708
|
|
|
* @return RelationInterface|void |
709
|
|
|
*/ |
710
|
|
|
private function initiateRelation($name, $data, $loaded) |
711
|
|
|
{ |
712
|
|
|
if (!isset($this->ormSchema[ORMInterface::M_RELATIONS][$name])) { |
713
|
|
|
throw new RecordException( |
714
|
|
|
"Undefined relation {$name} in record " . static::class . '.' |
715
|
|
|
); |
716
|
|
|
} |
717
|
|
|
|
718
|
|
|
$relation = $this->ormSchema[ORMInterface::M_RELATIONS][$name]; |
719
|
|
|
|
720
|
|
|
return $this->relations[$name] = $this->orm->relation( |
|
|
|
|
721
|
|
|
$relation[ORMInterface::R_TYPE], |
722
|
|
|
$this, |
723
|
|
|
$relation[ORMInterface::R_DEFINITION], |
724
|
|
|
$data, |
725
|
|
|
$loaded |
726
|
|
|
); |
727
|
|
|
} |
728
|
|
|
|
729
|
|
|
/** |
730
|
|
|
* {@inheritdoc} |
731
|
|
|
* |
732
|
|
|
* @see Component::staticContainer() |
733
|
|
|
* |
734
|
|
|
* @param array $fields Record fields to set, will be passed thought filters. |
735
|
|
|
* @param ORM $orm ORM component, global container will be called if not instance provided. |
736
|
|
|
* @event created() |
737
|
|
|
*/ |
738
|
|
View Code Duplication |
public static function create($fields = [], ORMInterface $orm = null) |
|
|
|
|
739
|
|
|
{ |
740
|
|
|
/** |
741
|
|
|
* @var RecordEntity $record |
742
|
|
|
*/ |
743
|
|
|
$record = new static([], false, $orm); |
744
|
|
|
|
745
|
|
|
//Forcing validation (empty set of fields is not valid set of fields) |
746
|
|
|
$record->setFields($fields)->dispatch('created', new EntityEvent($record)); |
747
|
|
|
|
748
|
|
|
return $record; |
749
|
|
|
} |
750
|
|
|
} |
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.