1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace As3\Modlr\Models; |
4
|
|
|
|
5
|
|
|
use As3\Modlr\Models\Relationships; |
6
|
|
|
use As3\Modlr\Persister\Record; |
7
|
|
|
use As3\Modlr\Store\Store; |
8
|
|
|
use As3\Modlr\Metadata\EntityMetadata; |
9
|
|
|
|
10
|
|
|
/** |
11
|
|
|
* Represents a data record from a persistence (database) layer. |
12
|
|
|
* |
13
|
|
|
* @author Jacob Bare <[email protected]> |
14
|
|
|
*/ |
15
|
|
|
class Model |
16
|
|
|
{ |
17
|
|
|
/** |
18
|
|
|
* The id value of this model. |
19
|
|
|
* Always converted to a string when in the model context. |
20
|
|
|
* |
21
|
|
|
* @var string |
22
|
|
|
*/ |
23
|
|
|
protected $identifier; |
24
|
|
|
|
25
|
|
|
/** |
26
|
|
|
* The Model's attributes |
27
|
|
|
* |
28
|
|
|
* @var Attributes |
29
|
|
|
*/ |
30
|
|
|
protected $attributes; |
31
|
|
|
|
32
|
|
|
/** |
33
|
|
|
* The Model's has-one relationships |
34
|
|
|
* |
35
|
|
|
* @var Relationships\HasOne |
36
|
|
|
*/ |
37
|
|
|
protected $hasOneRelationships; |
38
|
|
|
|
39
|
|
|
/** |
40
|
|
|
* The Model's has-many relationships |
41
|
|
|
* |
42
|
|
|
* @var Relationships\HasMany |
43
|
|
|
*/ |
44
|
|
|
protected $hasManyRelationships; |
45
|
|
|
|
46
|
|
|
/** |
47
|
|
|
* Enables/disables collection auto-initialization on iteration. |
48
|
|
|
* Will not load/fill the collection from the database if false. |
49
|
|
|
* Is useful for large hasMany iterations where only id and type are required (ala serialization). |
50
|
|
|
* |
51
|
|
|
* @var bool |
52
|
|
|
*/ |
53
|
|
|
protected $collectionAutoInit = true; |
54
|
|
|
|
55
|
|
|
/** |
56
|
|
|
* The model state. |
57
|
|
|
* |
58
|
|
|
* @var State |
59
|
|
|
*/ |
60
|
|
|
protected $state; |
61
|
|
|
|
62
|
|
|
/** |
63
|
|
|
* The EntityMetadata that defines this Model. |
64
|
|
|
* |
65
|
|
|
* @var EntityMetadata |
66
|
|
|
*/ |
67
|
|
|
protected $metadata; |
68
|
|
|
|
69
|
|
|
/** |
70
|
|
|
* The Model Store for handling lifecycle operations. |
71
|
|
|
* |
72
|
|
|
* @var Store |
73
|
|
|
*/ |
74
|
|
|
protected $store; |
75
|
|
|
|
76
|
|
|
/** |
77
|
|
|
* Constructor. |
78
|
|
|
* |
79
|
|
|
* @param EntityMetadata $metadata The internal entity metadata that supports this Model. |
80
|
|
|
* @param string $identifier The database identifier. |
81
|
|
|
* @param Store $store The model store service for handling persistence operations. |
82
|
|
|
* @param Record|null $record The model's attributes and relationships from the db layer to init the model with. New models will constructed with a null record. |
83
|
|
|
*/ |
84
|
|
|
public function __construct(EntityMetadata $metadata, $identifier, Store $store, Record $record = null) |
85
|
|
|
{ |
86
|
|
|
$this->metadata = $metadata; |
87
|
|
|
$this->identifier = $identifier; |
88
|
|
|
$this->store = $store; |
89
|
|
|
$this->state = new State(); |
90
|
|
|
$this->initialize($record); |
91
|
|
|
} |
92
|
|
|
|
93
|
|
|
/** |
94
|
|
|
* Gets the unique identifier of this model. |
95
|
|
|
* |
96
|
|
|
* @api |
97
|
|
|
* @return string |
98
|
|
|
*/ |
99
|
|
|
public function getId() |
100
|
|
|
{ |
101
|
|
|
return $this->identifier; |
102
|
|
|
} |
103
|
|
|
|
104
|
|
|
/** |
105
|
|
|
* Gets the model type. |
106
|
|
|
* |
107
|
|
|
* @api |
108
|
|
|
* @return string |
109
|
|
|
*/ |
110
|
|
|
public function getType() |
111
|
|
|
{ |
112
|
|
|
return $this->metadata->type; |
113
|
|
|
} |
114
|
|
|
|
115
|
|
|
/** |
116
|
|
|
* Gets the model store. |
117
|
|
|
* |
118
|
|
|
* @api |
119
|
|
|
* @return Store |
120
|
|
|
*/ |
121
|
|
|
public function getStore() |
122
|
|
|
{ |
123
|
|
|
return $this->store; |
124
|
|
|
} |
125
|
|
|
|
126
|
|
|
/** |
127
|
|
|
* Gets the composite key of the model by combining the model type with the unique id. |
128
|
|
|
* |
129
|
|
|
* @api |
130
|
|
|
* @return string |
131
|
|
|
*/ |
132
|
|
|
public function getCompositeKey() |
133
|
|
|
{ |
134
|
|
|
return sprintf('%s.%s', $this->getType(), $this->getId()); |
135
|
|
|
} |
136
|
|
|
|
137
|
|
|
/** |
138
|
|
|
* Enables or disables has-many collection auto-initialization from the database. |
139
|
|
|
* |
140
|
|
|
* @param bool $bit Whether to enable/disable. |
141
|
|
|
* @return self |
142
|
|
|
*/ |
143
|
|
|
public function enableCollectionAutoInit($bit = true) |
144
|
|
|
{ |
145
|
|
|
$this->collectionAutoInit = (Boolean) $bit; |
146
|
|
|
return $this; |
147
|
|
|
} |
148
|
|
|
|
149
|
|
|
/** |
150
|
|
|
* Gets a model property. |
151
|
|
|
* Will either be an attribute value, a has-one model, or an array representation of a has-many collection. |
152
|
|
|
* Returns null if the property does not exist on the model or is not set. |
153
|
|
|
* Is a proxy for @see getAttribute($key) and getRelationship($key) |
154
|
|
|
* |
155
|
|
|
* @api |
156
|
|
|
* @param string $key The property field key. |
157
|
|
|
* @return Model|Model[]|null|mixed |
158
|
|
|
*/ |
159
|
|
|
public function get($key) |
160
|
|
|
{ |
161
|
|
|
if (true === $this->isAttribute($key)) { |
162
|
|
|
return $this->getAttribute($key); |
163
|
|
|
} |
164
|
|
|
return $this->getRelationship($key); |
165
|
|
|
} |
166
|
|
|
|
167
|
|
|
/** |
168
|
|
|
* Determines if a property key is an attribute. |
169
|
|
|
* |
170
|
|
|
* @api |
171
|
|
|
* @param string $key The property key. |
172
|
|
|
* @return bool |
173
|
|
|
*/ |
174
|
|
|
public function isAttribute($key) |
175
|
|
|
{ |
176
|
|
|
return $this->getMetadata()->hasAttribute($key); |
177
|
|
|
} |
178
|
|
|
|
179
|
|
|
/** |
180
|
|
|
* Determines if an attribute key is calculated. |
181
|
|
|
* |
182
|
|
|
* @param string $key The attribute key. |
183
|
|
|
* @return bool |
184
|
|
|
*/ |
185
|
|
|
protected function isCalculatedAttribute($key) |
186
|
|
|
{ |
187
|
|
|
if (false === $this->isAttribute($key)) { |
188
|
|
|
return false; |
189
|
|
|
} |
190
|
|
|
return $this->getMetadata()->getAttribute($key)->isCalculated(); |
191
|
|
|
} |
192
|
|
|
|
193
|
|
|
/** |
194
|
|
|
* Gets an attribute value. |
195
|
|
|
* |
196
|
|
|
* @param string $key The attribute key (field) name. |
197
|
|
|
* @return mixed |
198
|
|
|
*/ |
199
|
|
|
protected function getAttribute($key) |
200
|
|
|
{ |
201
|
|
|
if (true === $this->isCalculatedAttribute($key)) { |
202
|
|
|
return $this->getCalculatedAttribute($key); |
203
|
|
|
} |
204
|
|
|
$this->touch(); |
205
|
|
|
return $this->attributes->get($key); |
206
|
|
|
} |
207
|
|
|
|
208
|
|
|
/** |
209
|
|
|
* Gets a calculated attribute value. |
210
|
|
|
* |
211
|
|
|
* @param string $key The attribute key (field) name. |
212
|
|
|
* @return mixed |
213
|
|
|
*/ |
214
|
|
|
protected function getCalculatedAttribute($key) |
215
|
|
|
{ |
216
|
|
|
$attrMeta = $this->getMetadata()->getAttribute($key); |
217
|
|
|
$class = $attrMeta->calculated['class']; |
218
|
|
|
$method = $attrMeta->calculated['method']; |
219
|
|
|
|
220
|
|
|
$value = $class::$method($this); |
221
|
|
|
return $this->convertAttributeValue($key, $value); |
222
|
|
|
} |
223
|
|
|
|
224
|
|
|
/** |
225
|
|
|
* Determines if a property key is a relationship (either has-one or has-many). |
226
|
|
|
* |
227
|
|
|
* @api |
228
|
|
|
* @param string $key The property key. |
229
|
|
|
* @return bool |
230
|
|
|
*/ |
231
|
|
|
public function isRelationship($key) |
232
|
|
|
{ |
233
|
|
|
return $this->getMetadata()->hasRelationship($key); |
234
|
|
|
} |
235
|
|
|
|
236
|
|
|
/** |
237
|
|
|
* Determines if a property key is a an inverse relationship. |
238
|
|
|
* |
239
|
|
|
* @api |
240
|
|
|
* @param string $key The property key. |
241
|
|
|
* @return bool |
242
|
|
|
*/ |
243
|
|
|
public function isInverse($key) |
244
|
|
|
{ |
245
|
|
|
if (false === $this->isRelationship($key)) { |
246
|
|
|
return false; |
247
|
|
|
} |
248
|
|
|
return $this->getMetadata()->getRelationship($key)->isInverse; |
249
|
|
|
} |
250
|
|
|
|
251
|
|
|
/** |
252
|
|
|
* Determines if a property key is a has-one relationship. |
253
|
|
|
* |
254
|
|
|
* @api |
255
|
|
|
* @param string $key The property key. |
256
|
|
|
* @return bool |
257
|
|
|
*/ |
258
|
|
|
public function isHasOne($key) |
259
|
|
|
{ |
260
|
|
|
if (false === $this->isRelationship($key)) { |
261
|
|
|
return false; |
262
|
|
|
} |
263
|
|
|
return $this->getMetadata()->getRelationship($key)->isOne(); |
264
|
|
|
} |
265
|
|
|
|
266
|
|
|
/** |
267
|
|
|
* Determines if a property key is a has-many relationship. |
268
|
|
|
* |
269
|
|
|
* @api |
270
|
|
|
* @param string $key The property key. |
271
|
|
|
* @return bool |
272
|
|
|
*/ |
273
|
|
|
public function isHasMany($key) |
274
|
|
|
{ |
275
|
|
|
if (false === $this->isRelationship($key)) { |
276
|
|
|
return false; |
277
|
|
|
} |
278
|
|
|
return $this->getMetadata()->getRelationship($key)->isMany(); |
279
|
|
|
} |
280
|
|
|
|
281
|
|
|
/** |
282
|
|
|
* Gets a relationship value. |
283
|
|
|
* |
284
|
|
|
* @param string $key The relationship key (field) name. |
285
|
|
|
* @return Model|array|null |
286
|
|
|
* @throws \RuntimeException If hasMany relationships are accessed directly. |
287
|
|
|
*/ |
288
|
|
|
protected function getRelationship($key) |
289
|
|
|
{ |
290
|
|
|
if (true === $this->isHasOne($key)) { |
291
|
|
|
$this->touch(); |
292
|
|
|
return $this->hasOneRelationships->get($key); |
293
|
|
|
} |
294
|
|
|
if (true === $this->isHasMany($key)) { |
295
|
|
|
$this->touch(); |
296
|
|
|
$collection = $this->hasManyRelationships->get($key); |
297
|
|
|
if ($collection->isLoaded($collection)) { |
298
|
|
|
return iterator_to_array($collection); |
299
|
|
|
} |
300
|
|
|
return (true === $this->collectionAutoInit) ? iterator_to_array($collection) : $collection->allWithoutLoad(); |
301
|
|
|
} |
302
|
|
|
return null; |
303
|
|
|
} |
304
|
|
|
|
305
|
|
|
/** |
306
|
|
|
* Pushes a Model into a has-many relationship collection. |
307
|
|
|
* This method must be used for has-many relationships. Direct set is not supported. |
308
|
|
|
* To completely replace a has-many, call clear() first and then push() the new Models. |
309
|
|
|
* |
310
|
|
|
* @api |
311
|
|
|
* @param string $key |
312
|
|
|
* @param Model $model |
313
|
|
|
* @return self |
314
|
|
|
*/ |
315
|
|
|
public function push($key, Model $model) |
316
|
|
|
{ |
317
|
|
|
if (true === $this->isHasOne($key)) { |
318
|
|
|
return $this->setHasOne($key, $model); |
319
|
|
|
} |
320
|
|
|
if (false === $this->isHasMany($key)) { |
321
|
|
|
return $this; |
322
|
|
|
} |
323
|
|
|
if (true === $this->isInverse($key)) { |
324
|
|
|
throw ModelException::cannotModifyInverse($this, $key); |
325
|
|
|
} |
326
|
|
|
$this->touch(); |
327
|
|
|
$collection = $this->hasManyRelationships->get($key); |
328
|
|
|
$collection->push($model); |
329
|
|
|
$this->doDirtyCheck(); |
330
|
|
|
return $this; |
331
|
|
|
} |
332
|
|
|
|
333
|
|
|
/** |
334
|
|
|
* Clears a has-many relationship collection, sets an attribute to null, or sets a has-one relationship to null. |
335
|
|
|
* |
336
|
|
|
* @api |
337
|
|
|
* @param string $key The property key. |
338
|
|
|
* @return self |
339
|
|
|
*/ |
340
|
|
|
public function clear($key) |
341
|
|
|
{ |
342
|
|
|
if (true === $this->isAttribute($key)) { |
343
|
|
|
return $this->setAttribute($key, null); |
344
|
|
|
} |
345
|
|
|
if (true === $this->isHasOne($key)) { |
346
|
|
|
return $this->setHasOne($key, null); |
347
|
|
|
} |
348
|
|
|
if (true === $this->isInverse($key)) { |
349
|
|
|
throw ModelException::cannotModifyInverse($this, $key); |
350
|
|
|
} |
351
|
|
|
if (true === $this->isHasMany($key)) { |
352
|
|
|
$collection = $this->hasManyRelationships->get($key); |
353
|
|
|
$collection->clear(); |
354
|
|
|
$this->doDirtyCheck(); |
355
|
|
|
return $this; |
356
|
|
|
} |
357
|
|
|
return $this; |
358
|
|
|
} |
359
|
|
|
|
360
|
|
|
/** |
361
|
|
|
* Removes a specific Model from a has-many relationship collection. |
362
|
|
|
* |
363
|
|
|
* @api |
364
|
|
|
* @param string $key The has-many relationship key. |
365
|
|
|
* @param Model $model The model to remove from the collection. |
366
|
|
|
* @return self |
367
|
|
|
*/ |
368
|
|
View Code Duplication |
public function remove($key, Model $model) |
|
|
|
|
369
|
|
|
{ |
370
|
|
|
if (false === $this->isHasMany($key)) { |
371
|
|
|
return $this; |
372
|
|
|
} |
373
|
|
|
if (true === $this->isInverse($key)) { |
374
|
|
|
throw ModelException::cannotModifyInverse($this, $key); |
375
|
|
|
} |
376
|
|
|
$this->touch(); |
377
|
|
|
$collection = $this->hasManyRelationships->get($key); |
378
|
|
|
$collection->remove($model); |
379
|
|
|
$this->doDirtyCheck(); |
380
|
|
|
return $this; |
381
|
|
|
} |
382
|
|
|
|
383
|
|
|
/** |
384
|
|
|
* Sets a model property: an attribute value, a has-one model, or an entire has-many model collection. |
385
|
|
|
* Note: To push/remove a single Model into a has-many collection, or clear a collection, use @see push(), remove() and clear(). |
386
|
|
|
* Is a proxy for @see setAttribute() and setRelationship() |
387
|
|
|
* |
388
|
|
|
* @api |
389
|
|
|
* @param string $key The property field key. |
390
|
|
|
* @param Model|Collection|null|mixed The value to set. |
391
|
|
|
* @return self. |
|
|
|
|
392
|
|
|
*/ |
393
|
|
|
public function set($key, $value) |
394
|
|
|
{ |
395
|
|
|
if (true === $this->isAttribute($key)) { |
396
|
|
|
return $this->setAttribute($key, $value); |
397
|
|
|
} |
398
|
|
|
return $this->setRelationship($key, $value); |
399
|
|
|
} |
400
|
|
|
|
401
|
|
|
/** |
402
|
|
|
* Sets an attribute value. |
403
|
|
|
* Will convert the value to the proper, internal PHP/Modlr data type. |
404
|
|
|
* Will do a dirty check immediately after setting. |
405
|
|
|
* |
406
|
|
|
* @param string $key The attribute key (field) name. |
407
|
|
|
* @param mixed $value The value to apply. |
408
|
|
|
* @return self |
409
|
|
|
*/ |
410
|
|
|
protected function setAttribute($key, $value) |
411
|
|
|
{ |
412
|
|
|
if (true === $this->isCalculatedAttribute($key)) { |
413
|
|
|
return $this; |
414
|
|
|
} |
415
|
|
|
$this->touch(); |
416
|
|
|
$value = $this->convertAttributeValue($key, $value); |
417
|
|
|
$this->attributes->set($key, $value); |
418
|
|
|
$this->doDirtyCheck(); |
419
|
|
|
return $this; |
420
|
|
|
} |
421
|
|
|
|
422
|
|
|
protected function convertAttributeValue($key, $value) |
423
|
|
|
{ |
424
|
|
|
return $this->store->convertAttributeValue($this->getDataType($key), $value); |
425
|
|
|
} |
426
|
|
|
|
427
|
|
|
/** |
428
|
|
|
* Gets a data type from an attribute key. |
429
|
|
|
* |
430
|
|
|
* @param string $key The attribute key. |
431
|
|
|
* @return string |
432
|
|
|
*/ |
433
|
|
|
protected function getDataType($key) |
434
|
|
|
{ |
435
|
|
|
return $this->getMetadata()->getAttribute($key)->dataType; |
436
|
|
|
} |
437
|
|
|
|
438
|
|
|
/** |
439
|
|
|
* Sets a relationship value. |
440
|
|
|
* |
441
|
|
|
* @param string $key |
442
|
|
|
* @param Model|null $value |
443
|
|
|
* @return self |
444
|
|
|
*/ |
445
|
|
|
protected function setRelationship($key, $value) |
446
|
|
|
{ |
447
|
|
|
if (true === $this->isHasOne($key)) { |
448
|
|
|
return $this->setHasOne($key, $value); |
449
|
|
|
} |
450
|
|
|
if (true === $this->isHasMany($key)) { |
451
|
|
|
throw new \RuntimeException('You cannot set a hasMany relationship directly. Please access using push(), clear(), and/or remove()'); |
452
|
|
|
} |
453
|
|
|
return $this; |
454
|
|
|
} |
455
|
|
|
|
456
|
|
|
/** |
457
|
|
|
* Sets a has-one relationship. |
458
|
|
|
* |
459
|
|
|
* @param string $key The relationship key (field) name. |
460
|
|
|
* @param Model|null $model The model to relate. |
461
|
|
|
* @return self |
462
|
|
|
*/ |
463
|
|
View Code Duplication |
protected function setHasOne($key, Model $model = null) |
|
|
|
|
464
|
|
|
{ |
465
|
|
|
if (true === $this->isInverse($key)) { |
466
|
|
|
throw ModelException::cannotModifyInverse($this, $key); |
467
|
|
|
} |
468
|
|
|
if (null !== $model) { |
469
|
|
|
$this->validateRelSet($key, $model->getType()); |
470
|
|
|
} |
471
|
|
|
$this->touch(); |
472
|
|
|
$this->hasOneRelationships->set($key, $model); |
473
|
|
|
$this->doDirtyCheck(); |
474
|
|
|
return $this; |
475
|
|
|
} |
476
|
|
|
|
477
|
|
|
/** |
478
|
|
|
* Validates that the model type (from a Model or Collection instance) can be set to the relationship field. |
479
|
|
|
* |
480
|
|
|
* @param string $relKey The relationship field key. |
481
|
|
|
* @param string $type The model type that is being related. |
482
|
|
|
* @return self |
483
|
|
|
*/ |
484
|
|
|
protected function validateRelSet($relKey, $type) |
485
|
|
|
{ |
486
|
|
|
$relMeta = $this->getMetadata()->getRelationship($relKey); |
487
|
|
|
$relatedModelMeta = $this->store->getMetadataForRelationship($relMeta); |
|
|
|
|
488
|
|
|
$this->store->validateRelationshipSet($relatedModelMeta, $type); |
489
|
|
|
return $this; |
490
|
|
|
} |
491
|
|
|
|
492
|
|
|
/** |
493
|
|
|
* Determines if the model uses a particlar mixin. |
494
|
|
|
* |
495
|
|
|
* @api |
496
|
|
|
* @param string $name |
497
|
|
|
* @return bool |
498
|
|
|
*/ |
499
|
|
|
public function usesMixin($name) |
500
|
|
|
{ |
501
|
|
|
return $this->metadata->hasMixin($name); |
502
|
|
|
} |
503
|
|
|
|
504
|
|
|
/** |
505
|
|
|
* Saves the model. |
506
|
|
|
* |
507
|
|
|
* @api |
508
|
|
|
* @param Implement cascade relationship saves. Or should the store handle this? |
509
|
|
|
* @return self |
510
|
|
|
*/ |
511
|
|
|
public function save() |
512
|
|
|
{ |
513
|
|
|
if (true === $this->getState()->is('deleted')) { |
514
|
|
|
return $this; |
515
|
|
|
} |
516
|
|
|
$this->store->commit($this); |
517
|
|
|
return $this; |
518
|
|
|
} |
519
|
|
|
|
520
|
|
|
/** |
521
|
|
|
* Rolls back a model to its original, database values. |
522
|
|
|
* |
523
|
|
|
* @api |
524
|
|
|
* @return self |
525
|
|
|
*/ |
526
|
|
|
public function rollback() |
527
|
|
|
{ |
528
|
|
|
$this->attributes->rollback(); |
529
|
|
|
$this->hasOneRelationships->rollback(); |
530
|
|
|
$this->hasManyRelationships->rollback(); |
531
|
|
|
$this->doDirtyCheck(); |
532
|
|
|
return $this; |
533
|
|
|
} |
534
|
|
|
|
535
|
|
|
/** |
536
|
|
|
* Reloads the model from the database. |
537
|
|
|
* |
538
|
|
|
* @api |
539
|
|
|
* @return self |
540
|
|
|
*/ |
541
|
|
|
public function reload() |
542
|
|
|
{ |
543
|
|
|
return $this->touch(true); |
544
|
|
|
} |
545
|
|
|
|
546
|
|
|
/** |
547
|
|
|
* Restores an in-memory deleted object back to the database. |
548
|
|
|
* |
549
|
|
|
* @api |
550
|
|
|
* @todo Implement if needed. Or should restore clear a pending delete? |
551
|
|
|
* @return self |
552
|
|
|
*/ |
553
|
|
|
public function restore() |
554
|
|
|
{ |
555
|
|
|
return $this; |
556
|
|
|
} |
557
|
|
|
|
558
|
|
|
/** |
559
|
|
|
* Marks the record for deletion. |
560
|
|
|
* Will not remove from the database until $this->save() is called. |
561
|
|
|
* |
562
|
|
|
* @api |
563
|
|
|
* @return self |
564
|
|
|
* @throws \RuntimeException If a new (unsaved) model is deleted. |
565
|
|
|
*/ |
566
|
|
|
public function delete() |
567
|
|
|
{ |
568
|
|
|
if (true === $this->getState()->is('new')) { |
569
|
|
|
throw new \RuntimeException('You cannot delete a new model'); |
570
|
|
|
} |
571
|
|
|
if (true === $this->getState()->is('deleted')) { |
572
|
|
|
return $this; |
573
|
|
|
} |
574
|
|
|
$this->getState()->setDeleting(); |
575
|
|
|
return $this; |
576
|
|
|
} |
577
|
|
|
|
578
|
|
|
/** |
579
|
|
|
* Touches the model. |
580
|
|
|
* If the model is currently empty, it will query the database and fill/load the model. |
581
|
|
|
* |
582
|
|
|
* @param bool $force Whether to force the load, even if the model is currently loaded. |
583
|
|
|
* @return self |
584
|
|
|
*/ |
585
|
|
|
protected function touch($force = false) |
586
|
|
|
{ |
587
|
|
|
if (true === $this->getState()->is('deleted')) { |
588
|
|
|
return $this; |
589
|
|
|
} |
590
|
|
|
if (true === $this->getState()->is('empty') || true === $force) { |
591
|
|
|
$record = $this->store->retrieveRecord($this->getType(), $this->getId()); |
592
|
|
|
$this->initialize($record); |
593
|
|
|
$this->state->setLoaded(); |
594
|
|
|
// @todo Should this trigger a postReload event? Likely not. |
595
|
|
|
} |
596
|
|
|
return $this; |
597
|
|
|
} |
598
|
|
|
|
599
|
|
|
/** |
600
|
|
|
* Applies an array of raw model properties (attributes and relationships) to the model instance. |
601
|
|
|
* |
602
|
|
|
* @todo Confirm that we want this method. It's currently used for creating and updating via the API adapter. Also see initialize() |
603
|
|
|
* @param array $properties The properties to apply. |
604
|
|
|
* @return self |
605
|
|
|
*/ |
606
|
|
|
public function apply(array $properties) |
607
|
|
|
{ |
608
|
|
|
$properties = $this->applyDefaultAttrValues($properties); |
609
|
|
|
foreach ($properties as $key => $value) { |
610
|
|
|
if (true === $this->isAttribute($key)) { |
611
|
|
|
$this->set($key, $value); |
612
|
|
|
continue; |
613
|
|
|
} |
614
|
|
|
if (true === $this->isHasOne($key)) { |
615
|
|
|
if (empty($value)) { |
616
|
|
|
$this->clear($key); |
617
|
|
|
continue; |
618
|
|
|
} |
619
|
|
|
$value = $this->store->loadProxyModel($value['type'], $value['id']); |
620
|
|
|
$this->set($key, $value); |
621
|
|
|
continue; |
622
|
|
|
} |
623
|
|
|
|
624
|
|
|
} |
625
|
|
|
|
626
|
|
|
foreach ($this->getMetadata()->getRelationships() as $key => $relMeta) { |
627
|
|
|
if (true === $relMeta->isOne()) { |
628
|
|
|
continue; |
629
|
|
|
} |
630
|
|
|
// Array key exists must exist to determine if the |
631
|
|
|
if (!isset($properties[$key]) || true === $relMeta->isInverse) { |
632
|
|
|
continue; |
633
|
|
|
} |
634
|
|
|
|
635
|
|
|
$this->clear($key); |
636
|
|
|
$collection = $this->store->createCollection($relMeta, $properties[$key]); |
637
|
|
|
foreach ($collection->allWithoutLoad() as $value) { |
638
|
|
|
$this->push($key, $value); |
639
|
|
|
} |
640
|
|
|
} |
641
|
|
|
$this->doDirtyCheck(); |
642
|
|
|
return $this; |
643
|
|
|
} |
644
|
|
|
|
645
|
|
|
/** |
646
|
|
|
* Initializes the model and loads its attributes and relationships. |
647
|
|
|
* |
648
|
|
|
* @todo Made public so collections can initialize models. Not sure if we want this?? |
649
|
|
|
* @param Record|null $record The db attributes and relationships to apply. |
650
|
|
|
* @return self |
651
|
|
|
*/ |
652
|
|
|
public function initialize(Record $record = null) |
653
|
|
|
{ |
654
|
|
|
$hasOne = []; |
655
|
|
|
$hasMany = []; |
656
|
|
|
$attributes = []; |
657
|
|
|
|
658
|
|
|
if (null !== $record) { |
659
|
|
|
$attributes = $this->applyDefaultAttrValues($attributes); |
660
|
|
|
foreach ($record->getProperties() as $key => $value) { |
661
|
|
|
if (true === $this->isAttribute($key)) { |
662
|
|
|
// Load attribute. |
663
|
|
|
$attributes[$key] = $this->convertAttributeValue($key, $value); |
664
|
|
|
continue; |
665
|
|
|
} |
666
|
|
|
if (true === $this->isHasOne($key)) { |
667
|
|
|
// Load hasOne relationship. |
668
|
|
|
$hasOne[$key] = $this->store->loadProxyModel($value['type'], $value['id']); |
669
|
|
|
continue; |
670
|
|
|
} |
671
|
|
|
} |
672
|
|
|
} |
673
|
|
|
|
674
|
|
|
foreach ($this->getMetadata()->getRelationships() as $key => $relMeta) { |
675
|
|
|
if (true === $relMeta->isOne()) { |
676
|
|
|
continue; |
677
|
|
|
} |
678
|
|
|
if (true === $relMeta->isInverse) { |
679
|
|
|
$hasMany[$key] = $this->store->createInverseCollection($relMeta, $this); |
680
|
|
|
} else { |
681
|
|
|
$references = (null === $record || !isset($record->getProperties()[$key])) ? [] : $record->getProperties()[$key]; |
682
|
|
|
$hasMany[$key] = $this->store->createCollection($relMeta, $references); |
683
|
|
|
} |
684
|
|
|
} |
685
|
|
|
|
686
|
|
|
$this->attributes = (null === $this->attributes) ? new Attributes($attributes) : $this->attributes->replace($attributes); |
687
|
|
|
$this->hasOneRelationships = (null === $this->hasOneRelationships) ? new Relationships\HasOne($hasOne) : $this->hasOneRelationships->replace($hasOne); |
688
|
|
|
$this->hasManyRelationships = (null === $this->hasManyRelationships) ? new Relationships\HasMany($hasMany) : $this->hasManyRelationships->replace($hasMany); |
689
|
|
|
$this->doDirtyCheck(); |
690
|
|
|
return $this; |
691
|
|
|
} |
692
|
|
|
|
693
|
|
|
/** |
694
|
|
|
* Applies default attribute values from metadata, if set. |
695
|
|
|
* |
696
|
|
|
* @param array $attributes The attributes to apply the defaults to. |
697
|
|
|
* @return array |
698
|
|
|
*/ |
699
|
|
|
protected function applyDefaultAttrValues(array $attributes = []) |
700
|
|
|
{ |
701
|
|
|
// Set defaults for each attribute. |
702
|
|
|
foreach ($this->getMetadata()->getAttributes() as $key => $attrMeta) { |
703
|
|
|
if (!isset($attrMeta->defaultValue) || isset($attributes[$key])) { |
704
|
|
|
continue; |
705
|
|
|
} |
706
|
|
|
$attributes[$key] = $this->convertAttributeValue($key, $attrMeta->defaultValue); |
707
|
|
|
} |
708
|
|
|
|
709
|
|
|
// Set defaults for the entire entity. |
710
|
|
|
foreach ($this->getMetadata()->defaultValues as $key => $value) { |
711
|
|
|
if (isset($attributes[$key])) { |
712
|
|
|
continue; |
713
|
|
|
} |
714
|
|
|
$attributes[$key] = $this->convertAttributeValue($key, $value); |
715
|
|
|
} |
716
|
|
|
return $attributes; |
717
|
|
|
} |
718
|
|
|
|
719
|
|
|
/** |
720
|
|
|
* Determines if the model is currently dirty. |
721
|
|
|
* Checks against the attribute and relationship dirty states. |
722
|
|
|
* |
723
|
|
|
* @api |
724
|
|
|
* @return bool |
725
|
|
|
*/ |
726
|
|
|
public function isDirty() |
727
|
|
|
{ |
728
|
|
|
return true === $this->attributes->areDirty() |
729
|
|
|
|| true === $this->hasOneRelationships->areDirty() |
730
|
|
|
|| true === $this->hasManyRelationships->areDirty() |
731
|
|
|
; |
732
|
|
|
} |
733
|
|
|
|
734
|
|
|
/** |
735
|
|
|
* Does a dirty check and sets the state to this model. |
736
|
|
|
* |
737
|
|
|
* @return self |
738
|
|
|
*/ |
739
|
|
|
protected function doDirtyCheck() |
740
|
|
|
{ |
741
|
|
|
$this->state->setDirty($this->isDirty()); |
742
|
|
|
return $this; |
743
|
|
|
} |
744
|
|
|
|
745
|
|
|
/** |
746
|
|
|
* Gets the current change set of attributes and relationships. |
747
|
|
|
* |
748
|
|
|
* @api |
749
|
|
|
* @return array |
750
|
|
|
*/ |
751
|
|
|
public function getChangeSet() |
752
|
|
|
{ |
753
|
|
|
$changeset = [ |
754
|
|
|
'attributes' => $this->attributes->calculateChangeSet(), |
755
|
|
|
'hasOne' => $this->hasOneRelationships->calculateChangeSet(), |
756
|
|
|
'hasMany' => $this->hasManyRelationships->calculateChangeSet(), |
757
|
|
|
]; |
758
|
|
|
|
759
|
|
|
foreach ($changeset as $type => $properties) { |
760
|
|
|
$changeset[$type] = $this->filterNotSavedProperties($type, $properties); |
761
|
|
|
} |
762
|
|
|
return $changeset; |
763
|
|
|
} |
764
|
|
|
|
765
|
|
|
/** |
766
|
|
|
* Gets the model state object. |
767
|
|
|
* |
768
|
|
|
* @todo Should this be public? State setting should likely be locked from the outside world. |
769
|
|
|
* @return State |
770
|
|
|
*/ |
771
|
|
|
public function getState() |
772
|
|
|
{ |
773
|
|
|
return $this->state; |
774
|
|
|
} |
775
|
|
|
|
776
|
|
|
/** |
777
|
|
|
* Gets the metadata for this model. |
778
|
|
|
* |
779
|
|
|
* @return EntityMetadata |
780
|
|
|
*/ |
781
|
|
|
public function getMetadata() |
782
|
|
|
{ |
783
|
|
|
return $this->metadata; |
784
|
|
|
} |
785
|
|
|
|
786
|
|
|
/** |
787
|
|
|
* Removes properties marked as non-saved. |
788
|
|
|
* |
789
|
|
|
* @param string $propType |
790
|
|
|
* @param array $properties |
791
|
|
|
* @return array |
792
|
|
|
*/ |
793
|
|
|
private function filterNotSavedProperties($propType, array $properties) |
794
|
|
|
{ |
795
|
|
|
$method = ('attributes' === $propType) ? 'getAttributes' : 'getRelationships'; |
796
|
|
|
foreach ($this->getMetadata()->$method() as $fieldKey => $propMeta) { |
797
|
|
|
if (true === $propMeta->shouldSave() || !isset($properties[$fieldKey])) { |
798
|
|
|
continue; |
799
|
|
|
} |
800
|
|
|
unset($properties[$fieldKey]); |
801
|
|
|
} |
802
|
|
|
return $properties; |
803
|
|
|
} |
804
|
|
|
} |
805
|
|
|
|
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.