1
|
|
|
<?php |
2
|
|
|
namespace Darya\ORM; |
3
|
|
|
|
4
|
|
|
use Darya\ORM\Record; |
5
|
|
|
use Darya\Storage\Readable; |
6
|
|
|
use Darya\Storage\Modifiable; |
7
|
|
|
use Darya\Storage\Queryable; |
8
|
|
|
use Darya\Storage\Query\Builder; |
9
|
|
|
use Exception; |
10
|
|
|
use InvalidArgumentException; |
11
|
|
|
use ReflectionClass; |
12
|
|
|
|
13
|
|
|
/** |
14
|
|
|
* Darya's abstract entity relation. |
15
|
|
|
* |
16
|
|
|
* TODO: errors() method. |
17
|
|
|
* TODO: Filter, order, limit, offset for load() and retrieve(). |
18
|
|
|
* TODO: Shouldn't delimitClass() and prepareForeignKey() be static? |
19
|
|
|
* |
20
|
|
|
* @property-read string $name |
21
|
|
|
* @property-read Record $parent |
22
|
|
|
* @property-read Record $target |
23
|
|
|
* @property-read string $foreignKey |
24
|
|
|
* @property-read string $localKey |
25
|
|
|
* @property-read array $constraint |
26
|
|
|
* @property-read Record[] $related |
27
|
|
|
* @property-read bool $loaded |
28
|
|
|
* @property-read Queryable $storage |
29
|
|
|
* |
30
|
|
|
* @author Chris Andrew <[email protected]> |
31
|
|
|
*/ |
32
|
|
|
abstract class Relation |
33
|
|
|
{ |
34
|
|
|
const HAS = 'has'; |
35
|
|
|
const HAS_MANY = 'has_many'; |
36
|
|
|
const BELONGS_TO = 'belongs_to'; |
37
|
|
|
const BELONGS_TO_MANY = 'belongs_to_many'; |
38
|
|
|
|
39
|
|
|
/** |
40
|
|
|
* A map of relation type constants to their respective implementations. |
41
|
|
|
* |
42
|
|
|
* @var array |
43
|
|
|
*/ |
44
|
|
|
protected static $classMap = array( |
45
|
|
|
self::HAS => 'Darya\ORM\Relation\Has', |
46
|
|
|
self::HAS_MANY => 'Darya\ORM\Relation\HasMany', |
47
|
|
|
self::BELONGS_TO => 'Darya\ORM\Relation\BelongsTo', |
48
|
|
|
self::BELONGS_TO_MANY => 'Darya\ORM\Relation\BelongsToMany', |
49
|
|
|
); |
50
|
|
|
|
51
|
|
|
/** |
52
|
|
|
* The name of the relation in the context of the parent model. |
53
|
|
|
* |
54
|
|
|
* @var string |
55
|
|
|
*/ |
56
|
|
|
protected $name = ''; |
57
|
|
|
|
58
|
|
|
/** |
59
|
|
|
* The parent model. |
60
|
|
|
* |
61
|
|
|
* @var Record |
62
|
|
|
*/ |
63
|
|
|
protected $parent; |
64
|
|
|
|
65
|
|
|
/** |
66
|
|
|
* The target model. |
67
|
|
|
* |
68
|
|
|
* @var Record |
69
|
|
|
*/ |
70
|
|
|
protected $target; |
71
|
|
|
|
72
|
|
|
/** |
73
|
|
|
* Foreign key on the "belongs-to" model. |
74
|
|
|
* |
75
|
|
|
* @var string |
76
|
|
|
*/ |
77
|
|
|
protected $foreignKey; |
78
|
|
|
|
79
|
|
|
/** |
80
|
|
|
* Local key on the "has" model. |
81
|
|
|
* |
82
|
|
|
* @var string |
83
|
|
|
*/ |
84
|
|
|
protected $localKey; |
85
|
|
|
|
86
|
|
|
/** |
87
|
|
|
* Filter for constraining related models loaded from storage. |
88
|
|
|
* |
89
|
|
|
* @var array |
90
|
|
|
*/ |
91
|
|
|
protected $constraint = array(); |
92
|
|
|
|
93
|
|
|
/** |
94
|
|
|
* Sort order for related models. |
95
|
|
|
* |
96
|
|
|
* @var array |
97
|
|
|
*/ |
98
|
|
|
protected $sort = array(); |
99
|
|
|
|
100
|
|
|
/** |
101
|
|
|
* The related instances. |
102
|
|
|
* |
103
|
|
|
* @var Record[] |
104
|
|
|
*/ |
105
|
|
|
protected $related = array(); |
106
|
|
|
|
107
|
|
|
/** |
108
|
|
|
* Detached instances that need dissociating on save. |
109
|
|
|
* |
110
|
|
|
* @var Record[] |
111
|
|
|
*/ |
112
|
|
|
protected $detached = array(); |
113
|
|
|
|
114
|
|
|
/** |
115
|
|
|
* Determines whether related instances have been loaded. |
116
|
|
|
* |
117
|
|
|
* @var bool |
118
|
|
|
*/ |
119
|
|
|
protected $loaded = false; |
120
|
|
|
|
121
|
|
|
/** |
122
|
|
|
* The storage interface. |
123
|
|
|
* |
124
|
|
|
* @var Queryable |
125
|
|
|
*/ |
126
|
|
|
protected $storage; |
127
|
|
|
|
128
|
|
|
/** |
129
|
|
|
* Helper method for methods that accept single or multiple values, or for |
130
|
|
|
* just casting to an array without losing a plain object. |
131
|
|
|
* |
132
|
|
|
* Returns an array with the given value as its sole element, if it is not |
133
|
|
|
* an array already. |
134
|
|
|
* |
135
|
|
|
* This exists because casting an object to an array results in its public |
136
|
|
|
* properties being set as the values. |
137
|
|
|
* |
138
|
|
|
* @param mixed $value |
139
|
|
|
* @return array |
140
|
|
|
*/ |
141
|
|
|
protected static function arrayify($value) |
142
|
|
|
{ |
143
|
|
|
return !is_array($value) ? array($value) : $value; |
144
|
|
|
} |
145
|
|
|
|
146
|
|
|
/** |
147
|
|
|
* Separate array elements with numeric keys from those with string keys. |
148
|
|
|
* |
149
|
|
|
* @param array $array |
150
|
|
|
* @return array array($numeric, $strings) |
151
|
|
|
*/ |
152
|
|
|
protected static function separateKeys(array $array) |
153
|
|
|
{ |
154
|
|
|
$numeric = array(); |
155
|
|
|
$strings = array(); |
156
|
|
|
|
157
|
|
|
foreach ($array as $key => $value) { |
158
|
|
|
if (is_numeric($key)) { |
159
|
|
|
$numeric[$key] = $value; |
160
|
|
|
} else { |
161
|
|
|
$strings[$key] = $value; |
162
|
|
|
} |
163
|
|
|
} |
164
|
|
|
|
165
|
|
|
return array($numeric, $strings); |
166
|
|
|
} |
167
|
|
|
|
168
|
|
|
/** |
169
|
|
|
* Resolve a relation class name from the given relation type constant. |
170
|
|
|
* |
171
|
|
|
* @param string $type |
172
|
|
|
* @return string |
173
|
|
|
*/ |
174
|
|
|
protected static function resolveClass($type) |
175
|
|
|
{ |
176
|
|
|
if (isset(static::$classMap[$type])) { |
177
|
|
|
return static::$classMap[$type]; |
178
|
|
|
} |
179
|
|
|
|
180
|
|
|
return static::$classMap[static::HAS]; |
181
|
|
|
} |
182
|
|
|
|
183
|
|
|
/** |
184
|
|
|
* Create a new relation of the given type using the given arguments. |
185
|
|
|
* |
186
|
|
|
* Applies numerically-keyed arguments to the constructor and string-keyed |
187
|
|
|
* arguments to methods with the same name. |
188
|
|
|
* |
189
|
|
|
* @param string $type |
190
|
|
|
* @param array $arguments |
191
|
|
|
* @return Relation |
192
|
|
|
*/ |
193
|
|
|
public static function factory($type = self::HAS, array $arguments) |
194
|
|
|
{ |
195
|
|
|
$class = static::resolveClass($type); |
196
|
|
|
|
197
|
|
|
$reflection = new ReflectionClass($class); |
198
|
|
|
|
199
|
|
|
list($arguments, $named) = static::separateKeys($arguments); |
200
|
|
|
|
201
|
|
|
/** |
202
|
|
|
* @var Relation $instance |
203
|
|
|
*/ |
204
|
|
|
$instance = $reflection->newInstanceArgs($arguments); |
205
|
|
|
|
206
|
|
|
foreach ($named as $method => $argument) { |
207
|
|
|
if (method_exists($instance, $method)) { |
208
|
|
|
$argument = static::arrayify($argument); |
209
|
|
|
call_user_func_array(array($instance, $method), $argument); |
210
|
|
|
} |
211
|
|
|
} |
212
|
|
|
|
213
|
|
|
return $instance; |
214
|
|
|
} |
215
|
|
|
|
216
|
|
|
/** |
217
|
|
|
* Instantiate a new relation. |
218
|
|
|
* |
219
|
|
|
* @param Record $parent Parent class |
220
|
|
|
* @param string $target Related class that extends \Darya\ORM\Record |
221
|
|
|
* @param string $foreignKey [optional] Custom foreign key |
222
|
|
|
* @param array $constraint [optional] Constraint filter for related models |
223
|
|
|
* @throws InvalidArgumentException |
224
|
|
|
*/ |
225
|
|
|
public function __construct(Record $parent, $target, $foreignKey = null, array $constraint = array()) |
226
|
|
|
{ |
227
|
|
|
if (!is_subclass_of($target, 'Darya\ORM\Record')) { |
228
|
|
|
throw new InvalidArgumentException('Target class not does not extend Darya\ORM\Record'); |
229
|
|
|
} |
230
|
|
|
|
231
|
|
|
$this->parent = $parent; |
232
|
|
|
$this->target = !is_object($target) ? new $target : $target; |
233
|
|
|
|
234
|
|
|
$this->foreignKey = $foreignKey; |
235
|
|
|
$this->setDefaultKeys(); |
236
|
|
|
$this->constrain($constraint); |
237
|
|
|
} |
238
|
|
|
|
239
|
|
|
/** |
240
|
|
|
* Lowercase and delimit the given PascalCase class name. |
241
|
|
|
* |
242
|
|
|
* @param string $class |
243
|
|
|
* @return string |
244
|
|
|
*/ |
245
|
|
|
protected function delimitClass($class) |
246
|
|
|
{ |
247
|
|
|
$split = explode('\\', $class); |
248
|
|
|
$class = end($split); |
249
|
|
|
|
250
|
|
|
return preg_replace_callback('/([A-Z])/', function ($matches) { |
251
|
|
|
return '_' . strtolower($matches[1]); |
252
|
|
|
}, lcfirst($class)); |
253
|
|
|
} |
254
|
|
|
|
255
|
|
|
/** |
256
|
|
|
* Prepare a foreign key from the given class name. |
257
|
|
|
* |
258
|
|
|
* @param string $class |
259
|
|
|
* @return string |
260
|
|
|
*/ |
261
|
|
|
protected function prepareForeignKey($class) |
262
|
|
|
{ |
263
|
|
|
return $this->delimitClass($class) . '_id'; |
264
|
|
|
} |
265
|
|
|
|
266
|
|
|
/** |
267
|
|
|
* Retrieve the default filter for the related models. |
268
|
|
|
* |
269
|
|
|
* @return array |
270
|
|
|
*/ |
271
|
|
|
protected function defaultConstraint() |
272
|
|
|
{ |
273
|
|
|
return array( |
274
|
|
|
$this->foreignKey => $this->parent->id() |
275
|
|
|
); |
276
|
|
|
} |
277
|
|
|
|
278
|
|
|
/** |
279
|
|
|
* Set the default keys for the relation if they haven't already been set. |
280
|
|
|
*/ |
281
|
|
|
abstract protected function setDefaultKeys(); |
282
|
|
|
|
283
|
|
|
/** |
284
|
|
|
* Retrieve the values of the given attribute of the given instances. |
285
|
|
|
* |
286
|
|
|
* Works similarly to array_column(), but doesn't return data from any rows |
287
|
|
|
* without the given attribute set. |
288
|
|
|
* |
289
|
|
|
* Optionally accepts a second attribute to index by. |
290
|
|
|
* |
291
|
|
|
* @param Record[]|Record|array $instances |
292
|
|
|
* @param string $attribute |
293
|
|
|
* @param string $index [optional] |
294
|
|
|
* @return array |
295
|
|
|
*/ |
296
|
|
|
protected static function attributeList($instances, $attribute, $index = null) |
297
|
|
|
{ |
298
|
|
|
$values = array(); |
299
|
|
|
|
300
|
|
|
foreach (static::arrayify($instances) as $instance) { |
301
|
|
|
if (isset($instance[$attribute])) { |
302
|
|
|
if ($index !== null) { |
303
|
|
|
$values[$instance[$index]] = $instance[$attribute]; |
304
|
|
|
} else { |
305
|
|
|
$values[] = $instance[$attribute]; |
306
|
|
|
} |
307
|
|
|
} |
308
|
|
|
} |
309
|
|
|
|
310
|
|
|
return $values; |
311
|
|
|
} |
312
|
|
|
|
313
|
|
|
/** |
314
|
|
|
* Build an adjacency list of related models, indexed by their foreign keys. |
315
|
|
|
* |
316
|
|
|
* Optionally accepts a different attribute to index the models by. |
317
|
|
|
* |
318
|
|
|
* @param Record[] $instances |
319
|
|
|
* @param string $index [optional] |
320
|
|
|
* @return array |
321
|
|
|
*/ |
322
|
|
|
protected function adjacencyList(array $instances, $index = null) |
323
|
|
|
{ |
324
|
|
|
$index = $index ?: $this->foreignKey; |
325
|
|
|
|
326
|
|
|
$related = array(); |
327
|
|
|
|
328
|
|
|
foreach ($instances as $instance) { |
329
|
|
|
$related[$instance->get($index)][] = $instance; |
330
|
|
|
} |
331
|
|
|
|
332
|
|
|
return $related; |
333
|
|
|
} |
334
|
|
|
|
335
|
|
|
/** |
336
|
|
|
* Reduce the cached related models to those with the given IDs. |
337
|
|
|
* |
338
|
|
|
* If no IDs are given then all of the in-memory models will be removed. |
339
|
|
|
* |
340
|
|
|
* @param int[] $ids |
341
|
|
|
*/ |
342
|
|
|
protected function reduce(array $ids = array()) |
343
|
|
|
{ |
344
|
|
|
if (empty($this->related)) { |
345
|
|
|
return; |
346
|
|
|
} |
347
|
|
|
|
348
|
|
|
$keys = array(); |
349
|
|
|
|
350
|
|
|
foreach ($this->related as $key => $instance) { |
351
|
|
|
if (!in_array($instance->id(), $ids)) { |
352
|
|
|
$keys[$key] = null; |
353
|
|
|
} |
354
|
|
|
} |
355
|
|
|
|
356
|
|
|
$this->related = array_values(array_diff_key($this->related, $keys)); |
357
|
|
|
} |
358
|
|
|
|
359
|
|
|
/** |
360
|
|
|
* Replace a cached related model. |
361
|
|
|
* |
362
|
|
|
* If the related model does not have an ID or it is not found, it is simply |
363
|
|
|
* appended. |
364
|
|
|
* |
365
|
|
|
* TODO: Remove from $this->detached if found? |
366
|
|
|
* |
367
|
|
|
* @param Record $instance |
368
|
|
|
*/ |
369
|
|
|
protected function replace(Record $instance) |
370
|
|
|
{ |
371
|
|
|
$this->verify($instance); |
372
|
|
|
|
373
|
|
|
if (!$instance->id()) { |
374
|
|
|
$this->related[] = $instance; |
375
|
|
|
|
376
|
|
|
return; |
377
|
|
|
} |
378
|
|
|
|
379
|
|
|
foreach ($this->related as $key => $related) { |
380
|
|
|
if ($related->id() === $instance->id() || $related === $instance) { |
381
|
|
|
$this->related[$key] = $instance; |
382
|
|
|
|
383
|
|
|
return; |
384
|
|
|
} |
385
|
|
|
} |
386
|
|
|
|
387
|
|
|
$this->related[] = $instance; |
388
|
|
|
} |
389
|
|
|
|
390
|
|
|
/** |
391
|
|
|
* Save the given record to storage if it hasn't got an ID. |
392
|
|
|
* |
393
|
|
|
* @param Record $instance |
394
|
|
|
*/ |
395
|
|
|
protected function persist(Record $instance) |
396
|
|
|
{ |
397
|
|
|
if (!$instance->id()) { |
398
|
|
|
$instance->save(); |
399
|
|
|
} |
400
|
|
|
} |
401
|
|
|
|
402
|
|
|
/** |
403
|
|
|
* Verify that the given models are instances of the relation's target |
404
|
|
|
* class. |
405
|
|
|
* |
406
|
|
|
* Throws an exception if any of them aren't. |
407
|
|
|
* |
408
|
|
|
* @param Record[]|Record $instances |
409
|
|
|
* @throws Exception |
410
|
|
|
*/ |
411
|
|
|
protected function verify($instances) |
412
|
|
|
{ |
413
|
|
|
static::verifyModels($instances, get_class($this->target)); |
414
|
|
|
} |
415
|
|
|
|
416
|
|
|
/** |
417
|
|
|
* Verify that the given objects are instances of the given class. |
418
|
|
|
* |
419
|
|
|
* @param object[]|object $instances |
420
|
|
|
* @param string $class |
421
|
|
|
* @throws Exception |
422
|
|
|
*/ |
423
|
|
|
protected static function verifyModels($instances, $class) |
424
|
|
|
{ |
425
|
|
|
if (!class_exists($class)) { |
426
|
|
|
return; |
427
|
|
|
} |
428
|
|
|
|
429
|
|
|
foreach (static::arrayify($instances) as $instance) { |
430
|
|
|
if (!$instance instanceof $class) { |
431
|
|
|
throw new Exception('Related models must be an instance of ' . $class); |
432
|
|
|
} |
433
|
|
|
} |
434
|
|
|
} |
435
|
|
|
|
436
|
|
|
/** |
437
|
|
|
* Verify that the given models are instances of the relation's parent |
438
|
|
|
* class. |
439
|
|
|
* |
440
|
|
|
* Throws an exception if any of them aren't. |
441
|
|
|
* |
442
|
|
|
* @param Record[]|Record $instances |
443
|
|
|
* @throws Exception |
444
|
|
|
*/ |
445
|
|
|
protected function verifyParents($instances) |
446
|
|
|
{ |
447
|
|
|
static::verifyModels($instances, get_class($this->parent)); |
448
|
|
|
} |
449
|
|
|
|
450
|
|
|
/** |
451
|
|
|
* Retrieve and optionally set the storage used for the target model. |
452
|
|
|
* |
453
|
|
|
* Falls back to target model storage, then parent model storage. |
454
|
|
|
* |
455
|
|
|
* @param Queryable $storage |
456
|
|
|
* @return Queryable |
457
|
|
|
*/ |
458
|
|
|
public function storage(Queryable $storage = null) |
459
|
|
|
{ |
460
|
|
|
$this->storage = $storage ?: $this->storage; |
461
|
|
|
|
462
|
|
|
return $this->storage ?: $this->target->storage() ?: $this->parent->storage(); |
463
|
|
|
} |
464
|
|
|
|
465
|
|
|
/** |
466
|
|
|
* Retrieve and optionally set the name of the relation on the parent model. |
467
|
|
|
* |
468
|
|
|
* @param string $name [optional] |
469
|
|
|
* @return string |
470
|
|
|
*/ |
471
|
|
|
public function name($name = '') |
472
|
|
|
{ |
473
|
|
|
$this->name = (string) $name ?: $this->name; |
474
|
|
|
|
475
|
|
|
return $this->name; |
476
|
|
|
} |
477
|
|
|
|
478
|
|
|
/** |
479
|
|
|
* Retrieve and optionally set the foreign key for the "belongs-to" model. |
480
|
|
|
* |
481
|
|
|
* @param string $foreignKey [optional] |
482
|
|
|
* @return string |
483
|
|
|
*/ |
484
|
|
|
public function foreignKey($foreignKey = '') |
485
|
|
|
{ |
486
|
|
|
$this->foreignKey = (string) $foreignKey ?: $this->foreignKey; |
487
|
|
|
|
488
|
|
|
return $this->foreignKey; |
489
|
|
|
} |
490
|
|
|
|
491
|
|
|
/** |
492
|
|
|
* Retrieve and optionally set the local key for the "has" model. |
493
|
|
|
* |
494
|
|
|
* @param string $localKey [optional] |
495
|
|
|
* @return string |
496
|
|
|
*/ |
497
|
|
|
public function localKey($localKey = '') |
498
|
|
|
{ |
499
|
|
|
$this->localKey = (string) $localKey ?: $this->localKey; |
500
|
|
|
|
501
|
|
|
return $this->localKey; |
502
|
|
|
} |
503
|
|
|
|
504
|
|
|
/** |
505
|
|
|
* Set a filter to constrain which models are considered related. |
506
|
|
|
* |
507
|
|
|
* @param array $filter |
508
|
|
|
*/ |
509
|
|
|
public function constrain(array $filter) |
510
|
|
|
{ |
511
|
|
|
$this->constraint = $filter; |
512
|
|
|
} |
513
|
|
|
|
514
|
|
|
/** |
515
|
|
|
* Retrieve the custom filter used to constrain related models. |
516
|
|
|
* |
517
|
|
|
* @return array |
518
|
|
|
*/ |
519
|
|
|
public function constraint() |
520
|
|
|
{ |
521
|
|
|
return $this->constraint; |
522
|
|
|
} |
523
|
|
|
|
524
|
|
|
/** |
525
|
|
|
* Retrieve the filter for this relation. |
526
|
|
|
* |
527
|
|
|
* @return array |
528
|
|
|
*/ |
529
|
|
|
public function filter() |
530
|
|
|
{ |
531
|
|
|
return array_merge($this->defaultConstraint(), $this->constraint()); |
532
|
|
|
} |
533
|
|
|
|
534
|
|
|
/** |
535
|
|
|
* Set the sorting order for this relation. |
536
|
|
|
* |
537
|
|
|
* @param array|string $order |
538
|
|
|
* @return array|string |
539
|
|
|
*/ |
540
|
|
|
public function sort($order) |
541
|
|
|
{ |
542
|
|
|
return $this->sort = $order; |
|
|
|
|
543
|
|
|
} |
544
|
|
|
|
545
|
|
|
/** |
546
|
|
|
* Retrieve the order for this relation. |
547
|
|
|
* |
548
|
|
|
* @return array|string |
549
|
|
|
*/ |
550
|
|
|
public function order() |
551
|
|
|
{ |
552
|
|
|
return $this->sort; |
553
|
|
|
} |
554
|
|
|
|
555
|
|
|
/** |
556
|
|
|
* Read related model data from storage. |
557
|
|
|
* |
558
|
|
|
* TODO: $filter, $order, $offset |
559
|
|
|
* |
560
|
|
|
* @param int $limit [optional] |
561
|
|
|
* @return array |
562
|
|
|
*/ |
563
|
|
|
public function read($limit = 0) |
564
|
|
|
{ |
565
|
|
|
return $this->storage()->read($this->target->table(), $this->filter(), $this->order(), $limit); |
566
|
|
|
} |
567
|
|
|
|
568
|
|
|
/** |
569
|
|
|
* Query related model data from storage. |
570
|
|
|
* |
571
|
|
|
* @return Builder |
572
|
|
|
*/ |
573
|
|
|
public function query() |
574
|
|
|
{ |
575
|
|
|
$class = get_class($this->target); |
576
|
|
|
|
577
|
|
|
$builder = $this->storage()->query($this->target->table()) |
578
|
|
|
->filters($this->filter()) |
579
|
|
|
->orders($this->order()); |
580
|
|
|
|
581
|
|
|
$builder->callback(function ($result) use ($class) { |
582
|
|
|
return $class::hydrate($result->data); |
583
|
|
|
}); |
584
|
|
|
|
585
|
|
|
return $builder; |
586
|
|
|
} |
587
|
|
|
|
588
|
|
|
/** |
589
|
|
|
* Read, generate and set cached related models from storage. |
590
|
|
|
* |
591
|
|
|
* This will completely replace any cached related models. |
592
|
|
|
* |
593
|
|
|
* @param int $limit [optional] |
594
|
|
|
* @return Record[] |
595
|
|
|
*/ |
596
|
|
|
public function load($limit = 0) |
597
|
|
|
{ |
598
|
|
|
$data = $this->read($limit); |
599
|
|
|
$class = get_class($this->target); |
600
|
|
|
$this->related = $class::generate($data); |
601
|
|
|
$this->loaded = true; |
602
|
|
|
|
603
|
|
|
return $this->related; |
604
|
|
|
} |
605
|
|
|
|
606
|
|
|
/** |
607
|
|
|
* Determine whether cached related models have been loaded from storage. |
608
|
|
|
* |
609
|
|
|
* @return bool |
610
|
|
|
*/ |
611
|
|
|
public function loaded() |
612
|
|
|
{ |
613
|
|
|
return $this->loaded; |
614
|
|
|
} |
615
|
|
|
|
616
|
|
|
/** |
617
|
|
|
* Eagerly load the related models for the given parent instances. |
618
|
|
|
* |
619
|
|
|
* Returns the given instances with their related models loaded. |
620
|
|
|
* |
621
|
|
|
* @param array $instances |
622
|
|
|
* @return array |
623
|
|
|
*/ |
624
|
|
|
abstract public function eager(array $instances); |
625
|
|
|
|
626
|
|
|
/** |
627
|
|
|
* Retrieve one or many related model instances, depending on the relation. |
628
|
|
|
* |
629
|
|
|
* @return Record[]|Record|null |
630
|
|
|
*/ |
631
|
|
|
abstract public function retrieve(); |
632
|
|
|
|
633
|
|
|
/** |
634
|
|
|
* Retrieve one related model instance. |
635
|
|
|
* |
636
|
|
|
* @return Record|null |
637
|
|
|
*/ |
638
|
|
|
public function one() |
639
|
|
|
{ |
640
|
|
|
if (!$this->loaded() && empty($this->related)) { |
641
|
|
|
$this->load(1); |
642
|
|
|
} |
643
|
|
|
|
644
|
|
|
// TODO: Load and merge with cached? |
645
|
|
|
|
646
|
|
|
return !empty($this->related) ? $this->related[0] : null; |
647
|
|
|
} |
648
|
|
|
|
649
|
|
|
/** |
650
|
|
|
* Retrieve all related model instances. |
651
|
|
|
* |
652
|
|
|
* @return Record[]|null |
653
|
|
|
*/ |
654
|
|
|
public function all() |
655
|
|
|
{ |
656
|
|
|
if (!$this->loaded() && empty($this->related)) { |
657
|
|
|
$this->load(); |
658
|
|
|
} |
659
|
|
|
|
660
|
|
|
// TODO: Load and merge with cached? |
661
|
|
|
|
662
|
|
|
return $this->related; |
663
|
|
|
} |
664
|
|
|
|
665
|
|
|
/** |
666
|
|
|
* Count the number of related model instances. |
667
|
|
|
* |
668
|
|
|
* Counts loaded or attached instances if they are present, queries storage |
669
|
|
|
* otherwise. |
670
|
|
|
* |
671
|
|
|
* @return int |
672
|
|
|
*/ |
673
|
|
|
public function count() |
674
|
|
|
{ |
675
|
|
|
if (!$this->loaded() && empty($this->related)) { |
676
|
|
|
return $this->storage()->count($this->target->table(), $this->filter()); |
677
|
|
|
} |
678
|
|
|
|
679
|
|
|
return count($this->related); |
680
|
|
|
} |
681
|
|
|
|
682
|
|
|
/** |
683
|
|
|
* Set the related models. |
684
|
|
|
* |
685
|
|
|
* Overwrites any currently set related models. |
686
|
|
|
* |
687
|
|
|
* @param Record[] $instances |
688
|
|
|
*/ |
689
|
|
|
public function set($instances) |
690
|
|
|
{ |
691
|
|
|
$this->verify($instances); |
692
|
|
|
$this->related = static::arrayify($instances); |
693
|
|
|
$this->loaded = true; |
694
|
|
|
} |
695
|
|
|
|
696
|
|
|
/** |
697
|
|
|
* Clear the related models. |
698
|
|
|
*/ |
699
|
|
|
public function clear() |
700
|
|
|
{ |
701
|
|
|
$this->related = array(); |
702
|
|
|
$this->loaded = false; |
703
|
|
|
} |
704
|
|
|
|
705
|
|
|
/** |
706
|
|
|
* Attach the given models. |
707
|
|
|
* |
708
|
|
|
* @param Record[]|Record $instances |
709
|
|
|
*/ |
710
|
|
|
public function attach($instances) |
711
|
|
|
{ |
712
|
|
|
$this->verify($instances); |
713
|
|
|
|
714
|
|
|
foreach (static::arrayify($instances) as $instance) { |
715
|
|
|
$this->replace($instance); |
716
|
|
|
} |
717
|
|
|
} |
718
|
|
|
|
719
|
|
|
/** |
720
|
|
|
* Detach the given models. |
721
|
|
|
* |
722
|
|
|
* Detaches all attached models if none are given. |
723
|
|
|
* |
724
|
|
|
* @param Record[]|Record $instances [optional] |
725
|
|
|
*/ |
726
|
|
|
public function detach($instances = array()) |
727
|
|
|
{ |
728
|
|
|
$this->verify($instances); |
729
|
|
|
|
730
|
|
|
$instances = static::arrayify($instances) ?: $this->related; |
731
|
|
|
|
732
|
|
|
$relatedIds = static::attributeList($this->related, 'id'); |
733
|
|
|
$detached = array(); |
734
|
|
|
$ids = array(); |
735
|
|
|
|
736
|
|
|
// Collect the IDs and instances of the models to be detached |
737
|
|
|
foreach ($instances as $instance) { |
738
|
|
|
if (in_array($instance->id(), $relatedIds)) { |
739
|
|
|
$ids[] = $instance->id(); |
740
|
|
|
$detached[] = $instance; |
741
|
|
|
} |
742
|
|
|
} |
743
|
|
|
|
744
|
|
|
// Reduce related models to those that haven't been detached |
745
|
|
|
$this->reduce(array_diff($relatedIds, $ids)); |
746
|
|
|
|
747
|
|
|
// Merge the newly detached models in with the existing ones |
748
|
|
|
$this->detached = array_merge($this->detached, $detached); |
|
|
|
|
749
|
|
|
} |
750
|
|
|
|
751
|
|
|
/** |
752
|
|
|
* Associate the given models. |
753
|
|
|
* |
754
|
|
|
* Returns the number of models successfully associated. |
755
|
|
|
* |
756
|
|
|
* @param Record[]|Record $instances |
757
|
|
|
* @return int |
758
|
|
|
*/ |
759
|
|
|
abstract public function associate($instances); |
760
|
|
|
|
761
|
|
|
/** |
762
|
|
|
* Dissociate the given models. |
763
|
|
|
* |
764
|
|
|
* Returns the number of models successfully dissociated. |
765
|
|
|
* |
766
|
|
|
* @param Record[]|Record $instances [optional] |
767
|
|
|
* @return int |
768
|
|
|
*/ |
769
|
|
|
abstract public function dissociate($instances = array()); |
770
|
|
|
|
771
|
|
|
/** |
772
|
|
|
* Save the relationship. |
773
|
|
|
* |
774
|
|
|
* Associates related models and dissociates detached models. |
775
|
|
|
* |
776
|
|
|
* Optionally accepts a set of IDs to save by. Saves all related models |
777
|
|
|
* otherwise. |
778
|
|
|
* |
779
|
|
|
* Returns the number of associated models. |
780
|
|
|
* |
781
|
|
|
* @param int[] $ids |
782
|
|
|
* @return int |
783
|
|
|
*/ |
784
|
|
|
public function save(array $ids = array()) |
785
|
|
|
{ |
786
|
|
|
$related = $this->related; |
787
|
|
|
$detached = $this->detached; |
788
|
|
|
|
789
|
|
|
// Filter the IDs to associate and dissociate if any have been given |
790
|
|
|
if (!empty($ids)) { |
791
|
|
|
$filter = function ($instance) use ($ids) { |
792
|
|
|
return in_array($instance->id(), $ids); |
793
|
|
|
}; |
794
|
|
|
|
795
|
|
|
$related = array_filter($related, $filter); |
796
|
|
|
$detached = array_filter($detached, $filter); |
797
|
|
|
} |
798
|
|
|
|
799
|
|
|
// Bail if we have nothing to associate or dissociate |
800
|
|
|
if (empty($related) && empty($detached)) { |
801
|
|
|
return 0; |
802
|
|
|
} |
803
|
|
|
|
804
|
|
|
// Dissociate, then associate |
805
|
|
|
if (!empty($detached)) { |
806
|
|
|
$this->dissociate($detached); |
807
|
|
|
} |
808
|
|
|
|
809
|
|
|
$associated = $this->associate($related); |
810
|
|
|
|
811
|
|
|
// Update detached models to be persisted |
812
|
|
|
$this->detached = array(); |
813
|
|
|
|
814
|
|
|
// Persist relationships on all related models |
815
|
|
|
foreach ($related as $instance) { |
816
|
|
|
$instance->saveRelations(); |
817
|
|
|
} |
818
|
|
|
|
819
|
|
|
return $associated; |
820
|
|
|
} |
821
|
|
|
|
822
|
|
|
/** |
823
|
|
|
* Dynamic read-only access for relation properties. |
824
|
|
|
* |
825
|
|
|
* @param string $property |
826
|
|
|
* @return mixed |
827
|
|
|
*/ |
828
|
|
|
public function __get($property) |
829
|
|
|
{ |
830
|
|
|
if (property_exists($this, $property)) { |
831
|
|
|
return $this->$property; |
832
|
|
|
} |
833
|
|
|
} |
834
|
|
|
} |
835
|
|
|
|
Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.
For example, imagine you have a variable
$accountId
that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to theid
property of an instance of theAccount
class. This class holds a proper account, so the id value must no longer be false.Either this assignment is in error or a type check should be added for that assignment.