1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* |
4
|
|
|
* This file is part of the Aura project for PHP. |
5
|
|
|
* |
6
|
|
|
* @package Aura.Marshal |
7
|
|
|
* |
8
|
|
|
* @license https://opensource.org/licenses/mit-license.php MIT |
9
|
|
|
* |
10
|
|
|
*/ |
11
|
|
|
namespace Aura\Marshal\Type; |
12
|
|
|
|
13
|
|
|
use Aura\Marshal\Collection\BuilderInterface as CollectionBuilderInterface; |
14
|
|
|
use Aura\Marshal\Collection\GenericCollection; |
15
|
|
|
use Aura\Marshal\Data; |
16
|
|
|
use Aura\Marshal\Exception; |
17
|
|
|
use Aura\Marshal\Lazy\BuilderInterface as LazyBuilderInterface; |
18
|
|
|
use Aura\Marshal\Entity\BuilderInterface as EntityBuilderInterface; |
19
|
|
|
use Aura\Marshal\Entity\GenericEntity; |
20
|
|
|
use Aura\Marshal\Relation\RelationInterface; |
21
|
|
|
use SplObjectStorage; |
22
|
|
|
|
23
|
|
|
/** |
24
|
|
|
* |
25
|
|
|
* Describes a particular type within the domain, and retains an IdentityMap |
26
|
|
|
* of entities for the type. Converts loaded data to entity objects lazily. |
27
|
|
|
* |
28
|
|
|
* @package Aura.Marshal |
29
|
|
|
* |
30
|
|
|
*/ |
31
|
|
|
class GenericType extends Data |
32
|
|
|
{ |
33
|
|
|
/** |
34
|
|
|
* |
35
|
|
|
* A builder to create collection objects for this type. |
36
|
|
|
* |
37
|
|
|
* @var CollectionBuilderInterface |
38
|
|
|
* |
39
|
|
|
*/ |
40
|
|
|
protected $collection_builder; |
41
|
|
|
|
42
|
|
|
/** |
43
|
|
|
* |
44
|
|
|
* A builder to create entity objects for this type. |
45
|
|
|
* |
46
|
|
|
* @var EntityBuilderInterface |
47
|
|
|
* |
48
|
|
|
*/ |
49
|
|
|
protected $entity_builder; |
50
|
|
|
|
51
|
|
|
/** |
52
|
|
|
* |
53
|
|
|
* The entity field representing its unique identifier value. The |
54
|
|
|
* IdentityMap will be keyed on these values. |
55
|
|
|
* |
56
|
|
|
* @var string |
57
|
|
|
* |
58
|
|
|
*/ |
59
|
|
|
protected $identity_field; |
60
|
|
|
|
61
|
|
|
/** |
62
|
|
|
* |
63
|
|
|
* An array of fields to index on for quicker lookups. The array format |
64
|
|
|
* is: |
65
|
|
|
* |
66
|
|
|
* $index_fields[$field_name][$field_value] = (array) $offsets; |
67
|
|
|
* |
68
|
|
|
* Note that we always have an array of offsets, and the keys are by |
69
|
|
|
* the field name and the values for that field. |
70
|
|
|
* |
71
|
|
|
* @var array<string, array<string, int[]>> |
72
|
|
|
* |
73
|
|
|
*/ |
74
|
|
|
protected $index_fields = []; |
75
|
|
|
|
76
|
|
|
/** |
77
|
|
|
* |
78
|
|
|
* An index of entities on the identity field. The format is: |
79
|
|
|
* |
80
|
|
|
* $index_identity[$identity_value] = $offset; |
81
|
|
|
* |
82
|
|
|
* Note that we always have only one offset, keyed by identity value. |
83
|
|
|
* |
84
|
|
|
* @var array<mixed, int> |
85
|
|
|
* |
86
|
|
|
*/ |
87
|
|
|
protected $index_identity = []; |
88
|
|
|
|
89
|
|
|
/** |
90
|
|
|
* |
91
|
|
|
* An index of all entities added via newEntity(). The format is: |
92
|
|
|
* |
93
|
|
|
* $index_new[] = $offset; |
94
|
|
|
* |
95
|
|
|
* Note that we always have one offset, and the key is merely sequential. |
96
|
|
|
* |
97
|
|
|
* @var int[] |
98
|
|
|
* |
99
|
|
|
*/ |
100
|
|
|
protected $index_new = []; |
101
|
|
|
|
102
|
|
|
/** |
103
|
|
|
* |
104
|
|
|
* An array of all entities removed via `removeEntity()`. |
105
|
|
|
* |
106
|
|
|
* @var mixed[] |
107
|
|
|
* |
108
|
|
|
*/ |
109
|
|
|
protected $removed = []; |
110
|
|
|
|
111
|
|
|
/** |
112
|
|
|
* |
113
|
|
|
* An object store of the initial data for entities in the IdentityMap. |
114
|
|
|
* |
115
|
|
|
* @var SplObjectStorage<GenericEntity, array<string, mixed>> |
116
|
|
|
* |
117
|
|
|
*/ |
118
|
|
|
protected $initial_data; |
119
|
|
|
|
120
|
|
|
/** |
121
|
|
|
* |
122
|
|
|
* A builder to create Lazy objects. |
123
|
|
|
* |
124
|
|
|
* @var LazyBuilderInterface |
125
|
|
|
* |
126
|
|
|
*/ |
127
|
|
|
protected $lazy_builder; |
128
|
|
|
|
129
|
|
|
/** |
130
|
|
|
* |
131
|
|
|
* An array of relationship descriptions, where the key is a |
132
|
|
|
* field name for the entity and the value is a relation object. |
133
|
|
|
* |
134
|
|
|
* @var array<string, RelationInterface> |
135
|
|
|
* |
136
|
|
|
*/ |
137
|
|
|
protected $relations = []; |
138
|
|
|
|
139
|
|
|
/** |
140
|
|
|
* |
141
|
|
|
* Constructor; overrides the parent entirely. |
142
|
|
|
* |
143
|
|
|
* @param array<int|string, mixed> $data The initial data for all entities in the type. |
144
|
|
|
* |
145
|
|
|
*/ |
146
|
|
|
public function __construct(array $data = []) |
147
|
|
|
{ |
148
|
|
|
$this->initial_data = new SplObjectStorage; |
149
|
|
|
$this->load($data); |
150
|
|
|
} |
151
|
|
|
|
152
|
|
|
/** |
153
|
|
|
* |
154
|
|
|
* Sets the name of the field that uniquely identifies each entity for |
155
|
|
|
* this type. |
156
|
|
|
* |
157
|
|
|
* @param string $identity_field The identity field name. |
158
|
|
|
* |
159
|
|
|
* @return void |
160
|
|
|
* |
161
|
|
|
*/ |
162
|
|
|
public function setIdentityField($identity_field) |
163
|
|
|
{ |
164
|
|
|
$this->identity_field = $identity_field; |
165
|
|
|
} |
166
|
|
|
|
167
|
|
|
/** |
168
|
|
|
* |
169
|
|
|
* Returns the name of the field that uniquely identifies each entity of |
170
|
|
|
* this type. |
171
|
|
|
* |
172
|
|
|
* @return string |
173
|
|
|
* |
174
|
|
|
*/ |
175
|
|
|
public function getIdentityField() |
176
|
|
|
{ |
177
|
|
|
return $this->identity_field; |
178
|
|
|
} |
179
|
|
|
|
180
|
|
|
/** |
181
|
|
|
* |
182
|
|
|
* Sets the fields that should be indexed at load() time; removes all |
183
|
|
|
* previous field indexes. |
184
|
|
|
* |
185
|
|
|
* @param string[] $fields The fields to be indexed. |
186
|
|
|
* |
187
|
|
|
* @return void |
188
|
|
|
* |
189
|
|
|
*/ |
190
|
|
|
public function setIndexFields(array $fields = []) |
191
|
|
|
{ |
192
|
|
|
$this->index_fields = []; |
193
|
|
|
foreach ($fields as $field) { |
194
|
|
|
$this->index_fields[$field] = []; |
195
|
|
|
} |
196
|
|
|
} |
197
|
|
|
|
198
|
|
|
/** |
199
|
|
|
* |
200
|
|
|
* Returns the list of indexed field names. |
201
|
|
|
* |
202
|
|
|
* @return string[] |
203
|
|
|
* |
204
|
|
|
*/ |
205
|
|
|
public function getIndexFields() |
206
|
|
|
{ |
207
|
|
|
return array_keys($this->index_fields); |
208
|
|
|
} |
209
|
|
|
|
210
|
|
|
/** |
211
|
|
|
* |
212
|
|
|
* Sets the builder object to create entity objects. |
213
|
|
|
* |
214
|
|
|
* @param EntityBuilderInterface $entity_builder The builder object. |
215
|
|
|
* |
216
|
|
|
* @return void |
217
|
|
|
* |
218
|
|
|
*/ |
219
|
|
|
public function setEntityBuilder(EntityBuilderInterface $entity_builder) |
220
|
|
|
{ |
221
|
|
|
$this->entity_builder = $entity_builder; |
222
|
|
|
} |
223
|
|
|
|
224
|
|
|
/** |
225
|
|
|
* |
226
|
|
|
* Returns the builder that creates entity objects. |
227
|
|
|
* |
228
|
|
|
* @return object |
229
|
|
|
* |
230
|
|
|
*/ |
231
|
|
|
public function getEntityBuilder() |
232
|
|
|
{ |
233
|
|
|
return $this->entity_builder; |
234
|
|
|
} |
235
|
|
|
|
236
|
|
|
/** |
237
|
|
|
* |
238
|
|
|
* Sets the builder object to create collection objects. |
239
|
|
|
* |
240
|
|
|
* @param CollectionBuilderInterface $collection_builder The builder object. |
241
|
|
|
* |
242
|
|
|
* @return void |
243
|
|
|
* |
244
|
|
|
*/ |
245
|
|
|
public function setCollectionBuilder(CollectionBuilderInterface $collection_builder) |
246
|
|
|
{ |
247
|
|
|
$this->collection_builder = $collection_builder; |
248
|
|
|
} |
249
|
|
|
|
250
|
|
|
/** |
251
|
|
|
* |
252
|
|
|
* Returns the builder that creates collection objects. |
253
|
|
|
* |
254
|
|
|
* @return CollectionBuilderInterface |
255
|
|
|
* |
256
|
|
|
*/ |
257
|
|
|
public function getCollectionBuilder() |
258
|
|
|
{ |
259
|
|
|
return $this->collection_builder; |
260
|
|
|
} |
261
|
|
|
|
262
|
|
|
/** |
263
|
|
|
* |
264
|
|
|
* Sets the lazy builder to create lazy objects. |
265
|
|
|
* |
266
|
|
|
* @param LazyBuilderInterface $lazy_builder The lazy builder. |
267
|
|
|
* |
268
|
|
|
* @return void |
269
|
|
|
* |
270
|
|
|
*/ |
271
|
|
|
public function setLazyBuilder(LazyBuilderInterface $lazy_builder) |
272
|
|
|
{ |
273
|
|
|
$this->lazy_builder = $lazy_builder; |
274
|
|
|
} |
275
|
|
|
|
276
|
|
|
/** |
277
|
|
|
* |
278
|
|
|
* Returns the lazy builder that creates lazy objects. |
279
|
|
|
* |
280
|
|
|
* @return LazyBuilderInterface |
281
|
|
|
* |
282
|
|
|
*/ |
283
|
|
|
public function getLazyBuilder() |
284
|
|
|
{ |
285
|
|
|
return $this->lazy_builder; |
286
|
|
|
} |
287
|
|
|
|
288
|
|
|
/** |
289
|
|
|
* |
290
|
|
|
* Loads the IdentityMap for this type with data for entity objects. |
291
|
|
|
* |
292
|
|
|
* Typically, the $data value is a sequential array of associative arrays. |
293
|
|
|
* As long as the $data value can be iterated over and accessed as an |
294
|
|
|
* array, you can pass in any kind of $data. |
295
|
|
|
* |
296
|
|
|
* The elements from $data will be placed into the IdentityMap and indexed |
297
|
|
|
* according to the value of their identity field. |
298
|
|
|
* |
299
|
|
|
* You can call load() multiple times, but entities already in the |
300
|
|
|
* IdentityMap will not be overwritten. |
301
|
|
|
* |
302
|
|
|
* The loaded elements are cast to objects; this allows consistent |
303
|
|
|
* addressing of elements before and after conversion to entity objects. |
304
|
|
|
* |
305
|
|
|
* The loaded elements will be converted to entity objects by the |
306
|
|
|
* entity builder only as you request them from the IdentityMap. |
307
|
|
|
* |
308
|
|
|
* @param array<int|string, mixed> $data Entity data to load into the IdentityMap. |
309
|
|
|
* |
310
|
|
|
* @param string $return_field Return values from this field; if empty, |
311
|
|
|
* return values from the identity field (the default). |
312
|
|
|
* |
313
|
|
|
* @return mixed[] The return values from the data elements, regardless |
314
|
|
|
* of whether they were loaded or not. |
315
|
|
|
* |
316
|
|
|
*/ |
317
|
|
|
public function load(array $data, $return_field = null) |
318
|
|
|
{ |
319
|
|
|
// what is the identity field for the type? |
320
|
|
|
$identity_field = $this->getIdentityField(); |
321
|
|
|
|
322
|
|
|
// what indexes do we need to track? |
323
|
|
|
$index_fields = array_keys($this->index_fields); |
324
|
|
|
|
325
|
|
|
// return a list of field values in $data |
326
|
|
|
$return_values = []; |
327
|
|
|
|
328
|
|
|
// what should the return field be? |
329
|
|
|
if (! $return_field) { |
330
|
|
|
$return_field = $identity_field; |
331
|
|
|
} |
332
|
|
|
|
333
|
|
|
// load each data element as a entity |
334
|
|
|
foreach ($data as $initial_data) { |
335
|
|
|
// cast the element to an object for consistent addressing |
336
|
|
|
$initial_data = $initial_data; |
337
|
|
|
// retain the return value on the entity |
338
|
|
|
$return_values[] = $initial_data[$return_field]; |
339
|
|
|
// load into the map |
340
|
|
|
$this->loadData($initial_data, $identity_field, $index_fields); |
341
|
|
|
} |
342
|
|
|
|
343
|
|
|
// return the list of field values in $data, and done |
344
|
|
|
return $return_values; |
345
|
|
|
} |
346
|
|
|
|
347
|
|
|
/** |
348
|
|
|
* |
349
|
|
|
* Loads a single entity into the identity map. |
350
|
|
|
* |
351
|
|
|
* @param array<string, mixed> $initial_data The initial data for the entity. |
352
|
|
|
* |
353
|
|
|
* @return object The newly-loaded entity. |
354
|
|
|
* |
355
|
|
|
*/ |
356
|
|
|
public function loadEntity(array $initial_data) |
357
|
|
|
{ |
358
|
|
|
// what is the identity field for the type? |
359
|
|
|
$identity_field = $this->getIdentityField(); |
360
|
|
|
|
361
|
|
|
// what indexes do we need to track? |
362
|
|
|
$index_fields = array_keys($this->index_fields); |
363
|
|
|
|
364
|
|
|
// load the data and get the offset |
365
|
|
|
$offset = $this->loadData( |
366
|
|
|
$initial_data, |
367
|
|
|
$identity_field, |
368
|
|
|
$index_fields |
369
|
|
|
); |
370
|
|
|
|
371
|
|
|
// return the entity at the offset |
372
|
|
|
return $this->offsetGet($offset); |
373
|
|
|
} |
374
|
|
|
|
375
|
|
|
/** |
376
|
|
|
* |
377
|
|
|
* Loads an entity collection into the identity map. |
378
|
|
|
* |
379
|
|
|
* @param array<array<string, mixed>> $data The initial data for the entities. |
380
|
|
|
* |
381
|
|
|
* @return object The newly-loaded collection. |
382
|
|
|
* |
383
|
|
|
*/ |
384
|
|
|
public function loadCollection(array $data) |
385
|
|
|
{ |
386
|
|
|
// what is the identity field for the type? |
387
|
|
|
$identity_field = $this->getIdentityField(); |
388
|
|
|
|
389
|
|
|
// what indexes do we need to track? |
390
|
|
|
$index_fields = array_keys($this->index_fields); |
391
|
|
|
|
392
|
|
|
// the entities for the collection |
393
|
|
|
$entities = []; |
394
|
|
|
|
395
|
|
|
// load each new entity |
396
|
|
|
foreach ($data as $initial_data) { |
397
|
|
|
$offset = $this->loadData( |
398
|
|
|
$initial_data, |
399
|
|
|
$identity_field, |
400
|
|
|
$index_fields |
401
|
|
|
); |
402
|
|
|
$entity = $this->offsetGet($offset); |
403
|
|
|
$entities[] =& $entity; |
404
|
|
|
} |
405
|
|
|
|
406
|
|
|
// return a collection of the loaded entities |
407
|
|
|
return $this->collection_builder->newInstance($entities); |
408
|
|
|
} |
409
|
|
|
|
410
|
|
|
/** |
411
|
|
|
* |
412
|
|
|
* Loads an entity into the identity map. |
413
|
|
|
* |
414
|
|
|
* @param array<string, mixed> $initial_data The initial data for the entity. |
415
|
|
|
* |
416
|
|
|
* @param string $identity_field The identity field for the entity. |
417
|
|
|
* |
418
|
|
|
* @param string[] $index_fields The fields to index on. |
419
|
|
|
* |
420
|
|
|
* @return int The identity map offset of the new entity. |
421
|
|
|
* |
422
|
|
|
*/ |
423
|
|
|
protected function loadData( |
424
|
|
|
array $initial_data, |
425
|
|
|
$identity_field, |
426
|
|
|
array $index_fields |
427
|
|
|
) { |
428
|
|
|
// does the identity already exist in the map? |
429
|
|
|
$identity_value = $initial_data[$identity_field]; |
430
|
|
|
if (isset($this->index_identity[$identity_value])) { |
431
|
|
|
// yes; we're done, return the offset number |
432
|
|
|
return $this->index_identity[$identity_value]; |
433
|
|
|
} |
434
|
|
|
|
435
|
|
|
// convert the initial data to a real entity in the identity map |
436
|
|
|
$this->data[] = $this->entity_builder->newInstance($initial_data); |
437
|
|
|
|
438
|
|
|
// get the entity and retain initial data |
439
|
|
|
$entity = end($this->data); |
440
|
|
|
$this->initial_data->attach($entity, $initial_data); |
441
|
|
|
|
442
|
|
|
// build indexes by offset |
443
|
|
|
$offset = key($this->data); |
444
|
|
|
$this->index_identity[$identity_value] = $offset; |
445
|
|
|
foreach ($index_fields as $field) { |
446
|
|
|
$value = $entity->$field; |
447
|
|
|
$this->index_fields[$field][$value][] = $offset; |
448
|
|
|
} |
449
|
|
|
|
450
|
|
|
// set related fields |
451
|
|
|
foreach ($this->getRelations() as $field => $relation) { |
452
|
|
|
$entity->$field = $this->lazy_builder->newInstance($relation); |
453
|
|
|
} |
454
|
|
|
|
455
|
|
|
// done! return the new offset number. |
456
|
|
|
return $offset; |
|
|
|
|
457
|
|
|
} |
458
|
|
|
|
459
|
|
|
/** |
460
|
|
|
* |
461
|
|
|
* Returns the array keys for the for the entities in the IdentityMap; |
462
|
|
|
* the keys were generated at load() time from the identity field values. |
463
|
|
|
* |
464
|
|
|
* @return mixed[] |
465
|
|
|
* |
466
|
|
|
*/ |
467
|
|
|
public function getIdentityValues() |
468
|
|
|
{ |
469
|
|
|
return array_keys($this->index_identity); |
470
|
|
|
} |
471
|
|
|
|
472
|
|
|
/** |
473
|
|
|
* |
474
|
|
|
* Returns the values for a particular field for all the entities in the |
475
|
|
|
* IdentityMap. |
476
|
|
|
* |
477
|
|
|
* @param string $field The field name to get values for. |
478
|
|
|
* |
479
|
|
|
* @return array<mixed, mixed> An array of key-value pairs where the key is the identity |
480
|
|
|
* value and the value is the requested field value. |
481
|
|
|
* |
482
|
|
|
*/ |
483
|
|
|
public function getFieldValues($field) |
484
|
|
|
{ |
485
|
|
|
$values = []; |
486
|
|
|
$identity_field = $this->getIdentityField(); |
487
|
|
|
foreach ($this->data as $offset => $entity) { |
488
|
|
|
$identity_value = $entity->$identity_field; |
489
|
|
|
$values[$identity_value] = $entity->$field; |
490
|
|
|
} |
491
|
|
|
return $values; |
492
|
|
|
} |
493
|
|
|
|
494
|
|
|
/** |
495
|
|
|
* |
496
|
|
|
* Retrieves a single entity from the IdentityMap by the value of its |
497
|
|
|
* identity field. |
498
|
|
|
* |
499
|
|
|
* @param int $identity_value The identity value of the entity to be |
500
|
|
|
* retrieved. |
501
|
|
|
* |
502
|
|
|
* @return ?object A entity object via the entity builder. |
503
|
|
|
* |
504
|
|
|
*/ |
505
|
|
|
public function getEntity($identity_value) |
506
|
|
|
{ |
507
|
|
|
// if the entity is not in the identity index, exit early |
508
|
|
|
if (! isset($this->index_identity[$identity_value])) { |
509
|
|
|
return null; |
510
|
|
|
} |
511
|
|
|
|
512
|
|
|
// look up the sequential offset for the identity value |
513
|
|
|
$offset = $this->index_identity[$identity_value]; |
514
|
|
|
return $this->offsetGet($offset); |
515
|
|
|
} |
516
|
|
|
|
517
|
|
|
/** |
518
|
|
|
* |
519
|
|
|
* Retrieves the first entity from the IdentityMap that matches the value |
520
|
|
|
* of an arbitrary field; it will be converted to a entity object |
521
|
|
|
* if it is not already an object of the proper class. |
522
|
|
|
* |
523
|
|
|
* N.b.: This will not be performant for large sets where the field is not |
524
|
|
|
* an identity field and is not indexed. |
525
|
|
|
* |
526
|
|
|
* @param string $field The field to match on. |
527
|
|
|
* |
528
|
|
|
* @param mixed $value The value of the field to match on. |
529
|
|
|
* |
530
|
|
|
* @return ?object A entity object via the entity builder. |
531
|
|
|
* |
532
|
|
|
*/ |
533
|
|
|
public function getEntityByField($field, $value) |
534
|
|
|
{ |
535
|
|
|
// pre-emptively look for an identity field |
536
|
|
|
if ($field == $this->identity_field) { |
537
|
|
|
return $this->getEntity($value); |
538
|
|
|
} |
539
|
|
|
|
540
|
|
|
// pre-emptively look for an indexed field for that value |
541
|
|
|
if (isset($this->index_fields[$field])) { |
542
|
|
|
return $this->getEntityByIndex($field, $value); |
543
|
|
|
} |
544
|
|
|
|
545
|
|
|
// long slow loop through all the entities to find a match. |
546
|
|
|
foreach ($this->data as $offset => $entity) { |
547
|
|
|
if ($entity->$field == $value) { |
548
|
|
|
return $this->offsetGet($offset); |
549
|
|
|
} |
550
|
|
|
} |
551
|
|
|
|
552
|
|
|
// no match! |
553
|
|
|
return null; |
554
|
|
|
} |
555
|
|
|
|
556
|
|
|
/** |
557
|
|
|
* |
558
|
|
|
* Retrieves the first entity from the IdentityMap matching an index |
559
|
|
|
* lookup. |
560
|
|
|
* |
561
|
|
|
* @param string $field The indexed field name. |
562
|
|
|
* |
563
|
|
|
* @param string $value The field value to match on. |
564
|
|
|
* |
565
|
|
|
* @return ?object A entity object via the entity builder. |
566
|
|
|
* |
567
|
|
|
*/ |
568
|
|
|
protected function getEntityByIndex($field, $value) |
569
|
|
|
{ |
570
|
|
|
if (! isset($this->index_fields[$field][$value])) { |
571
|
|
|
return null; |
572
|
|
|
} |
573
|
|
|
$offset = $this->index_fields[$field][$value][0]; |
574
|
|
|
return $this->offsetGet($offset); |
575
|
|
|
} |
576
|
|
|
|
577
|
|
|
/** |
578
|
|
|
* |
579
|
|
|
* Retrieves a collection of elements from the IdentityMap by the values |
580
|
|
|
* of their identity fields; each element will be converted to a entity |
581
|
|
|
* object if it is not already an object of the proper class. |
582
|
|
|
* |
583
|
|
|
* @param mixed[] $identity_values An array of identity values to retrieve. |
584
|
|
|
* |
585
|
|
|
* @return GenericCollection A collection object via the collection builder. |
586
|
|
|
* |
587
|
|
|
*/ |
588
|
|
|
public function getCollection(array $identity_values) |
589
|
|
|
{ |
590
|
|
|
$list = []; |
591
|
|
|
foreach ($identity_values as $identity_value) { |
592
|
|
|
// look up the offset for the identity value |
593
|
|
|
$offset = $this->index_identity[$identity_value]; |
594
|
|
|
// assigning by reference keeps the connections |
595
|
|
|
// when the element is converted to a entity |
596
|
|
|
$list[] =& $this->data[$offset]; |
597
|
|
|
} |
598
|
|
|
return $this->collection_builder->newInstance($list); |
599
|
|
|
} |
600
|
|
|
|
601
|
|
|
/** |
602
|
|
|
* |
603
|
|
|
* Retrieves a collection of objects from the IdentityMap matching the |
604
|
|
|
* value of an arbitrary field; these will be converted to entities |
605
|
|
|
* if they are not already objects of the proper class. |
606
|
|
|
* |
607
|
|
|
* The value to be matched can be an array of values, so that you |
608
|
|
|
* can get many values of the field being matched. |
609
|
|
|
* |
610
|
|
|
* If the field is indexed, the order of the returned collection |
611
|
|
|
* will match the order of the values being searched. If the field is not |
612
|
|
|
* indexed, the order of the returned collection will be the same as the |
613
|
|
|
* IdentityMap. |
614
|
|
|
* |
615
|
|
|
* The fastest results are from the identity field; second fastest, from |
616
|
|
|
* an indexed field; slowest are from non-indexed fields, because it has |
617
|
|
|
* to look through the entire IdentityMap to find matches. |
618
|
|
|
* |
619
|
|
|
* @param string $field The field to match on. |
620
|
|
|
* |
621
|
|
|
* @param mixed $values The value of the field to match on; if an array, |
622
|
|
|
* any value in the array will be counted as a match. |
623
|
|
|
* |
624
|
|
|
* @return GenericCollection A collection object via the collection builder. |
625
|
|
|
* |
626
|
|
|
*/ |
627
|
|
|
public function getCollectionByField($field, $values) |
628
|
|
|
{ |
629
|
|
|
$values = (array) $values; |
630
|
|
|
|
631
|
|
|
// pre-emptively look for an identity field |
632
|
|
|
if ($field == $this->identity_field) { |
633
|
|
|
return $this->getCollection($values); |
634
|
|
|
} |
635
|
|
|
|
636
|
|
|
// pre-emptively look for an indexed field |
637
|
|
|
if (isset($this->index_fields[$field])) { |
638
|
|
|
return $this->getCollectionByIndex($field, $values); |
639
|
|
|
} |
640
|
|
|
|
641
|
|
|
// long slow loop through all the entities to find a match |
642
|
|
|
$list = []; |
643
|
|
|
foreach ($this->data as $identity_value => $entity) { |
644
|
|
|
if (in_array($entity->$field, $values)) { |
645
|
|
|
// assigning by reference keeps the connections |
646
|
|
|
// when the original is converted to a entity |
647
|
|
|
$list[] =& $this->data[$identity_value]; |
648
|
|
|
} |
649
|
|
|
} |
650
|
|
|
return $this->collection_builder->newInstance($list); |
651
|
|
|
} |
652
|
|
|
|
653
|
|
|
/** |
654
|
|
|
* |
655
|
|
|
* Looks through the index for a field to retrieve a collection of |
656
|
|
|
* objects from the IdentityMap; these will be converted to entities |
657
|
|
|
* if they are not already objects of the proper class. |
658
|
|
|
* |
659
|
|
|
* N.b.: The value to be matched can be an array of values, so that you |
660
|
|
|
* can get many values of the field being matched. |
661
|
|
|
* |
662
|
|
|
* N.b.: The order of the returned collection will match the order of the |
663
|
|
|
* values being searched, not the order of the entities in the IdentityMap. |
664
|
|
|
* |
665
|
|
|
* @param string $field The field to match on. |
666
|
|
|
* |
667
|
|
|
* @param mixed $values The value of the field to match on; if an array, |
668
|
|
|
* any value in the array will be counted as a match. |
669
|
|
|
* |
670
|
|
|
* @return GenericCollection A collection object via the collection builder. |
671
|
|
|
* |
672
|
|
|
*/ |
673
|
|
|
protected function getCollectionByIndex($field, $values) |
674
|
|
|
{ |
675
|
|
|
$values = (array) $values; |
676
|
|
|
$list = []; |
677
|
|
|
foreach ($values as $value) { |
678
|
|
|
// is there an index for that field value? |
679
|
|
|
if (isset($this->index_fields[$field][$value])) { |
680
|
|
|
// assigning by reference keeps the connections |
681
|
|
|
// when the original is converted to a entity. |
682
|
|
|
foreach ($this->index_fields[$field][$value] as $offset) { |
683
|
|
|
$list[] =& $this->data[$offset]; |
684
|
|
|
} |
685
|
|
|
} |
686
|
|
|
} |
687
|
|
|
return $this->collection_builder->newInstance($list); |
688
|
|
|
} |
689
|
|
|
|
690
|
|
|
/** |
691
|
|
|
* |
692
|
|
|
* Sets a relationship to another type, assigning it to a field |
693
|
|
|
* name to be used in entity objects. |
694
|
|
|
* |
695
|
|
|
* @param string $name The field name to use for the related entity |
696
|
|
|
* or collection. |
697
|
|
|
* |
698
|
|
|
* @param RelationInterface $relation The relationship definition object. |
699
|
|
|
* |
700
|
|
|
* @return void |
701
|
|
|
* |
702
|
|
|
*/ |
703
|
|
|
public function setRelation($name, RelationInterface $relation) |
704
|
|
|
{ |
705
|
|
|
if (isset($this->relations[$name])) { |
706
|
|
|
throw new Exception("Relation '$name' already exists."); |
707
|
|
|
} |
708
|
|
|
$this->relations[$name] = $relation; |
709
|
|
|
} |
710
|
|
|
|
711
|
|
|
/** |
712
|
|
|
* |
713
|
|
|
* Returns a relationship definition object by name. |
714
|
|
|
* |
715
|
|
|
* @param string $name The field name to use for the related entity |
716
|
|
|
* or collection. |
717
|
|
|
* |
718
|
|
|
* @return RelationInterface |
719
|
|
|
* |
720
|
|
|
*/ |
721
|
|
|
public function getRelation($name) |
722
|
|
|
{ |
723
|
|
|
return $this->relations[$name]; |
724
|
|
|
} |
725
|
|
|
|
726
|
|
|
/** |
727
|
|
|
* |
728
|
|
|
* Returns the array of all relationship definition objects. |
729
|
|
|
* |
730
|
|
|
* @return array<string, \Aura\Marshal\Relation\RelationInterface> |
731
|
|
|
* |
732
|
|
|
*/ |
733
|
|
|
public function getRelations() |
734
|
|
|
{ |
735
|
|
|
return $this->relations; |
736
|
|
|
} |
737
|
|
|
|
738
|
|
|
/** |
739
|
|
|
* |
740
|
|
|
* Adds a new entity to the IdentityMap. |
741
|
|
|
* |
742
|
|
|
* This entity will not show up in any indexes, whether by field or |
743
|
|
|
* by primary key. You will see it only by iterating through the |
744
|
|
|
* IdentityMap. Typically this is used to add to a collection, or |
745
|
|
|
* to create a new entity from user input. |
746
|
|
|
* |
747
|
|
|
* @param array<int|string, mixed> $data Data for the new entity. |
748
|
|
|
* |
749
|
|
|
* @return GenericEntity |
750
|
|
|
* |
751
|
|
|
*/ |
752
|
|
|
public function newEntity(array $data = []) |
753
|
|
|
{ |
754
|
|
|
$entity = $this->entity_builder->newInstance($data); |
755
|
|
|
$this->index_new[] = count($this->data); |
756
|
|
|
$this->data[] = $entity; |
757
|
|
|
return $entity; |
758
|
|
|
} |
759
|
|
|
|
760
|
|
|
/** |
761
|
|
|
* |
762
|
|
|
* Removes an entity from the collection. |
763
|
|
|
* |
764
|
|
|
* @param int $identity_value The identity value of the entity to be |
765
|
|
|
* removed. |
766
|
|
|
* |
767
|
|
|
* @return bool True on success, false on failure. |
768
|
|
|
* |
769
|
|
|
*/ |
770
|
|
|
public function removeEntity($identity_value) |
771
|
|
|
{ |
772
|
|
|
// if the entity is not in the identity index, exit early |
773
|
|
|
if (! isset($this->index_identity[$identity_value])) { |
774
|
|
|
return false; |
775
|
|
|
} |
776
|
|
|
|
777
|
|
|
// look up the sequential offset for the identity value |
778
|
|
|
$offset = $this->index_identity[$identity_value]; |
779
|
|
|
|
780
|
|
|
// get the entity |
781
|
|
|
$entity = $this->offsetGet($offset); |
782
|
|
|
|
783
|
|
|
// add the entity to the removed array |
784
|
|
|
$this->removed[$identity_value] = $entity; |
785
|
|
|
|
786
|
|
|
// remove the entity from the identity index |
787
|
|
|
unset($this->index_identity[$identity_value]); |
788
|
|
|
|
789
|
|
|
// get the index fields |
790
|
|
|
$index_fields = array_keys($this->index_fields); |
791
|
|
|
|
792
|
|
|
// loop through indices and remove offsets of this entity |
793
|
|
|
foreach ($index_fields as $field) { |
794
|
|
|
|
795
|
|
|
// get the field value |
796
|
|
|
$value = $entity->$field; |
797
|
|
|
|
798
|
|
|
/** |
799
|
|
|
* Find index of the offset with that value |
800
|
|
|
* |
801
|
|
|
* @var int|false $offset_idx |
802
|
|
|
*/ |
803
|
|
|
$offset_idx = array_search( |
804
|
|
|
$offset, |
805
|
|
|
$this->index_fields[$field][$value] |
806
|
|
|
); |
807
|
|
|
|
808
|
|
|
// if the index exists, remove it, preserving index integrity |
809
|
|
|
if ($offset_idx !== false) { |
810
|
|
|
array_splice( |
811
|
|
|
$this->index_fields[$field][$value], |
812
|
|
|
$offset_idx, |
813
|
|
|
1 |
814
|
|
|
); |
815
|
|
|
} |
816
|
|
|
} |
817
|
|
|
|
818
|
|
|
// really remove the entity, and done |
819
|
|
|
$this->offsetUnset($offset); |
820
|
|
|
return true; |
821
|
|
|
} |
822
|
|
|
|
823
|
|
|
/** |
824
|
|
|
* |
825
|
|
|
* Returns an array of all entities in the IdentityMap that have been |
826
|
|
|
* modified. |
827
|
|
|
* |
828
|
|
|
* @return array<mixed, GenericEntity|mixed> |
829
|
|
|
* |
830
|
|
|
*/ |
831
|
|
|
public function getChangedEntities() |
832
|
|
|
{ |
833
|
|
|
$list = []; |
834
|
|
|
foreach ($this->index_identity as $identity_value => $offset) { |
835
|
|
|
$entity = $this->data[$offset]; |
836
|
|
|
if ($this->getChangedFields($entity)) { |
837
|
|
|
$list[$identity_value] = $entity; |
838
|
|
|
} |
839
|
|
|
} |
840
|
|
|
return $list; |
841
|
|
|
} |
842
|
|
|
|
843
|
|
|
/** |
844
|
|
|
* |
845
|
|
|
* Returns an array of all entities in the IdentityMap that were created |
846
|
|
|
* using `newEntity()`. |
847
|
|
|
* |
848
|
|
|
* @return array<GenericEntity|mixed> |
849
|
|
|
* |
850
|
|
|
*/ |
851
|
|
|
public function getNewEntities() |
852
|
|
|
{ |
853
|
|
|
$list = []; |
854
|
|
|
foreach ($this->index_new as $offset) { |
855
|
|
|
$list[] = $this->data[$offset]; |
856
|
|
|
} |
857
|
|
|
return $list; |
858
|
|
|
} |
859
|
|
|
|
860
|
|
|
/** |
861
|
|
|
* |
862
|
|
|
* Returns all non-removed entities in the type. |
863
|
|
|
* |
864
|
|
|
* @return array<int|string, \Aura\Marshal\GenericEntity|mixed> |
865
|
|
|
* |
866
|
|
|
*/ |
867
|
|
|
public function getAllEntities() |
868
|
|
|
{ |
869
|
|
|
return $this->data; |
870
|
|
|
} |
871
|
|
|
|
872
|
|
|
/** |
873
|
|
|
* |
874
|
|
|
* Returns an array of all entities that were removed using |
875
|
|
|
* `removeEntity()`. |
876
|
|
|
* |
877
|
|
|
* @return mixed[] |
878
|
|
|
* |
879
|
|
|
*/ |
880
|
|
|
public function getRemovedEntities() |
881
|
|
|
{ |
882
|
|
|
return $this->removed; |
883
|
|
|
} |
884
|
|
|
|
885
|
|
|
/** |
886
|
|
|
* |
887
|
|
|
* Returns the initial data for a given entity. |
888
|
|
|
* |
889
|
|
|
* @param GenericEntity $entity The entity to find initial data for. |
890
|
|
|
* |
891
|
|
|
* @return null|array<string, mixed> The initial data for the entity. |
892
|
|
|
* |
893
|
|
|
*/ |
894
|
|
|
public function getInitialData($entity) |
895
|
|
|
{ |
896
|
|
|
if ($this->initial_data->contains($entity)) { |
897
|
|
|
return $this->initial_data[$entity]; |
898
|
|
|
} |
899
|
|
|
|
900
|
|
|
return null; |
901
|
|
|
} |
902
|
|
|
|
903
|
|
|
/** |
904
|
|
|
* |
905
|
|
|
* Returns the changed fields and their values for an entity. |
906
|
|
|
* |
907
|
|
|
* @param GenericEntity $entity The entity to find changes for. |
908
|
|
|
* |
909
|
|
|
* @return array<string, mixed> An array of key-value pairs where the key is the field |
910
|
|
|
* name and the value is the changed value. |
911
|
|
|
* |
912
|
|
|
*/ |
913
|
|
|
public function getChangedFields($entity) |
914
|
|
|
{ |
915
|
|
|
// the eventual list of changed fields and values |
916
|
|
|
$changed = []; |
917
|
|
|
|
918
|
|
|
// initial data for this entity |
919
|
|
|
$initial_data = $this->getInitialData($entity) ?? []; |
920
|
|
|
|
921
|
|
|
// go through all the initial data values |
922
|
|
|
foreach ($initial_data as $field => $old) { |
923
|
|
|
|
924
|
|
|
// what is the new value on the entity? |
925
|
|
|
$new = $entity->$field; |
926
|
|
|
|
927
|
|
|
// are both old and new values numeric? |
928
|
|
|
$numeric = is_numeric($old) && is_numeric($new); |
929
|
|
|
|
930
|
|
|
// if both old and new are numeric, compare loosely. |
931
|
|
|
if ($numeric && $old != $new) { |
932
|
|
|
// loosely different, retain the new value |
933
|
|
|
$changed[$field] = $new; |
934
|
|
|
} |
935
|
|
|
|
936
|
|
|
// if one or the other is not numeric, compare strictly |
937
|
|
|
if (! $numeric && $old !== $new) { |
938
|
|
|
// strictly different, retain the new value |
939
|
|
|
$changed[$field] = $new; |
940
|
|
|
} |
941
|
|
|
} |
942
|
|
|
|
943
|
|
|
// done! |
944
|
|
|
return $changed; |
945
|
|
|
} |
946
|
|
|
|
947
|
|
|
/** |
948
|
|
|
* |
949
|
|
|
* Unsets all entities from this type. |
950
|
|
|
* |
951
|
|
|
* @return void |
952
|
|
|
* |
953
|
|
|
*/ |
954
|
|
|
public function clear() |
955
|
|
|
{ |
956
|
|
|
$this->data = []; |
957
|
|
|
$this->index_identity = []; |
958
|
|
|
$this->index_new = []; |
959
|
|
|
$this->removed = []; |
960
|
|
|
$this->initial_data = new SplObjectStorage; |
961
|
|
|
} |
962
|
|
|
} |
963
|
|
|
|