Aggregate::parseForCommonValues()   B
last analyzed

Complexity

Conditions 3
Paths 3

Size

Total Lines 26
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 1 Features 0
Metric Value
c 2
b 1
f 0
dl 0
loc 26
rs 8.8571
cc 3
eloc 10
nc 3
nop 1
1
<?php
2
3
namespace Analogue\ORM\System;
4
5
use Analogue\ORM\Relationships\Pivot;
6
use Illuminate\Support\Collection;
7
use Analogue\ORM\System\Wrappers\Factory;
8
use Analogue\ORM\System\Proxies\EntityProxy;
9
use Analogue\ORM\System\Proxies\CollectionProxy;
10
use Analogue\ORM\Exceptions\MappingException;
11
12
/**
13
 * This class is aimed to facilitate the handling of
14
 * complex root aggregate scenarios.
15
 */
16
class Aggregate implements InternallyMappable
17
{
18
    /**
19
     * The Root Entity
20
     *
21
     * @var \Analogue\ORM\System\Wrappers\Wrapper
22
     */
23
    protected $wrappedEntity;
24
25
    /**
26
     * Parent Root Aggregate
27
     *
28
     * @var \Analogue\ORM\System\Aggregate
29
     */
30
    protected $parent;
31
32
    /**
33
     * Parent's relationship method
34
     *
35
     * @var string
36
     */
37
    protected $parentRelationship;
38
39
    /**
40
     * Root Entity
41
     *
42
     * @var \Analogue\ORM\System\Aggregate
43
     */
44
    protected $root;
45
46
    /**
47
     * An associative array containing entity's
48
     * relationships converted to Aggregates
49
     *
50
     * @var array
51
     */
52
    protected $relationships = [];
53
54
    /**
55
     * Relationship that need post-command synchronization
56
     *
57
     * @var array
58
     */
59
    protected $needSync = [];
60
61
    /**
62
     * Mapper
63
     *
64
     * @var \Analogue\ORM\System\Mapper;
65
     */
66
    protected $mapper;
67
68
    /**
69
     * Entity Map
70
     *
71
     * @var \Analogue\ORM\EntityMap;
72
     */
73
    protected $entityMap;
74
75
    /**
76
     * Create a new Aggregated Entity instance
77
     *
78
     * @param mixed          $entity
79
     * @param Aggregate|null $parent
80
     * @param string         $parentRelationship
81
     * @param Aggregate|null $root
82
     * @throws MappingException
83
     */
84
    public function __construct($entity, Aggregate $parent = null, $parentRelationship = null, Aggregate $root = null)
85
    {
86
        $factory = new Factory;
87
        
88
        $this->wrappedEntity = $factory->make($entity);
89
90
        $this->parent = $parent;
91
92
        $this->parentRelationship = $parentRelationship;
93
94
        $this->root = $root;
95
96
        $this->mapper = Manager::getMapper($entity);
97
98
        $this->entityMap = $this->mapper->getEntityMap();
99
             
100
        $this->parseRelationships();
101
    }
102
103
    /**
104
     * Parse Every relationships defined on the entity
105
     *
106
     * @throws MappingException
107
     * @return void
108
     */
109
    protected function parseRelationships()
110
    {
111
        foreach ($this->entityMap->getSingleRelationships() as $relation) {
112
            $this->parseSingleRelationship($relation);
113
        }
114
115
        foreach ($this->entityMap->getManyRelationships() as $relation) {
116
            $this->parseManyRelationship($relation);
117
        }
118
    }
119
120
    /**
121
     * Parse for values common to single & many relations
122
     *
123
     * @param  string $relation
124
     * @throws MappingException
125
     * @return mixed|boolean
126
     */
127
    protected function parseForCommonValues($relation)
128
    {
129
        if (!$this->hasAttribute($relation)) {
130
            // If no attribute exists for this relationships
131
            // we'll make it a simple empty array. This will
132
            // save us from constantly checking for the attributes
133
            // actual existence.
134
            $this->relationships[$relation] = [];
135
            return false;
136
        }
137
138
        $value = $this->getRelationshipValue($relation);
139
140
        if (is_null($value)) {
141
            $this->relationships[$relation] = [];
142
143
            // If the relationship's content is the null value
144
            // and the Entity's exist in DB, we'll interpret this
145
            // as the need to detach all related Entities,
146
            // therefore a sync operation is needed.
147
            $this->needSync[] = $relation;
148
            return false;
149
        }
150
151
        return $value;
152
    }
153
154
    /**
155
     * Parse a 'single' relationship
156
     *
157
     * @param  string $relation
158
     * @throws MappingException
159
     * @return boolean
160
     */
161
    protected function parseSingleRelationship($relation)
162
    {
163
        if (!$value = $this->parseForCommonValues($relation)) {
164
            return true;
165
        }
166
        
167
        if ($value instanceof Collection || is_array($value) || $value instanceof CollectionProxy) {
168
            throw new MappingException("Entity's attribute $relation should not be array, or collection");
169
        }
170
171
        if ($value instanceof EntityProxy && !$value->isLoaded()) {
172
            $this->relationships[$relation] = [];
173
            return true;
174
        }
175
176
        // If the attribute is a loaded proxy, swap it for its
177
        // loaded entity.
178
        if ($value instanceof EntityProxy && $value->isLoaded()) {
179
            $value = $value->getUnderlyingObject();
180
        }
181
182
        if ($this->isParentOrRoot($value)) {
183
            $this->relationships[$relation] = [];
184
            return true;
185
        }
186
187
        // At this point, we can assume the attribute is an Entity instance
188
        // so we'll treat it as such.
189
        $subAggregate = $this->createSubAggregate($value, $relation);
190
         
191
        // Even if it's a single entity, we'll store it as an array
192
        // just for consistency with other relationships
193
        $this->relationships[$relation] = [$subAggregate];
194
195
        // We always need to check a loaded relation is in sync
196
        // with its local key
197
        $this->needSync[] = $relation;
198
199
        return true;
200
    }
201
202
    /**
203
     * Check if value isn't parent or root in the aggregate
204
     *
205
     * @param  mixed
206
     * @return boolean|null
207
     */
208
    protected function isParentOrRoot($value)
209
    {
210 View Code Duplication
        if (!is_null($this->root)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
211
            $rootClass = get_class($this->root->getEntityObject());
212
            if ($rootClass == get_class($value)) {
213
                return true;
214
            }
215
        }
216
217 View Code Duplication
        if (!is_null($this->parent)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
218
            $parentClass = get_class($this->parent->getEntityObject());
219
            if ($parentClass == get_class($value)) {
220
                return true;
221
            }
222
        }
223
    }
224
225
    /**
226
     * Parse a 'many' relationship
227
     *
228
     * @param  string $relation
229
     * @throws MappingException
230
     * @return boolean
231
     */
232
    protected function parseManyRelationship($relation)
233
    {
234
        if (!$value = $this->parseForCommonValues($relation)) {
235
            return true;
236
        }
237
        
238
        if (is_array($value) || $value instanceof Collection) {
239
            $this->needSync[] = $relation;
240
        }
241
242
        // If the relation is a proxy, we test is the relation
243
        // has been lazy loaded, otherwise we'll just treat
244
        // the subset of newly added items.
245
        if ($value instanceof CollectionProxy && $value->isLoaded()) {
246
            $this->needSync[] = $relation;
247
            $value = $value->getUnderlyingCollection();
248
        }
249
250
        if ($value instanceof CollectionProxy && !$value->isLoaded()) {
251
            $value = $value->getAddedItems();
252
        }
253
254
        // At this point $value should be either an array or an instance
255
        // of a collection class.
256
        if (!is_array($value) && !$value instanceof Collection) {
257
            throw new MappingException("'$relation' attribute should be array() or Collection");
258
        }
259
260
        $this->relationships[$relation] = $this->createSubAggregates($value, $relation);
261
        
262
        return true;
263
    }
264
265
    /**
266
     * Return Entity's relationship attribute
267
     *
268
     * @param  string $relation
269
     * @throws MappingException
270
     * @return mixed
271
     */
272
    protected function getRelationshipValue($relation)
273
    {
274
        $value = $this->getEntityAttribute($relation);
275
        //if($relation == "role") tdd($this->wrappedEntity->getEntityAttributes());
0 ignored issues
show
Unused Code Comprehensibility introduced by
65% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
276
        if (is_bool($value) || is_float($value) || is_int($value) || is_string($value)) {
277
            throw new MappingException("Entity's attribute $relation should be array, object, collection or null");
278
        }
279
280
        return $value;
281
    }
282
283
    /**
284
     * Create a child, aggregated entity
285
     *
286
     * @param  mixed $entities
287
     * @param string $relation
288
     * @return array
289
     */
290
    protected function createSubAggregates($entities, $relation)
291
    {
292
        $aggregates = [];
293
294
        foreach ($entities as $entity) {
295
            $aggregates[] = $this->createSubAggregate($entity, $relation);
296
        }
297
298
        return $aggregates;
299
    }
300
301
    /**
302
     * Create a related subAggregate
303
     *
304
     * @param  mixed $entity
305
     * @param  string $relation
306
     * @throws MappingException
307
     * @return self
308
     */
309
    protected function createSubAggregate($entity, $relation)
310
    {
311
        // If root isn't defined, then this is the Aggregate Root
312
        if (is_null($this->root)) {
313
            $root = $this;
314
        } else {
315
            $root = $this->root;
316
        }
317
318
        return new self($entity, $this, $relation, $root);
319
    }
320
321
    /**
322
     * Get the Entity's primary key attribute
323
     *
324
     * @return string|integer
325
     */
326
    public function getEntityId()
327
    {
328
        return $this->wrappedEntity->getEntityAttribute($this->entityMap->getKeyName());
329
    }
330
331
    /**
332
     * Get the name of the primary key
333
     *
334
     * @return string
335
     */
336
    public function getEntityKey()
337
    {
338
        return $this->entityMap->getKeyName();
339
    }
340
341
    /**
342
     * Return the entity map for the current entity
343
     *
344
     * @return \Analogue\ORM\EntityMap
345
     */
346
    public function getEntityMap()
347
    {
348
        return $this->entityMap;
349
    }
350
351
    /**
352
     * Return the Entity's hash $class.$id
353
     *
354
     * @return string
355
     */
356
    public function getEntityHash()
357
    {
358
        return $this->getEntityClass() . '.' . $this->getEntityId();
359
    }
360
361
    /**
362
     * Get wrapped entity class
363
     *
364
     * @return string
365
     */
366
    public function getEntityClass()
367
    {
368
        return $this->entityMap->getClass();
369
    }
370
371
    /**
372
     * Return the Mapper's entity cache
373
     *
374
     * @return \Analogue\ORM\System\EntityCache
375
     */
376
    protected function getEntityCache()
377
    {
378
        return $this->mapper->getEntityCache();
379
    }
380
381
    /**
382
     * Get a relationship as an aggregated entities' array
383
     *
384
     * @param  string $name
385
     * @return array
386
     */
387
    public function getRelationship($name)
388
    {
389
        if (array_key_exists($name, $this->relationships)) {
390
            return $this->relationships[$name];
391
        } else {
392
            return [];
393
        }
394
    }
395
396
    /**
397
     * [TO IMPLEMENT]
398
     *
399
     * @return array
400
     */
401
    public function getPivotAttributes()
402
    {
403
        return [];
404
    }
405
406
    /**
407
     * Get Non existing related entities from several relationships
408
     *
409
     * @param  array $relationships
410
     * @return array
411
     */
412
    public function getNonExistingRelated(array $relationships)
413
    {
414
        $nonExisting = [];
415
416
        foreach ($relationships as $relation) {
417
            if ($this->hasAttribute($relation) && array_key_exists($relation, $this->relationships)) {
418
                $nonExisting = array_merge($nonExisting, $this->getNonExistingFromRelation($relation));
419
            }
420
        }
421
422
        return $nonExisting;
423
    }
424
425
    /**
426
     * Get non-existing related entities from a single relation
427
     *
428
     * @param  string $relation
429
     * @return array
430
     */
431
    protected function getNonExistingFromRelation($relation)
432
    {
433
        $nonExisting = [];
434
435
        foreach ($this->relationships[$relation] as $aggregate) {
436
            if (!$aggregate->exists()) {
437
                $nonExisting[] = $aggregate;
438
            }
439
        }
440
441
        return $nonExisting;
442
    }
443
444
    /**
445
     * Synchronize relationships if needed
446
     */
447
    public function syncRelationships(array $relationships)
448
    {
449
        if ($this->exists()) {
450
            foreach ($relationships as $relation) {
451
                if (in_array($relation, $this->needSync)) {
452
                    $this->synchronize($relation);
453
                }
454
            }
455
        }
456
    }
457
458
    /**
459
     * Synchronize a relationship attribute
460
     *
461
     * @param $relation
462
     */
463
    protected function synchronize($relation)
464
    {
465
        $actualContent = $this->relationships[$relation];
466
467
        $this->entityMap->$relation($this->getEntityObject())->sync($actualContent);
468
    }
469
470
    /**
471
     * Returns an array of Missing related Entities for the
472
     * given $relation
473
     *
474
     * @param  string $relation
475
     * @return array
476
     */
477
    public function getMissingEntities($relation)
478
    {
479
        $cachedRelations = $this->getCachedAttribute($relation);
480
481
        if (!is_null($cachedRelations)) {
482
            $missing = [];
483
484
            foreach ($cachedRelations as $hash) {
0 ignored issues
show
Bug introduced by
The expression $cachedRelations of type object|integer|double|string|array|boolean is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
485
                if (!$this->getRelatedAggregateFromHash($hash, $relation)) {
486
                    $missing[] = $hash;
487
                }
488
            }
489
490
            return $missing;
491
        } else {
492
            return [];
493
        }
494
    }
495
       
496
    /**
497
     * Get Relationships who have dirty attributes / dirty relationships
498
     *
499
     * @return array
500
     */
501
    public function getDirtyRelationships()
502
    {
503
        $dirtyAggregates = [];
504
505
        foreach ($this->relationships as $relation) {
506
            foreach ($relation as $aggregate) {
507
                if (!$aggregate->exists() || $aggregate->isDirty() || count($aggregate->getDirtyRelationships() > 0)) {
508
                    $dirtyAggregates[] = $aggregate;
509
                }
510
            }
511
        }
512
513
        return $dirtyAggregates;
514
    }
515
    
516
    /**
517
     * Compare the object's raw attributes with the record in cache
518
     *
519
     * @return boolean
520
     */
521
    public function isDirty()
522
    {
523
        if (count($this->getDirtyRawAttributes()) > 0) {
524
            return true;
525
        } else {
526
            return false;
527
        }
528
    }
529
530
    /**
531
     * Get Raw Entity's attributes, as they are represented
532
     * in the database, including value objects & foreign keys
533
     *
534
     * @return array
535
     */
536
    public function getRawAttributes()
537
    {
538
        $attributes = $this->wrappedEntity->getEntityAttributes();
539
540
        foreach ($this->entityMap->getRelationships() as $relation) {
541
            unset($attributes[$relation]);
542
        }
543
544
        $attributes = $this->flattenEmbeddables($attributes);
545
546
        $foreignKeys = $this->getForeignKeyAttributes();
547
548
        return $attributes + $foreignKeys;
549
    }
550
551
    /**
552
     * Convert Value Objects to raw db attributes
553
     *
554
     * @param  array $attributes
555
     * @return array
556
     */
557
    protected function flattenEmbeddables($attributes)
558
    {
559
        $embeddables = $this->entityMap->getEmbeddables();
560
        
561
        foreach ($embeddables as $localKey => $embed) {
562
            // Retrieve the value object from the entity's attributes
563
            $valueObject = $attributes[$localKey];
564
565
            // Unset the corresponding key
566
            unset($attributes[$localKey]);
567
568
            // TODO Make wrapper object compatible with value objects
569
            $valueObjectAttributes = $valueObject->getEntityAttributes();
570
571
            // Now (if setup in the entity map) we prefix the value object's
572
            // attributes with the snake_case name of the embedded class.
573
            $prefix = snake_case(class_basename($embed));
574
575
            foreach ($valueObjectAttributes as $key=>$value) {
576
                $valueObjectAttributes[$prefix . '_' . $key] = $value;
577
                unset($valueObjectAttributes[$key]);
578
            }
579
580
            $attributes = array_merge($attributes, $valueObjectAttributes);
581
        }
582
        
583
        return $attributes;
584
    }
585
586
    /**
587
     * Return's entity raw attributes in the state they were at last
588
     * query.
589
     *
590
     * @param  array|null $columns
591
     * @return array
592
     */
593 View Code Duplication
    protected function getCachedRawAttributes(array $columns = null)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
594
    {
595
        $cachedAttributes = $this->getCache()->get($this->getEntityId());
596
597
        if (is_null($columns)) {
598
            return $cachedAttributes;
599
        } else {
600
            return array_only($cachedAttributes, $columns);
601
        }
602
    }
603
604
    /**
605
     * Return a single attribute from the cache
606
     * @param  string $key
607
     * @return mixed
608
     */
609 View Code Duplication
    protected function getCachedAttribute($key)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
610
    {
611
        $cachedAttributes = $this->getCache()->get($this->getEntityId());
612
613
        if (!array_key_exists($key, $cachedAttributes)) {
614
            return null;
615
        } else {
616
            return $cachedAttributes[$key];
617
        }
618
    }
619
620
    /**
621
     * Convert related Entity's attributes to foreign keys
622
     *
623
     * @return array
624
     */
625
    protected function getForeignKeyAttributes()
626
    {
627
        $foreignKeys = [];
628
629
        foreach ($this->entityMap->getLocalRelationships() as $relation) {
630
            // check if relationship has been parsed, meaning it has an actual object
631
            // in the entity's attributes
632
            if ($this->isActualRelationships($relation)) {
633
                $foreignKeys = $foreignKeys + $this->getForeignKeyAttributesFromRelation($relation);
634
            }
635
        }
636
637
        if (!is_null($this->parent)) {
638
            $foreignKeys = $foreignKeys + $this->getForeignKeyAttributesFromParent();
639
        }
640
641
        return $foreignKeys;
642
    }
643
644
    /**
645
     * Return an associative array containing the key-value pair(s) from
646
     * the related entity.
647
     *
648
     * @param  string $relation
649
     * @return array
650
     */
651
    protected function getForeignKeyAttributesFromRelation($relation)
652
    {
653
        $localRelations = $this->entityMap->getLocalRelationships();
654
655
        if (in_array($relation, $localRelations)) {
656
            // Call Relationship's method
657
            $relationship = $this->entityMap->$relation($this->getEntityObject());
658
659
            $relatedAggregate = $this->relationships[$relation][0];
660
661
            return $relationship->getForeignKeyValuePair($relatedAggregate->getEntityObject());
662
        } else {
663
            return [];
664
        }
665
    }
666
667
    /**
668
     * Get foreign key attribute(s) from a parent entity in this
669
     * aggregate context
670
     *
671
     * @return array
672
     */
673
    protected function getForeignKeyAttributesFromParent()
674
    {
675
        $parentMap = $this->parent->getEntityMap();
676
677
        $parentForeignRelations = $parentMap->getForeignRelationships();
678
        $parentPivotRelations = $parentMap->getPivotRelationships();
679
680
        $parentRelation = $this->parentRelationship;
681
682
        if (in_array($parentRelation, $parentForeignRelations)
683
            && !in_array($parentRelation, $parentPivotRelations)
684
        ) {
685
            $parentObject = $this->parent->getEntityObject();
686
687
            // Call Relationship's method on parent map
688
            $relationship = $parentMap->$parentRelation($parentObject);
689
690
            return $relationship->getForeignKeyValuePair();
691
        } else {
692
            return [];
693
        }
694
    }
695
696
    /**
697
     * Update Pivot records on loaded relationships, by comparing the
698
     * values from the Entity Cache to the actual relationship inside
699
     * the aggregated entity.
700
     *
701
     * @return void
702
     */
703
    public function updatePivotRecords()
704
    {
705
        $pivots = $this->entityMap->getPivotRelationships();
706
707
        foreach ($pivots as $pivot) {
708
            if (array_key_exists($pivot, $this->relationships)) {
709
                $this->updatePivotRelation($pivot);
710
            }
711
        }
712
    }
713
714
    /**
715
     * Update Single pivot relationship
716
     *
717
     * @param  string $relation
718
     * @return void
719
     */
720
    protected function updatePivotRelation($relation)
721
    {
722
        $hashes = $this->getEntityHashesFromRelation($relation);
723
724
        $cachedAttributes = $this->getCachedRawAttributes();
725
726
        if (array_key_exists($relation, $cachedAttributes)) {
727
            // Compare the two array of hashes to find out existing
728
            // pivot records, and the ones to be created.
729
            $new = array_diff($hashes, array_keys($cachedAttributes[$relation]));
730
            $existing = array_intersect($hashes, array_keys($cachedAttributes[$relation]));
731
        } else {
732
            $existing = [];
733
            $new = $hashes;
734
        }
735
736
        if (count($new) > 0) {
737
            $pivots = $this->getRelatedAggregatesFromHashes($new, $relation);
738
739
            $this->entityMap->$relation($this->getEntityObject())->createPivots($pivots);
740
        }
741
742
        if (count($existing) > 0) {
743
            foreach ($existing as $pivotHash) {
744
                $this->updatePivotIfDirty($pivotHash, $relation);
745
            }
746
        }
747
    }
748
749
    /**
750
     * Compare existing pivot record in cache and update it
751
     * if the pivot attributes are dirty
752
     *
753
     * @param  string $pivotHash
754
     * @param  string $relation
755
     * @return void
756
     */
757
    protected function updatePivotIfDirty($pivotHash, $relation)
758
    {
759
        $aggregate = $this->getRelatedAggregateFromHash($pivotHash, $relation);
760
761
        if ($aggregate->hasAttribute('pivot')) {
762
            $pivot = $aggregate->getEntityAttribute('pivot')->getEntityAttributes();
763
764
            $cachedPivotAttributes = $this->getPivotAttributesFromCache($pivotHash, $relation);
765
766
            $actualPivotAttributes = array_only($pivot, array_keys($cachedPivotAttributes));
767
768
            $dirty = $this->getDirtyAttributes($actualPivotAttributes, $cachedPivotAttributes);
769
770
            if (count($dirty) > 0) {
771
                $id = $aggregate->getEntityId();
772
773
                $this->entityMap->$relation($this->getEntityObject())->updateExistingPivot($id, $dirty);
774
            }
775
        }
776
    }
777
778
    /**
779
     * Compare two attributes array and return dirty attributes
780
     *
781
     * @param  array $actual
782
     * @param  array $cached
783
     * @return array
784
     */
785
    protected function getDirtyAttributes(array $actual, array $cached)
786
    {
787
        $dirty = [];
788
789
        foreach ($actual as $key => $value) {
790
            if (!$this->originalIsNumericallyEquivalent($value, $cached[$key])) {
791
                $dirty[$key] = $actual[$key];
792
            }
793
        }
794
795
        return $dirty;
796
    }
797
798
    /**
799
     *
800
     * @param  string $pivotHash
801
     * @param  string $relation
802
     * @return array
803
     */
804
    protected function getPivotAttributesFromCache($pivotHash, $relation)
805
    {
806
        $cachedAttributes = $this->getCachedRawAttributes();
807
808
        $cachedRelations = $cachedAttributes[$relation];
809
810
        foreach ($cachedRelations as $cachedRelation) {
811
            if ($cachedRelation == $pivotHash) {
812
                return $cachedRelation->getPivotAttributes();
813
            }
814
        }
815
    }
816
817
    /**
818
     * Returns an array of related Aggregates from its entity hashes
819
     *
820
     * @param  array  $hashes
821
     * @param  string $relation
822
     * @return array
823
     */
824
    protected function getRelatedAggregatesFromHashes(array $hashes, $relation)
825
    {
826
        $related = [];
827
828
        foreach ($hashes as $hash) {
829
            $aggregate = $this->getRelatedAggregateFromHash($hash, $relation);
830
831
            if (!is_null($aggregate)) {
832
                $related[] = $aggregate;
833
            }
834
        }
835
836
        return $related;
837
    }
838
839
    /**
840
     * Get related aggregate from its hash
841
     *
842
     * @param  string $hash
843
     * @param  string $relation
844
     * @return \Analogue\ORM\System\Aggregate|null
845
     */
846
    protected function getRelatedAggregateFromHash($hash, $relation)
847
    {
848
        foreach ($this->relationships[$relation] as $aggregate) {
849
            if ($aggregate->getEntityHash() == $hash) {
850
                return $aggregate;
851
            }
852
        }
853
        return null;
854
    }
855
856
    /**
857
     * Return an array of Entity Hashes from a specific relation
858
     *
859
     * @param  string $relation
860
     * @return array
861
     */
862
    protected function getEntityHashesFromRelation($relation)
863
    {
864
        return array_map(function ($aggregate) {
865
            return $aggregate->getEntityHash();
866
        }, $this->relationships[$relation]);
867
    }
868
869
    /**
870
     * Check the existence of an actual relationship
871
     *
872
     * @param  string $relation
873
     * @return boolean
874
     */
875
    protected function isActualRelationships($relation)
876
    {
877
        return array_key_exists($relation, $this->relationships)
878
            && count($this->relationships[$relation]) > 0;
879
    }
880
881
    /**
882
     * Return cache instance for the current entity type
883
     *
884
     * @return \Analogue\ORM\System\EntityCache
885
     */
886
    protected function getCache()
887
    {
888
        return $this->mapper->getEntityCache();
889
    }
890
891
    /**
892
     * Get Only Raw Entiy's attributes which have been modified
893
     * since last query
894
     *
895
     * @return array
896
     */
897
    public function getDirtyRawAttributes()
898
    {
899
        $attributes = $this->getRawAttributes();
900
        $cachedAttributes = $this->getCachedRawAttributes(array_keys($attributes));
901
902
        $dirty = [];
903
904
        foreach ($attributes as $key => $value) {
905
            if ($this->isRelation($key) || $key == 'pivot') {
906
                continue;
907
            }
908
909
            if (!array_key_exists($key, $cachedAttributes) && !$value instanceof Pivot) {
910
                $dirty[$key] = $value;
911
            } elseif ($value !== $cachedAttributes[$key] &&
912
                !$this->originalIsNumericallyEquivalent($value, $cachedAttributes[$key])) {
913
                $dirty[$key] = $value;
914
            }
915
        }
916
917
        return $dirty;
918
    }
919
920
    /**
921
     * @param $key
922
     * @return bool
923
     */
924
    protected function isRelation($key)
925
    {
926
        return in_array($key, $this->entityMap->getRelationships());
927
    }
928
929
    /**
930
     * Determine if the new and old values for a given key are numerically equivalent.
931
     *
932
     * @param $current
933
     * @param $original
934
     * @return boolean
935
     */
936
    protected function originalIsNumericallyEquivalent($current, $original)
937
    {
938
        return is_numeric($current) && is_numeric($original) && strcmp((string) $current, (string) $original) === 0;
939
    }
940
941
    /**
942
     * Get the underlying entity object
943
     *
944
     * @return mixed
945
     */
946
    public function getEntityObject()
947
    {
948
        return $this->wrappedEntity->getObject();
949
    }
950
951
    /**
952
     * Return the Mapper instance for the current Entity Type
953
     *
954
     * @return \Analogue\ORM\System\Mapper
955
     */
956
    public function getMapper()
957
    {
958
        return $this->mapper;
959
    }
960
961
    /**
962
     * Check that the entity already exists in the database, by checking
963
     * if it has an EntityCache record
964
     *
965
     * @return boolean
966
     */
967
    public function exists()
968
    {
969
        return $this->getCache()->has($this->getEntityId());
970
    }
971
972
    /**
973
     * Set the object attribute raw values (hydration)
974
     *
975
     * @param array $attributes
976
     */
977
    public function setEntityAttributes(array $attributes)
978
    {
979
        $this->wrappedEntity->setEntityAttributes($attributes);
980
    }
981
982
    /**
983
     * Get the raw object's values.
984
     *
985
     * @return array
986
     */
987
    public function getEntityAttributes()
988
    {
989
        return $this->wrappedEntity->getEntityAttributes();
990
    }
991
992
    /**
993
     * Set the raw entity attributes
994
     * @param string $key
995
     * @param string $value
996
     */
997
    public function setEntityAttribute($key, $value)
998
    {
999
        $this->wrappedEntity->setEntityAttribute($key, $value);
1000
    }
1001
1002
    /**
1003
     * Return the entity's attribute
1004
     * @param  string $key
1005
     * @return mixed
1006
     */
1007
    public function getEntityAttribute($key)
1008
    {
1009
        return $this->wrappedEntity->getEntityAttribute($key);
1010
    }
1011
1012
    /**
1013
     * Does the attribute exists on the entity
1014
     *
1015
     * @param  string  $key
1016
     * @return boolean
1017
     */
1018
    public function hasAttribute($key)
1019
    {
1020
        return $this->wrappedEntity->hasAttribute($key);
1021
    }
1022
1023
    /**
1024
     * Set the lazyloading proxies on the wrapped entity
1025
     */
1026
    public function setProxies()
1027
    {
1028
        $this->wrappedEntity->setProxies();
1029
    }
1030
}
1031