1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* Spiral Framework. |
4
|
|
|
* |
5
|
|
|
* @license MIT |
6
|
|
|
* @author Anton Titov (Wolfy-J) |
7
|
|
|
*/ |
8
|
|
|
namespace Spiral\ORM; |
9
|
|
|
|
10
|
|
|
use Spiral\Core\Exceptions\SugarException; |
11
|
|
|
use Spiral\Core\Traits\SaturateTrait; |
12
|
|
|
use Spiral\Database\Entities\Table; |
13
|
|
|
use Spiral\Models\AccessorInterface; |
14
|
|
|
use Spiral\Models\EntityInterface; |
15
|
|
|
use Spiral\Models\Events\EntityEvent; |
16
|
|
|
use Spiral\Models\Exceptions\AccessorExceptionInterface; |
17
|
|
|
use Spiral\Models\SchematicEntity; |
18
|
|
|
use Spiral\ORM\Exceptions\FieldException; |
19
|
|
|
use Spiral\ORM\Exceptions\RecordException; |
20
|
|
|
use Spiral\ORM\Exceptions\RelationException; |
21
|
|
|
use Spiral\Validation\ValidatesInterface; |
22
|
|
|
|
23
|
|
|
/** |
24
|
|
|
* Record is base data entity for ORM component, it used to describe related table schema, |
25
|
|
|
* filters, validations and relations to other records. You can count Record class as ActiveRecord |
26
|
|
|
* pattern. ORM component will automatically analyze existed Records and create cached version of |
27
|
|
|
* their schema. |
28
|
|
|
* |
29
|
|
|
* @TODO: Add ability to set primary key manually, for example fpr uuid like fields. |
30
|
|
|
*/ |
31
|
|
|
class RecordEntity extends SchematicEntity implements RecordInterface |
32
|
|
|
{ |
33
|
|
|
/** |
34
|
|
|
* Static container fallback. |
35
|
|
|
*/ |
36
|
|
|
use SaturateTrait; |
37
|
|
|
|
38
|
|
|
/** |
39
|
|
|
* Field format declares how entity must process magic setters and getters. Available values: |
40
|
|
|
* camelCase, tableize. |
41
|
|
|
*/ |
42
|
|
|
const FIELD_FORMAT = 'tableize'; |
43
|
|
|
|
44
|
|
|
/** |
45
|
|
|
* We are going to inherit parent validation rules, this will let spiral translator know about |
46
|
|
|
* it and merge i18n messages. |
47
|
|
|
* |
48
|
|
|
* @see TranslatorTrait |
49
|
|
|
*/ |
50
|
|
|
const I18N_INHERIT_MESSAGES = true; |
51
|
|
|
|
52
|
|
|
/** |
53
|
|
|
* ORM records are be divided by two sections: active and passive records. When record is active |
54
|
|
|
* ORM allowed to modify associated record table using declared schema and created relations. |
55
|
|
|
* |
56
|
|
|
* Passive records (ACTIVE_SCHEMA = false) however can only read table schema from database and |
57
|
|
|
* forbidden to do any schema modification either by record or by relations. |
58
|
|
|
* |
59
|
|
|
* You can use ACTIVE_SCHEMA = false in cases where you need to create an ActiveRecord for |
60
|
|
|
* existed table. |
61
|
|
|
* |
62
|
|
|
* @see RecordSchema |
63
|
|
|
* @see \Spiral\ORM\Entities\SchemaBuilder |
64
|
|
|
*/ |
65
|
|
|
const ACTIVE_SCHEMA = true; |
66
|
|
|
|
67
|
|
|
/** |
68
|
|
|
* Indication that record were deleted. |
69
|
|
|
*/ |
70
|
|
|
const DELETED = 900; |
71
|
|
|
|
72
|
|
|
/** |
73
|
|
|
* Default ORM relation types, see ORM configuration and documentation for more information, |
74
|
|
|
* i had to remove 200 lines of comments to make record little bit smaller. |
75
|
|
|
* |
76
|
|
|
* @see RelationSchemaInterface |
77
|
|
|
* @see RelationSchema |
78
|
|
|
*/ |
79
|
|
|
const HAS_ONE = 101; |
80
|
|
|
const HAS_MANY = 102; |
81
|
|
|
const BELONGS_TO = 103; |
82
|
|
|
const MANY_TO_MANY = 104; |
83
|
|
|
|
84
|
|
|
/** |
85
|
|
|
* Morphed relation types are usually created by inversion or equivalent of primary relation |
86
|
|
|
* types. |
87
|
|
|
* |
88
|
|
|
* @see RelationSchemaInterface |
89
|
|
|
* @see RelationSchema |
90
|
|
|
* @see MorphedRelation |
91
|
|
|
*/ |
92
|
|
|
const BELONGS_TO_MORPHED = 108; |
93
|
|
|
const MANY_TO_MORPHED = 109; |
94
|
|
|
|
95
|
|
|
/** |
96
|
|
|
* Constants used to declare relations in record schema, used in normalized relation schema. |
97
|
|
|
* |
98
|
|
|
* @see RelationSchemaInterface |
99
|
|
|
*/ |
100
|
|
|
const OUTER_KEY = 901; //Outer key name |
101
|
|
|
const INNER_KEY = 902; //Inner key name |
102
|
|
|
const MORPH_KEY = 903; //Morph key name |
103
|
|
|
const PIVOT_TABLE = 904; //Pivot table name |
104
|
|
|
const PIVOT_COLUMNS = 905; //Pre-defined pivot table columns |
105
|
|
|
const PIVOT_DEFAULTS = 906; //Pre-defined pivot table default values |
106
|
|
|
const THOUGHT_INNER_KEY = 907; //Pivot table options |
107
|
|
|
const THOUGHT_OUTER_KEY = 908; //Pivot table options |
108
|
|
|
const WHERE = 909; //Where conditions |
109
|
|
|
const WHERE_PIVOT = 910; //Where pivot conditions |
110
|
|
|
|
111
|
|
|
/** |
112
|
|
|
* Additional constants used to control relation schema behaviour. |
113
|
|
|
* |
114
|
|
|
* @see Record::$schema |
115
|
|
|
* @see RelationSchemaInterface |
116
|
|
|
*/ |
117
|
|
|
const INVERSE = 1001; //Relation should be inverted to parent record |
118
|
|
|
const CONSTRAINT = 1002; //Relation should create foreign keys (default) |
119
|
|
|
const CONSTRAINT_ACTION = 1003; //Default relation foreign key delete/update action (CASCADE) |
120
|
|
|
const CREATE_PIVOT = 1004; //Many-to-Many should create pivot table automatically (default) |
121
|
|
|
const NULLABLE = 1005; //Relation can be nullable (default) |
122
|
|
|
const CREATE_INDEXES = 1006; //Indication that relation is allowed to create required indexes |
123
|
|
|
const MORPHED_ALIASES = 1007; //Aliases for morphed sub-relations |
124
|
|
|
|
125
|
|
|
/** |
126
|
|
|
* Relations marked as embedded will be automatically saved/validated with parent model. In |
127
|
|
|
* addition such models data can be set using setFields method (only for ONE relations). |
128
|
|
|
* |
129
|
|
|
* @see setFields() |
130
|
|
|
* @see save() |
131
|
|
|
* @see validate() |
132
|
|
|
*/ |
133
|
|
|
const EMBEDDED_RELATION = 1008; |
134
|
|
|
|
135
|
|
|
/** |
136
|
|
|
* Constants used to declare indexes in record schema. |
137
|
|
|
* |
138
|
|
|
* @see Record::$indexes |
139
|
|
|
*/ |
140
|
|
|
const INDEX = 1000; //Default index type |
141
|
|
|
const UNIQUE = 2000; //Unique index definition |
142
|
|
|
|
143
|
|
|
/** |
144
|
|
|
* Errors in relations and acessors. |
145
|
|
|
* |
146
|
|
|
* @var array |
147
|
|
|
*/ |
148
|
|
|
private $nestedErrors = []; |
149
|
|
|
|
150
|
|
|
/** |
151
|
|
|
* Indicates that record data were loaded from database (not recently created). |
152
|
|
|
* |
153
|
|
|
* @var bool |
154
|
|
|
*/ |
155
|
|
|
private $loaded = false; |
156
|
|
|
|
157
|
|
|
/** |
158
|
|
|
* Schema provided by ORM component. |
159
|
|
|
* |
160
|
|
|
* @var array |
161
|
|
|
*/ |
162
|
|
|
private $ormSchema = []; |
163
|
|
|
|
164
|
|
|
/** |
165
|
|
|
* SolidState will force record data to be saved as one big update set without any generating |
166
|
|
|
* separate update statements for changed columns. |
167
|
|
|
* |
168
|
|
|
* @var bool |
169
|
|
|
*/ |
170
|
|
|
private $solidState = false; |
171
|
|
|
|
172
|
|
|
/** |
173
|
|
|
* Populated when record loaded using many-to-many connection. Property will include every |
174
|
|
|
* column of connection row in pivot table. |
175
|
|
|
* |
176
|
|
|
* @see setContext() |
177
|
|
|
* @see getPivot(); |
178
|
|
|
* @var array |
179
|
|
|
*/ |
180
|
|
|
private $pivotData = []; |
181
|
|
|
|
182
|
|
|
/** |
183
|
|
|
* Record field updates (changed values). |
184
|
|
|
* |
185
|
|
|
* @var array |
186
|
|
|
*/ |
187
|
|
|
private $updates = []; |
188
|
|
|
|
189
|
|
|
/** |
190
|
|
|
* Constructed and pre-cached set of record relations. Relation will be in a form of data array |
191
|
|
|
* to be created on demand. |
192
|
|
|
* |
193
|
|
|
* @see relation() |
194
|
|
|
* @see __call() |
195
|
|
|
* @see __set() |
196
|
|
|
* @see __get() |
197
|
|
|
* @var RelationInterface[]|array |
198
|
|
|
*/ |
199
|
|
|
protected $relations = []; |
200
|
|
|
|
201
|
|
|
/** |
202
|
|
|
* Table name (without database prefix) record associated to, RecordSchema will generate table |
203
|
|
|
* name automatically using class name, however i'm strongly recommend to declare table name |
204
|
|
|
* manually as it gives more readable code. |
205
|
|
|
* |
206
|
|
|
* @var string |
207
|
|
|
*/ |
208
|
|
|
protected $table = null; |
209
|
|
|
|
210
|
|
|
/** |
211
|
|
|
* Database name/id where record table located in. By default database will be used if nothing |
212
|
|
|
* else is specified. |
213
|
|
|
* |
214
|
|
|
* @var string|null |
215
|
|
|
*/ |
216
|
|
|
protected $database = null; |
217
|
|
|
|
218
|
|
|
/** |
219
|
|
|
* Set of indexes to be created for associated record table, indexes only created when record is |
220
|
|
|
* not abstract and has active schema set to true. |
221
|
|
|
* |
222
|
|
|
* Use constants INDEX and UNIQUE to describe indexes, you can also create compound indexes: |
223
|
|
|
* protected $indexes = [ |
224
|
|
|
* [self::UNIQUE, 'email'], |
225
|
|
|
* [self::INDEX, 'board_id'], |
226
|
|
|
* [self::INDEX, 'board_id', 'check_id'] |
227
|
|
|
* ]; |
228
|
|
|
* |
229
|
|
|
* @var array |
230
|
|
|
*/ |
231
|
|
|
protected $indexes = []; |
232
|
|
|
|
233
|
|
|
/** |
234
|
|
|
* Record relations and columns can be described in one place - record schema. |
235
|
|
|
* Attention: while defining table structure make sure that ACTIVE_SCHEMA constant is set to t |
236
|
|
|
* rue. |
237
|
|
|
* |
238
|
|
|
* Example: |
239
|
|
|
* protected $schema = [ |
240
|
|
|
* 'id' => 'primary', |
241
|
|
|
* 'name' => 'string', |
242
|
|
|
* 'biography' => 'text' |
243
|
|
|
* ]; |
244
|
|
|
* |
245
|
|
|
* You can pass additional options for some of your columns: |
246
|
|
|
* protected $schema = [ |
247
|
|
|
* 'pinCode' => 'string(128)', //String length |
248
|
|
|
* 'status' => 'enum(active, hidden)', //Enum values |
249
|
|
|
* 'balance' => 'decimal(10, 2)' //Decimal size and precision |
250
|
|
|
* ]; |
251
|
|
|
* |
252
|
|
|
* Every created column will be stated as NOT NULL with forced default value, if you want to |
253
|
|
|
* have nullable columns, specify special data key: protected $schema = [ |
254
|
|
|
* 'name' => 'string, nullable' |
255
|
|
|
* ]; |
256
|
|
|
* |
257
|
|
|
* You can easily combine table and relations definition in one schema: |
258
|
|
|
* protected $schema = [ |
259
|
|
|
* |
260
|
|
|
* //Table schema |
261
|
|
|
* 'id' => 'bigPrimary', |
262
|
|
|
* 'name' => 'string', |
263
|
|
|
* 'email' => 'string', |
264
|
|
|
* 'phoneNumber' => 'string(32)', |
265
|
|
|
* |
266
|
|
|
* //Relations |
267
|
|
|
* 'profile' => [ |
268
|
|
|
* self::HAS_ONE => 'Records\Profile', |
269
|
|
|
* self::INVERSE => 'user' |
270
|
|
|
* ], |
271
|
|
|
* 'roles' => [ |
272
|
|
|
* self::MANY_TO_MANY => 'Records\Role', |
273
|
|
|
* self::INVERSE => 'users' |
274
|
|
|
* ] |
275
|
|
|
* ]; |
276
|
|
|
* |
277
|
|
|
* @var array |
278
|
|
|
*/ |
279
|
|
|
protected $schema = []; |
280
|
|
|
|
281
|
|
|
/** |
282
|
|
|
* Default field values. |
283
|
|
|
* |
284
|
|
|
* @var array |
285
|
|
|
*/ |
286
|
|
|
protected $defaults = []; |
287
|
|
|
|
288
|
|
|
/** |
289
|
|
|
* @invisible |
290
|
|
|
* @var ORM |
291
|
|
|
*/ |
292
|
|
|
protected $orm = null; |
293
|
|
|
|
294
|
|
|
/** |
295
|
|
|
* Due setContext() method and entity cache of ORM any custom initiation code in constructor |
296
|
|
|
* must not depends on database data. |
297
|
|
|
* |
298
|
|
|
* @see setContext |
299
|
|
|
* @param array $data |
300
|
|
|
* @param bool|false $loaded |
301
|
|
|
* @param ORM|null $orm |
302
|
|
|
* @param array $ormSchema |
303
|
|
|
* @throws SugarException |
304
|
|
|
*/ |
305
|
|
|
public function __construct( |
306
|
|
|
array $data = [], |
307
|
|
|
$loaded = false, |
308
|
|
|
ORM $orm = null, |
309
|
|
|
array $ormSchema = [] |
310
|
|
|
) { |
311
|
|
|
$this->loaded = $loaded; |
312
|
|
|
|
313
|
|
|
//We can use global container as fallback if no default values were provided |
314
|
|
|
$this->orm = $this->saturate($orm, ORM::class); |
315
|
|
|
|
316
|
|
|
$this->ormSchema = !empty($ormSchema) ? $ormSchema : $this->orm->schema(static::class); |
317
|
|
|
|
318
|
|
|
if (isset($data[ORM::PIVOT_DATA])) { |
319
|
|
|
$this->pivotData = $data[ORM::PIVOT_DATA]; |
320
|
|
|
unset($data[ORM::PIVOT_DATA]); |
321
|
|
|
} |
322
|
|
|
|
323
|
|
|
foreach (array_intersect_key($data, |
324
|
|
|
$this->ormSchema[ORM::M_RELATIONS]) as $name => $relation) { |
325
|
|
|
$this->relations[$name] = $relation; |
326
|
|
|
unset($data[$name]); |
327
|
|
|
} |
328
|
|
|
|
329
|
|
|
parent::__construct($data + $this->ormSchema[ORM::M_COLUMNS], $this->ormSchema); |
330
|
|
|
|
331
|
|
|
if (!$this->isLoaded()) { |
332
|
|
|
//Non loaded records should be in solid state by default and require initial validation |
333
|
|
|
$this->solidState(true)->invalidate(); |
334
|
|
|
} |
335
|
|
|
} |
336
|
|
|
|
337
|
|
|
/** |
338
|
|
|
* Change record solid state. SolidState will force record data to be saved as one big update |
339
|
|
|
* set without any generating separate update statements for changed columns. |
340
|
|
|
* |
341
|
|
|
* Attention, you have to carefully use forceUpdate flag with records without primary keys due |
342
|
|
|
* update criteria (WHERE condition) can not be easy constructed for records with primary key. |
343
|
|
|
* |
344
|
|
|
* @param bool $solidState |
345
|
|
|
* @param bool $forceUpdate Mark all fields as changed to force update later. |
346
|
|
|
* @return $this |
347
|
|
|
*/ |
348
|
|
|
public function solidState($solidState, $forceUpdate = false) |
349
|
|
|
{ |
350
|
|
|
$this->solidState = $solidState; |
351
|
|
|
|
352
|
|
|
if ($forceUpdate) { |
353
|
|
|
if ($this->ormSchema[ORM::M_PRIMARY_KEY]) { |
354
|
|
|
$this->updates = $this->stateCriteria(); |
355
|
|
|
} else { |
356
|
|
|
$this->updates = $this->ormSchema[ORM::M_COLUMNS]; |
357
|
|
|
} |
358
|
|
|
} |
359
|
|
|
|
360
|
|
|
return $this; |
361
|
|
|
} |
362
|
|
|
|
363
|
|
|
/** |
364
|
|
|
* Is record is solid state? |
365
|
|
|
* |
366
|
|
|
* @see solidState() |
367
|
|
|
* @return bool |
368
|
|
|
*/ |
369
|
|
|
public function isSolid() |
370
|
|
|
{ |
371
|
|
|
return $this->solidState; |
372
|
|
|
} |
373
|
|
|
|
374
|
|
|
/** |
375
|
|
|
* {@inheritdoc} |
376
|
|
|
*/ |
377
|
|
|
public function recordRole() |
378
|
|
|
{ |
379
|
|
|
return $this->ormSchema[ORM::M_ROLE_NAME]; |
380
|
|
|
} |
381
|
|
|
|
382
|
|
|
/** |
383
|
|
|
* {@inheritdoc} |
384
|
|
|
*/ |
385
|
|
|
public function primaryKey() |
386
|
|
|
{ |
387
|
|
|
return isset($this->fields[$this->ormSchema[ORM::M_PRIMARY_KEY]]) |
388
|
|
|
? $this->fields[$this->ormSchema[ORM::M_PRIMARY_KEY]] |
389
|
|
|
: null; |
390
|
|
|
} |
391
|
|
|
|
392
|
|
|
/** |
393
|
|
|
* {@inheritdoc} |
394
|
|
|
*/ |
395
|
|
|
public function isLoaded() |
396
|
|
|
{ |
397
|
|
|
return (bool)$this->loaded && !$this->isDeleted(); |
398
|
|
|
} |
399
|
|
|
|
400
|
|
|
/** |
401
|
|
|
* {@inheritdoc} |
402
|
|
|
*/ |
403
|
|
|
public function isDeleted() |
404
|
|
|
{ |
405
|
|
|
return $this->loaded === self::DELETED; |
406
|
|
|
} |
407
|
|
|
|
408
|
|
|
/** |
409
|
|
|
* Pivot data associated with record instance, populated only in cases when record loaded using |
410
|
|
|
* Many-to-Many relation. |
411
|
|
|
* |
412
|
|
|
* @return array |
413
|
|
|
*/ |
414
|
|
|
public function getPivot() |
415
|
|
|
{ |
416
|
|
|
return $this->pivotData; |
417
|
|
|
} |
418
|
|
|
|
419
|
|
|
/** |
420
|
|
|
* {@inheritdoc} |
421
|
|
|
* |
422
|
|
|
* @see $fillable |
423
|
|
|
* @see $secured |
424
|
|
|
* @see isFillable() |
425
|
|
|
* @param array|\Traversable $fields |
426
|
|
|
* @param bool $all Fill all fields including non fillable. |
427
|
|
|
* @return $this |
428
|
|
|
* @throws AccessorExceptionInterface |
429
|
|
|
* @event setFields($fields) |
430
|
|
|
*/ |
431
|
|
|
public function setFields($fields = [], $all = false) |
432
|
|
|
{ |
433
|
|
|
parent::setFields($fields, $all); |
434
|
|
|
|
435
|
|
|
foreach ($fields as $name => $nested) { |
436
|
|
|
//We can fill data of embedded of relations (usually HAS ONE) |
437
|
|
|
if ($this->isEmbedded($name)) { |
438
|
|
|
//Getting relation instance |
439
|
|
|
$relation = $this->relation($name); |
440
|
|
|
|
441
|
|
|
//Getting related object |
442
|
|
|
$related = $relation->getRelated(); |
443
|
|
|
if ($related instanceof EntityInterface) { |
444
|
|
|
$related->setFields($nested); |
445
|
|
|
} |
446
|
|
|
} |
447
|
|
|
} |
448
|
|
|
|
449
|
|
|
return $this; |
450
|
|
|
} |
451
|
|
|
|
452
|
|
|
/** |
453
|
|
|
* {@inheritdoc} |
454
|
|
|
* |
455
|
|
|
* Must track field updates. In addition Records will not allow to set unknown field. |
456
|
|
|
* |
457
|
|
|
* @throws RecordException |
458
|
|
|
*/ |
459
|
|
|
public function setField($name, $value, $filter = true) |
460
|
|
|
{ |
461
|
|
View Code Duplication |
if (!array_key_exists($name, $this->fields)) { |
462
|
|
|
throw new FieldException("Undefined field '{$name}' in '" . static::class . "'."); |
463
|
|
|
} |
464
|
|
|
|
465
|
|
|
$original = isset($this->fields[$name]) ? $this->fields[$name] : null; |
466
|
|
|
if ($value === null && in_array($name, $this->ormSchema[ORM::M_NULLABLE])) { |
467
|
|
|
//We must bypass setters and accessors when null value assigned to nullable column |
468
|
|
|
$this->fields[$name] = null; |
469
|
|
|
} else { |
470
|
|
|
parent::setField($name, $value, $filter); |
471
|
|
|
} |
472
|
|
|
|
473
|
|
View Code Duplication |
if (!array_key_exists($name, $this->updates)) { |
|
|
|
|
474
|
|
|
$this->updates[$name] = $original instanceof AccessorInterface |
475
|
|
|
? $original->serializeData() |
476
|
|
|
: $original; |
477
|
|
|
} |
478
|
|
|
} |
479
|
|
|
|
480
|
|
|
/** |
481
|
|
|
* {@inheritdoc} |
482
|
|
|
* |
483
|
|
|
* Record will skip filtration for nullable fields. |
484
|
|
|
*/ |
485
|
|
|
public function getField($name, $default = null, $filter = true) |
486
|
|
|
{ |
487
|
|
View Code Duplication |
if (!array_key_exists($name, $this->fields)) { |
488
|
|
|
throw new FieldException("Undefined field '{$name}' in '" . static::class . "'."); |
489
|
|
|
} |
490
|
|
|
|
491
|
|
|
$value = $this->fields[$name]; |
492
|
|
|
if ($value === null && in_array($name, $this->ormSchema[ORM::M_NULLABLE])) { |
493
|
|
|
//if (!isset($this->ormSchema[ORM::M_MUTATORS]['accessor'][$name])) { |
|
|
|
|
494
|
|
|
//We can skip setters for null values, but not accessors |
495
|
|
|
return $value; |
496
|
|
|
//} |
497
|
|
|
} |
498
|
|
|
|
499
|
|
|
return parent::getField($name, $default, $filter); |
500
|
|
|
} |
501
|
|
|
|
502
|
|
|
/** |
503
|
|
|
* Get or create record relation by it's name and pre-loaded (optional) set of data. |
504
|
|
|
* |
505
|
|
|
* @todo hasRelation? |
506
|
|
|
* @param string $name |
507
|
|
|
* @param mixed $data |
508
|
|
|
* @param bool $loaded |
509
|
|
|
* @return RelationInterface |
510
|
|
|
* @throws RelationException |
511
|
|
|
* @throws RecordException |
512
|
|
|
*/ |
513
|
|
|
public function relation($name, $data = null, $loaded = false) |
514
|
|
|
{ |
515
|
|
|
if (array_key_exists($name, $this->relations)) { |
516
|
|
|
if (!is_object($this->relations[$name])) { |
517
|
|
|
$data = $this->relations[$name]; |
518
|
|
|
unset($this->relations[$name]); |
519
|
|
|
|
520
|
|
|
//Loaded relation |
521
|
|
|
return $this->relation($name, $data, true); |
522
|
|
|
} |
523
|
|
|
|
524
|
|
|
//Already created |
525
|
|
|
return $this->relations[$name]; |
526
|
|
|
} |
527
|
|
|
|
528
|
|
|
|
529
|
|
|
//Constructing relation |
530
|
|
|
if (!isset($this->ormSchema[ORM::M_RELATIONS][$name])) { |
531
|
|
|
throw new RecordException( |
532
|
|
|
"Undefined relation {$name} in record " . static::class . "." |
533
|
|
|
); |
534
|
|
|
} |
535
|
|
|
|
536
|
|
|
$relation = $this->ormSchema[ORM::M_RELATIONS][$name]; |
537
|
|
|
|
538
|
|
|
return $this->relations[$name] = $this->orm->relation( |
539
|
|
|
$relation[ORM::R_TYPE], $this, $relation[ORM::R_DEFINITION], $data, $loaded |
540
|
|
|
); |
541
|
|
|
} |
542
|
|
|
|
543
|
|
|
/** |
544
|
|
|
* {@inheritdoc} |
545
|
|
|
* |
546
|
|
|
* @param string $field Specific field name to check for updates. |
547
|
|
|
*/ |
548
|
|
|
public function hasUpdates($field = null) |
549
|
|
|
{ |
550
|
|
View Code Duplication |
if (empty($field)) { |
|
|
|
|
551
|
|
|
if (!empty($this->updates)) { |
552
|
|
|
return true; |
553
|
|
|
} |
554
|
|
|
|
555
|
|
|
foreach ($this->fields as $field => $value) { |
556
|
|
|
if ($value instanceof RecordAccessorInterface && $value->hasUpdates()) { |
557
|
|
|
return true; |
558
|
|
|
} |
559
|
|
|
} |
560
|
|
|
|
561
|
|
|
return false; |
562
|
|
|
} |
563
|
|
|
|
564
|
|
|
if (array_key_exists($field, $this->updates)) { |
565
|
|
|
return true; |
566
|
|
|
} |
567
|
|
|
|
568
|
|
|
$value = $this->getField($field); |
569
|
|
|
if ($value instanceof RecordAccessorInterface && $value->hasUpdates()) { |
570
|
|
|
return true; |
571
|
|
|
} |
572
|
|
|
|
573
|
|
|
return false; |
574
|
|
|
} |
575
|
|
|
|
576
|
|
|
/** |
577
|
|
|
* {@inheritdoc} |
578
|
|
|
*/ |
579
|
|
|
public function flushUpdates() |
580
|
|
|
{ |
581
|
|
|
$this->updates = []; |
582
|
|
|
|
583
|
|
|
foreach ($this->fields as $value) { |
584
|
|
|
if ($value instanceof RecordAccessorInterface) { |
585
|
|
|
$value->flushUpdates(); |
586
|
|
|
} |
587
|
|
|
} |
588
|
|
|
} |
589
|
|
|
|
590
|
|
|
/** |
591
|
|
|
* {@inheritdoc} |
592
|
|
|
*/ |
593
|
|
|
public function isValid() |
594
|
|
|
{ |
595
|
|
|
$this->validate(); |
596
|
|
|
|
597
|
|
|
return empty($this->errors) && empty($this->nestedErrors); |
598
|
|
|
} |
599
|
|
|
|
600
|
|
|
/** |
601
|
|
|
* {@inheritdoc} |
602
|
|
|
*/ |
603
|
|
|
public function getErrors($reset = false) |
604
|
|
|
{ |
605
|
|
|
return parent::getErrors($reset) + $this->nestedErrors; |
606
|
|
|
} |
607
|
|
|
|
608
|
|
|
/** |
609
|
|
|
* {@inheritdoc} |
610
|
|
|
*/ |
611
|
|
|
public function __isset($name) |
612
|
|
|
{ |
613
|
|
|
if (isset($this->ormSchema[ORM::M_RELATIONS][$name])) { |
614
|
|
|
return !empty($this->relation($name)->getRelated()); |
615
|
|
|
} |
616
|
|
|
|
617
|
|
|
return parent::__isset($name); |
618
|
|
|
} |
619
|
|
|
|
620
|
|
|
/** |
621
|
|
|
* {@inheritdoc} |
622
|
|
|
* |
623
|
|
|
* @throws RecordException |
624
|
|
|
*/ |
625
|
|
|
public function __unset($offset) |
626
|
|
|
{ |
627
|
|
|
throw new FieldException("Records fields can not be unsetted."); |
628
|
|
|
} |
629
|
|
|
|
630
|
|
|
/** |
631
|
|
|
* {@inheritdoc} |
632
|
|
|
* |
633
|
|
|
* @see relation() |
634
|
|
|
*/ |
635
|
|
|
public function __get($offset) |
636
|
|
|
{ |
637
|
|
|
if (isset($this->ormSchema[ORM::M_RELATIONS][$offset])) { |
638
|
|
|
//Bypassing call to relation |
639
|
|
|
return $this->relation($offset)->getRelated(); |
640
|
|
|
} |
641
|
|
|
|
642
|
|
|
return $this->getField($offset, true); |
643
|
|
|
} |
644
|
|
|
|
645
|
|
|
/** |
646
|
|
|
* {@inheritdoc} |
647
|
|
|
* |
648
|
|
|
* @see relation() |
649
|
|
|
*/ |
650
|
|
|
public function __set($offset, $value) |
651
|
|
|
{ |
652
|
|
|
if (isset($this->ormSchema[ORM::M_RELATIONS][$offset])) { |
653
|
|
|
//Bypassing call to relation |
654
|
|
|
$this->relation($offset)->associate($value); |
655
|
|
|
|
656
|
|
|
return; |
657
|
|
|
} |
658
|
|
|
|
659
|
|
|
$this->setField($offset, $value, true); |
660
|
|
|
} |
661
|
|
|
|
662
|
|
|
/** |
663
|
|
|
* Direct access to relation by it's name. |
664
|
|
|
* |
665
|
|
|
* @see relation() |
666
|
|
|
* @param string $method |
667
|
|
|
* @param array $arguments |
668
|
|
|
* @return RelationInterface|mixed|AccessorInterface |
669
|
|
|
*/ |
670
|
|
|
public function __call($method, array $arguments) |
671
|
|
|
{ |
672
|
|
|
if (isset($this->ormSchema[ORM::M_RELATIONS][$method])) { |
673
|
|
|
return $this->relation($method); |
674
|
|
|
} |
675
|
|
|
|
676
|
|
|
//See FIELD_FORMAT constant |
677
|
|
|
return parent::__call($method, $arguments); |
678
|
|
|
} |
679
|
|
|
|
680
|
|
|
/** |
681
|
|
|
* @return array |
682
|
|
|
*/ |
683
|
|
|
public function __debugInfo() |
684
|
|
|
{ |
685
|
|
|
$info = [ |
686
|
|
|
'table' => $this->ormSchema[ORM::M_DB] . '/' . $this->ormSchema[ORM::M_TABLE], |
687
|
|
|
'pivotData' => $this->pivotData, |
688
|
|
|
'fields' => $this->getFields(), |
689
|
|
|
'errors' => $this->getErrors() |
690
|
|
|
]; |
691
|
|
|
|
692
|
|
|
if (empty($this->pivotData)) { |
693
|
|
|
unset($info['pivotData']); |
694
|
|
|
} |
695
|
|
|
|
696
|
|
|
return $info; |
697
|
|
|
} |
698
|
|
|
|
699
|
|
|
/** |
700
|
|
|
* Get associated Database\Table instance. |
701
|
|
|
* |
702
|
|
|
* @see save() |
703
|
|
|
* @see delete() |
704
|
|
|
* @return Table |
705
|
|
|
*/ |
706
|
|
|
protected function sourceTable() |
707
|
|
|
{ |
708
|
|
|
return $this->orm->database($this->ormSchema[ORM::M_DB])->table( |
709
|
|
|
$this->ormSchema[ORM::M_TABLE] |
710
|
|
|
); |
711
|
|
|
} |
712
|
|
|
|
713
|
|
|
/** |
714
|
|
|
* Get WHERE array to be used to perform record data update or deletion. Usually will include |
715
|
|
|
* record primary key. |
716
|
|
|
* |
717
|
|
|
* @return array |
718
|
|
|
*/ |
719
|
|
|
protected function stateCriteria() |
720
|
|
|
{ |
721
|
|
|
if (!empty($primaryKey = $this->ormSchema()[ORM::M_PRIMARY_KEY])) { |
722
|
|
|
return [$primaryKey => $this->primaryKey()]; |
723
|
|
|
} |
724
|
|
|
|
725
|
|
|
//We have to serialize record data |
726
|
|
|
return $this->updates + $this->serializeData(); |
727
|
|
|
} |
728
|
|
|
|
729
|
|
|
/** |
730
|
|
|
* {@inheritdoc} |
731
|
|
|
*/ |
732
|
|
|
protected function container() |
733
|
|
|
{ |
734
|
|
|
if (empty($this->orm)) { |
735
|
|
|
return parent::container(); |
736
|
|
|
} |
737
|
|
|
|
738
|
|
|
return $this->orm->container(); |
739
|
|
|
} |
740
|
|
|
|
741
|
|
|
/** |
742
|
|
|
* Create set of fields to be sent to UPDATE statement. |
743
|
|
|
* |
744
|
|
|
* @internal |
745
|
|
|
* @todo make public, move to Record? |
746
|
|
|
* @todo create compileInsert twin? |
747
|
|
|
* @see save() |
748
|
|
|
* @return array |
749
|
|
|
*/ |
750
|
|
|
protected function compileUpdates() |
751
|
|
|
{ |
752
|
|
|
if (!$this->hasUpdates() && !$this->isSolid()) { |
753
|
|
|
return []; |
754
|
|
|
} |
755
|
|
|
|
756
|
|
|
if ($this->isSolid()) { |
757
|
|
|
return $this->solidUpdate(); |
758
|
|
|
} |
759
|
|
|
|
760
|
|
|
$updates = []; |
761
|
|
|
foreach ($this->fields as $field => $value) { |
762
|
|
View Code Duplication |
if ($value instanceof RecordAccessorInterface) { |
|
|
|
|
763
|
|
|
if ($value->hasUpdates()) { |
764
|
|
|
$updates[$field] = $value->compileUpdates($field); |
765
|
|
|
continue; |
766
|
|
|
} |
767
|
|
|
|
768
|
|
|
//Will be handled as normal update if needed |
769
|
|
|
$value = $value->serializeData(); |
770
|
|
|
} |
771
|
|
|
|
772
|
|
|
if (array_key_exists($field, $this->updates)) { |
773
|
|
|
$updates[$field] = $value; |
774
|
|
|
} |
775
|
|
|
} |
776
|
|
|
|
777
|
|
|
//Primary key should not present in update set |
778
|
|
|
unset($updates[$this->ormSchema[ORM::M_PRIMARY_KEY]]); |
779
|
|
|
|
780
|
|
|
return $updates; |
781
|
|
|
} |
782
|
|
|
|
783
|
|
|
/** |
784
|
|
|
* {@inheritdoc} |
785
|
|
|
* |
786
|
|
|
* Will validate every loaded and embedded relation. |
787
|
|
|
*/ |
788
|
|
View Code Duplication |
protected function validate($reset = false) |
|
|
|
|
789
|
|
|
{ |
790
|
|
|
$this->nestedErrors = []; |
791
|
|
|
|
792
|
|
|
//Validating all compositions/accessors |
793
|
|
|
foreach ($this->fields as $field => $value) { |
794
|
|
|
//Ensuring value state |
795
|
|
|
$value = $this->getField($field); |
796
|
|
|
if (!$value instanceof ValidatesInterface) { |
797
|
|
|
continue; |
798
|
|
|
} |
799
|
|
|
|
800
|
|
|
if (!$value->isValid()) { |
801
|
|
|
$this->nestedErrors[$field] = $value->getErrors($reset); |
802
|
|
|
} |
803
|
|
|
} |
804
|
|
|
|
805
|
|
|
//We have to validate some relations before saving them |
806
|
|
|
$this->validateRelations($reset); |
807
|
|
|
|
808
|
|
|
parent::validate($reset); |
809
|
|
|
|
810
|
|
|
return empty($this->errors + $this->nestedErrors); |
811
|
|
|
} |
812
|
|
|
|
813
|
|
|
/** |
814
|
|
|
* {@inheritdoc} |
815
|
|
|
* |
816
|
|
|
* @see Component::staticContainer() |
817
|
|
|
* @param array $fields Record fields to set, will be passed thought filters. |
818
|
|
|
* @param ORM $orm ORM component, global container will be called if not instance provided. |
819
|
|
|
* @event created() |
820
|
|
|
*/ |
821
|
|
View Code Duplication |
public static function create($fields = [], ORM $orm = null) |
|
|
|
|
822
|
|
|
{ |
823
|
|
|
/** |
824
|
|
|
* @var RecordEntity $record |
825
|
|
|
*/ |
826
|
|
|
$record = new static([], false, $orm); |
827
|
|
|
|
828
|
|
|
//Forcing validation (empty set of fields is not valid set of fields) |
829
|
|
|
$record->setFields($fields)->dispatch('created', new EntityEvent($record)); |
830
|
|
|
|
831
|
|
|
return $record; |
832
|
|
|
} |
833
|
|
|
|
834
|
|
|
/** |
835
|
|
|
* Change record loaded state. |
836
|
|
|
* |
837
|
|
|
* @param bool|mixed $loader |
838
|
|
|
* @return $this |
839
|
|
|
*/ |
840
|
|
|
protected function loadedState($loader) |
841
|
|
|
{ |
842
|
|
|
$this->loaded = $loader; |
843
|
|
|
|
844
|
|
|
return $this; |
845
|
|
|
} |
846
|
|
|
|
847
|
|
|
/** |
848
|
|
|
* Related and cached ORM schema. |
849
|
|
|
* |
850
|
|
|
* @internal |
851
|
|
|
* @return array |
852
|
|
|
*/ |
853
|
|
|
protected function ormSchema() |
854
|
|
|
{ |
855
|
|
|
return $this->ormSchema; |
856
|
|
|
} |
857
|
|
|
|
858
|
|
|
/** |
859
|
|
|
* Check if relation is embedded. |
860
|
|
|
* |
861
|
|
|
* @internal |
862
|
|
|
* @param string $relation |
863
|
|
|
* @return bool |
864
|
|
|
*/ |
865
|
|
|
protected function isEmbedded($relation) |
866
|
|
|
{ |
867
|
|
|
return !empty( |
868
|
|
|
$this->ormSchema[ORM::M_RELATIONS][$relation][ORM::R_DEFINITION][self::EMBEDDED_RELATION] |
869
|
|
|
); |
870
|
|
|
} |
871
|
|
|
|
872
|
|
|
/** |
873
|
|
|
* Full structure update. |
874
|
|
|
* |
875
|
|
|
* @return array |
876
|
|
|
*/ |
877
|
|
|
private function solidUpdate() |
878
|
|
|
{ |
879
|
|
|
$updates = []; |
880
|
|
|
foreach ($this->fields as $field => $value) { |
881
|
|
View Code Duplication |
if ($value instanceof RecordAccessorInterface && $value->hasUpdates()) { |
|
|
|
|
882
|
|
|
if ($value->hasUpdates()) { |
883
|
|
|
$updates[$field] = $value->compileUpdates($field); |
884
|
|
|
} else { |
885
|
|
|
$updates[$field] = $value->serializeData(); |
886
|
|
|
} |
887
|
|
|
continue; |
888
|
|
|
} |
889
|
|
|
|
890
|
|
|
$updates[$field] = $value; |
891
|
|
|
} |
892
|
|
|
|
893
|
|
|
return $updates; |
894
|
|
|
} |
895
|
|
|
|
896
|
|
|
/** |
897
|
|
|
* Validate embedded relations. |
898
|
|
|
* |
899
|
|
|
* @param bool $reset |
900
|
|
|
*/ |
901
|
|
|
private function validateRelations($reset) |
902
|
|
|
{ |
903
|
|
|
foreach ($this->relations as $name => $relation) { |
904
|
|
|
if (!$relation instanceof ValidatesInterface) { |
905
|
|
|
//Never constructed |
906
|
|
|
continue; |
907
|
|
|
} |
908
|
|
|
|
909
|
|
|
if ($this->isEmbedded($name) && !$relation->isValid()) { |
910
|
|
|
$this->nestedErrors[$name] = $relation->getErrors($reset); |
911
|
|
|
} |
912
|
|
|
} |
913
|
|
|
} |
914
|
|
|
} |
915
|
|
|
|
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.