GenericType   F
last analyzed

Complexity

Total Complexity 71

Size/Duplication

Total Lines 930
Duplicated Lines 0 %

Importance

Changes 4
Bugs 0 Features 1
Metric Value
wmc 71
eloc 169
c 4
b 0
f 1
dl 0
loc 930
rs 2.7199

35 Methods

Rating   Name   Duplication   Size   Complexity  
A setIndexFields() 0 5 2
A setEntityBuilder() 0 3 1
A getIndexFields() 0 3 1
A setIdentityField() 0 3 1
A newEntity() 0 6 1
A getRelation() 0 3 1
A setRelation() 0 6 2
A loadCollection() 0 24 2
A getCollection() 0 11 2
A getCollectionBuilder() 0 3 1
A setLazyBuilder() 0 3 1
A getAllEntities() 0 3 1
A getRelations() 0 3 1
A getEntityBuilder() 0 3 1
A getCollectionByField() 0 24 5
A getFieldValues() 0 9 2
A getCollectionByIndex() 0 15 4
A getNewEntities() 0 7 2
A getEntity() 0 10 2
B getChangedFields() 0 32 7
A getIdentityField() 0 3 1
A getEntityByField() 0 21 5
A getLazyBuilder() 0 3 1
A getInitialData() 0 7 2
A loadEntity() 0 17 1
A setCollectionBuilder() 0 3 1
A getRemovedEntities() 0 3 1
A __construct() 0 4 1
A load() 0 28 3
A getEntityByIndex() 0 7 2
A getChangedEntities() 0 10 3
A getIdentityValues() 0 3 1
A loadData() 0 34 4
A removeEntity() 0 51 4
A clear() 0 7 1

How to fix   Complexity   

Complex Class

Complex classes like GenericType often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use GenericType, and based on these observations, apply Extract Interface, too.

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;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $offset also could return the type string which is incompatible with the documented return type integer.
Loading history...
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