Completed
Branch master (411345)
by Rémi
11:20
created

Aggregate::parseManyRelationship()   C

Complexity

Conditions 11
Paths 17

Size

Total Lines 32
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 11
eloc 13
nc 17
nop 1
dl 0
loc 32
rs 5.2653
c 0
b 0
f 0

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
use ProxyManager\Proxy\LazyLoadingInterface;
12
13
/**
14
 * This class is aimed to facilitate the handling of
15
 * complex root aggregate scenarios.
16
 */
17
class Aggregate implements InternallyMappable
18
{
19
    /**
20
     * The Root Entity
21
     *
22
     * @var \Analogue\ORM\System\Wrappers\Wrapper
23
     */
24
    protected $wrappedEntity;
25
26
    /** 
27
     * Class of the entity being aggregated
28
     * 
29
     * @var string
30
     */
31
    protected $class;
32
33
    /**
34
     * Parent Root Aggregate
35
     *
36
     * @var \Analogue\ORM\System\Aggregate
37
     */
38
    protected $parent;
39
40
    /**
41
     * Parent's relationship method
42
     *
43
     * @var string
44
     */
45
    protected $parentRelationship;
46
47
    /**
48
     * Root Entity
49
     *
50
     * @var \Analogue\ORM\System\Aggregate
51
     */
52
    protected $root;
53
54
    /**
55
     * An associative array containing entity's
56
     * relationships converted to Aggregates
57
     *
58
     * @var array
59
     */
60
    protected $relationships = [];
61
62
    /**
63
     * Relationship that need post-command synchronization
64
     *
65
     * @var array
66
     */
67
    protected $needSync = [];
68
69
    /**
70
     * Mapper
71
     *
72
     * @var \Analogue\ORM\System\Mapper;
73
     */
74
    protected $mapper;
75
76
    /**
77
     * Entity Map
78
     *
79
     * @var \Analogue\ORM\EntityMap;
80
     */
81
    protected $entityMap;
82
83
    /**
84
     * Create a new Aggregated Entity instance
85
     *
86
     * @param mixed          $entity
87
     * @param Aggregate|null $parent
88
     * @param string         $parentRelationship
89
     * @param Aggregate|null $root
90
     * @throws MappingException
91
     */
92
    public function __construct($entity, Aggregate $parent = null, $parentRelationship = null, Aggregate $root = null)
93
    {
94
        $factory = new Factory;
95
        
96
        $this->class = get_class($entity);
97
98
        $this->wrappedEntity = $factory->make($entity);
99
100
        $this->parent = $parent;
101
102
        $this->parentRelationship = $parentRelationship;
103
104
        $this->root = $root;
105
106
        $mapper = $this->getMapper($entity);
0 ignored issues
show
Unused Code introduced by
The call to Aggregate::getMapper() has too many arguments starting with $entity.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
107
108
        $this->entityMap = $mapper->getEntityMap();
109
             
110
        $this->parseRelationships();
111
    }
112
113
    /**
114
     * Parse Every relationships defined on the entity
115
     *
116
     * @throws MappingException
117
     * @return void
118
     */
119
    protected function parseRelationships()
120
    {
121
        foreach ($this->entityMap->getSingleRelationships() as $relation) {
122
            $this->parseSingleRelationship($relation);
123
        }
124
125
        foreach ($this->entityMap->getManyRelationships() as $relation) {
126
            $this->parseManyRelationship($relation);
127
        }
128
    }
129
130
    /**
131
     * Parse for values common to single & many relations
132
     *
133
     * @param  string $relation
134
     * @throws MappingException
135
     * @return mixed|boolean
136
     */
137
    protected function parseForCommonValues($relation)
138
    {
139
        if (!$this->hasAttribute($relation)) {
140
            // If no attribute exists for this relationships
141
            // we'll make it a simple empty array. This will
142
            // save us from constantly checking for the attributes
143
            // actual existence.
144
            $this->relationships[$relation] = [];
145
            return false;
146
        }
147
148
        $value = $this->getRelationshipValue($relation);
149
150
        if (is_null($value)) {
151
            $this->relationships[$relation] = [];
152
153
            // If the relationship's content is the null value
154
            // and the Entity's exist in DB, we'll interpret this
155
            // as the need to detach all related Entities,
156
            // therefore a sync operation is needed.
157
            $this->needSync[] = $relation;
158
            return false;
159
        }
160
161
        return $value;
162
    }
163
164
    /**
165
     * Parse a 'single' relationship
166
     *
167
     * @param  string $relation
168
     * @throws MappingException
169
     * @return boolean
170
     */
171
    protected function parseSingleRelationship($relation)
172
    {
173
        if (!$value = $this->parseForCommonValues($relation)) {
174
            return true;
175
        }
176
        
177
        if ($value instanceof Collection || is_array($value) || $value instanceof CollectionProxy) {
178
            throw new MappingException("Entity's attribute $relation should not be array, or collection");
179
        }
180
181
        if ($value instanceof LazyLoadingInterface && !$value->isProxyInitialized()) {
182
            $this->relationships[$relation] = [];
183
            return true;
184
        }
185
186
        // If the attribute is a loaded proxy, swap it for its
187
        // loaded entity.
188
        if ($value instanceof LazyLoadingInterface && $value->isProxyInitialized()) {
189
            $value = $value->getWrappedValueHolderValue();
0 ignored issues
show
Bug introduced by
The method getWrappedValueHolderValue() does not seem to exist on object<ProxyManager\Proxy\LazyLoadingInterface>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
190
        }
191
192
        if ($this->isParentOrRoot($value)) {
193
            $this->relationships[$relation] = [];
194
            return true;
195
        }
196
197
        // At this point, we can assume the attribute is an Entity instance
198
        // so we'll treat it as such.
199
        $subAggregate = $this->createSubAggregate($value, $relation);
200
         
201
        // Even if it's a single entity, we'll store it as an array
202
        // just for consistency with other relationships
203
        $this->relationships[$relation] = [$subAggregate];
204
205
        // We always need to check a loaded relation is in sync
206
        // with its local key
207
        $this->needSync[] = $relation;
208
209
        return true;
210
    }
211
212
    /**
213
     * Check if value isn't parent or root in the aggregate
214
     *
215
     * @param  mixed
216
     * @return boolean|null
217
     */
218
    protected function isParentOrRoot($value)
219
    {
220 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...
221
            $rootClass = get_class($this->root->getEntityObject());
222
            if ($rootClass == get_class($value)) {
223
                return true;
224
            }
225
        }
226
227 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...
228
            $parentClass = get_class($this->parent->getEntityObject());
229
            if ($parentClass == get_class($value)) {
230
                return true;
231
            }
232
        }
233
    }
234
235
    /**
236
     * Parse a 'many' relationship
237
     *
238
     * @param  string $relation
239
     * @throws MappingException
240
     * @return boolean
241
     */
242
    protected function parseManyRelationship($relation)
243
    {
244
        if (!$value = $this->parseForCommonValues($relation)) {
245
            return true;
246
        }
247
        
248
        if (is_array($value) || (! $value instanceof CollectionProxy && $value instanceof Collection)) {
249
            $this->needSync[] = $relation;
250
        }
251
252
        // If the relation is a proxy, we test is the relation
253
        // has been lazy loaded, otherwise we'll just treat
254
        // the subset of newly added items.
255
        if ($value instanceof CollectionProxy && $value->isProxyInitialized()) {
256
            $this->needSync[] = $relation;
257
            //$value = $value->getUnderlyingCollection();
0 ignored issues
show
Unused Code Comprehensibility introduced by
60% 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...
258
        }
259
260
        if ($value instanceof CollectionProxy && ! $value->isProxyInitialized()) {
261
            $value = $value->getAddedItems();
262
        }
263
264
        // At this point $value should be either an array or an instance
265
        // of a collection class.
266
        if (!is_array($value) && !$value instanceof Collection) {
267
            throw new MappingException("'$relation' attribute should be array() or Collection");
268
        }
269
270
        $this->relationships[$relation] = $this->createSubAggregates($value, $relation);
271
        
272
        return true;
273
    }
274
275
    /**
276
     * Return Entity's relationship attribute
277
     *
278
     * @param  string $relation
279
     * @throws MappingException
280
     * @return mixed
281
     */
282
    protected function getRelationshipValue($relation)
283
    {
284
        $value = $this->getEntityAttribute($relation);
285
        //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...
286
        if (is_bool($value) || is_float($value) || is_int($value) || is_string($value)) {
287
            throw new MappingException("Entity's attribute $relation should be array, object, collection or null");
288
        }
289
290
        return $value;
291
    }
292
293
    /**
294
     * Create a child, aggregated entity
295
     *
296
     * @param  mixed $entities
297
     * @param string $relation
298
     * @return array
299
     */
300
    protected function createSubAggregates($entities, $relation)
301
    {
302
        $aggregates = [];
303
304
        foreach ($entities as $entity) {
305
            $aggregates[] = $this->createSubAggregate($entity, $relation);
306
        }
307
308
        return $aggregates;
309
    }
310
311
    /**
312
     * Create a related subAggregate
313
     *
314
     * @param  mixed $entity
315
     * @param  string $relation
316
     * @throws MappingException
317
     * @return self
318
     */
319
    protected function createSubAggregate($entity, $relation)
320
    {
321
        // If root isn't defined, then this is the Aggregate Root
322
        if (is_null($this->root)) {
323
            $root = $this;
324
        } else {
325
            $root = $this->root;
326
        }
327
328
        return new self($entity, $this, $relation, $root);
329
    }
330
331
    /**
332
     * Get the Entity's primary key attribute
333
     *
334
     * @return string|integer
335
     */
336
    public function getEntityId()
337
    {
338
        return $this->wrappedEntity->getEntityKey();
339
    }
340
341
    /**
342
     * Get the name of the primary key
343
     *
344
     * @return string
345
     */
346
    public function getEntityKey()
347
    {
348
        return $this->entityMap->getKeyName();
349
    }
350
351
    /**
352
     * Return the entity map for the current entity
353
     *
354
     * @return \Analogue\ORM\EntityMap
355
     */
356
    public function getEntityMap()
357
    {
358
        return $this->entityMap;
359
    }
360
361
    /**
362
     * Return the Entity's hash $class.$id
363
     *
364
     * @return string
365
     */
366
    public function getEntityHash()
367
    {
368
        return $this->getEntityClass() . '.' . $this->getEntityId();
369
    }
370
371
    /**
372
     * Get wrapped entity class
373
     *
374
     * @return string
375
     */
376
    public function getEntityClass()
377
    {
378
        return $this->entityMap->getClass();
379
    }
380
381
    /**
382
     * Return the Mapper's entity cache
383
     *
384
     * @return \Analogue\ORM\System\EntityCache
385
     */
386
    protected function getEntityCache()
387
    {
388
        return $this->getMapper()->getEntityCache();
389
    }
390
391
    /**
392
     * Get a relationship as an aggregated entities' array
393
     *
394
     * @param  string $name
395
     * @return array
396
     */
397
    public function getRelationship($name)
398
    {
399
        if (array_key_exists($name, $this->relationships)) {
400
            return $this->relationships[$name];
401
        } else {
402
            return [];
403
        }
404
    }
405
406
    /**
407
     * [TO IMPLEMENT]
408
     *
409
     * @return array
410
     */
411
    public function getPivotAttributes()
412
    {
413
        return [];
414
    }
415
416
    /**
417
     * Get Non existing related entities from several relationships
418
     *
419
     * @param  array $relationships
420
     * @return array
421
     */
422
    public function getNonExistingRelated(array $relationships)
423
    {
424
        $nonExisting = [];
425
426
        foreach ($relationships as $relation) {
427
428
            if ($this->hasAttribute($relation) && array_key_exists($relation, $this->relationships)) {
429
                $nonExisting = array_merge($nonExisting, $this->getNonExistingFromRelation($relation));
430
            }
431
        }
432
433
        return $nonExisting;
434
    }
435
436
    /**
437
     * Get non-existing related entities from a single relation
438
     *
439
     * @param  string $relation
440
     * @return array
441
     */
442
    protected function getNonExistingFromRelation($relation)
443
    {
444
        $nonExisting = [];
445
446
        foreach ($this->relationships[$relation] as $aggregate) {
447
            if (!$aggregate->exists()) {
448
                $nonExisting[] = $aggregate;
449
            }
450
        }
451
452
        return $nonExisting;
453
    }
454
455
    /**
456
     * Synchronize relationships if needed
457
     *
458
     * @param array
459
     * @return void
460
     */
461
    public function syncRelationships(array $relationships)
462
    {
463
        foreach ($relationships as $relation) {
464
            if (in_array($relation, $this->needSync)) {
465
                $this->synchronize($relation);
466
            }
467
        }
468
    }
469
470
    /**
471
     * Synchronize a relationship attribute
472
     *
473
     * @param $relation
474
     * @return void
475
     */
476
    protected function synchronize($relation)
477
    {
478
        $actualContent = $this->relationships[$relation];
479
480
        $relationshipObject = $this->entityMap->$relation($this->getEntityObject());
481
        $relationshipObject->setParent($this->wrappedEntity);
482
        $relationshipObject->sync($actualContent);
483
    }
484
485
    /**
486
     * Returns an array of Missing related Entities for the
487
     * given $relation
488
     *
489
     * @param  string $relation
490
     * @return array
491
     */
492
    public function getMissingEntities($relation)
493
    {
494
        $cachedRelations = $this->getCachedAttribute($relation);
495
496
        if (!is_null($cachedRelations)) {
497
            $missing = [];
498
499
            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...
500
                if (!$this->getRelatedAggregateFromHash($hash, $relation)) {
501
                    $missing[] = $hash;
502
                }
503
            }
504
505
            return $missing;
506
        } else {
507
            return [];
508
        }
509
    }
510
       
511
    /**
512
     * Get Relationships who have dirty attributes / dirty relationships
513
     *
514
     * @return array
515
     */
516
    public function getDirtyRelationships()
517
    {
518
        $dirtyAggregates = [];
519
520
        foreach ($this->relationships as $relation) {
521
            foreach ($relation as $aggregate) {
522
523
                if (!$aggregate->exists() || $aggregate->isDirty() || count($aggregate->getDirtyRelationships()) > 0) {
524
                    $dirtyAggregates[] = $aggregate;
525
                }
526
            }
527
        }
528
529
        return $dirtyAggregates;
530
    }
531
    
532
    /**
533
     * Compare the object's raw attributes with the record in cache
534
     *
535
     * @return boolean
536
     */
537
    public function isDirty()
538
    {
539
        if (count($this->getDirtyRawAttributes()) > 0) {
540
            return true;
541
        } else {
542
            return false;
543
        }
544
    }
545
546
    /**
547
     * Get Raw Entity's attributes, as they are represented
548
     * in the database, including value objects, foreign keys,
549
     * and discriminator column
550
     *
551
     * @return array
552
     */
553
    public function getRawAttributes()
554
    {
555
        $attributes = $this->wrappedEntity->getEntityAttributes();
556
557
        foreach ($this->entityMap->getRelationships() as $relation) {
558
            unset($attributes[$relation]);
559
        }
560
561
        if($this->entityMap->getInheritanceType() == 'single_table') {
562
            $attributes = $this->addDiscriminatorColumn($attributes);
563
        }
564
565
        $attributes = $this->flattenEmbeddables($attributes);
566
567
        $foreignKeys = $this->getForeignKeyAttributes();
568
569
        return $attributes + $foreignKeys;
570
    }
571
572
    /**
573
     * Add Discriminator Column if it doesn't exist on the actual entity
574
     * 
575
     * @param array $attributes
576
     * @return array
577
     */
578
    protected function addDiscriminatorColumn($attributes)
579
    {
580
        $discriminatorColumn = $this->entityMap->getDiscriminatorColumn();
581
        $entityClass = $this->entityMap->getClass();
582
583
        if(! array_key_exists($discriminatorColumn, $attributes)) {
584
            
585
            // Use key if present in discriminatorMap
586
            $map = $this->entityMap->getDiscriminatorColumnMap();
587
588
            $type = array_search($entityClass, $map);
589
590
            if($type === false) {
591
                // Use entity FQDN if no corresponding key is set
592
                $attributes[$discriminatorColumn] = $entityClass;
593
            }
594
            else {
595
                $attributes[$discriminatorColumn] = $type;
596
            }
597
        }
598
599
        return $attributes;
600
    }
601
602
    /**
603
     * Convert Value Objects to raw db attributes
604
     *
605
     * @param  array $attributes
606
     * @return array
607
     */
608
    protected function flattenEmbeddables($attributes)
609
    {
610
        $embeddables = $this->entityMap->getEmbeddables();
611
        
612
        foreach ($embeddables as $localKey => $embed) {
613
            // Retrieve the value object from the entity's attributes
614
            $valueObject = $attributes[$localKey];
615
616
            // Unset the corresponding key
617
            unset($attributes[$localKey]);
618
619
            // TODO Make wrapper object compatible with value objects
620
            $valueObjectAttributes = $valueObject->getEntityAttributes();
621
622
            // Now (if setup in the entity map) we prefix the value object's
623
            // attributes with the snake_case name of the embedded class.
624
            $prefix = snake_case(class_basename($embed));
625
626
            foreach ($valueObjectAttributes as $key=>$value) {
627
                $valueObjectAttributes[$prefix . '_' . $key] = $value;
628
                unset($valueObjectAttributes[$key]);
629
            }
630
631
            $attributes = array_merge($attributes, $valueObjectAttributes);
632
        }
633
        
634
        return $attributes;
635
    }
636
637
    /**
638
     * Return's entity raw attributes in the state they were at last
639
     * query.
640
     *
641
     * @param  array|null $columns
642
     * @return array
643
     */
644 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...
645
    {
646
        $cachedAttributes = $this->getCache()->get($this->getEntityId());
647
648
        if (is_null($columns)) {
649
            return $cachedAttributes;
650
        } else {
651
            return array_only($cachedAttributes, $columns);
652
        }
653
    }
654
655
    /**
656
     * Return a single attribute from the cache
657
     * @param  string $key
658
     * @return mixed
659
     */
660 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...
661
    {
662
        $cachedAttributes = $this->getCache()->get($this->getEntityId());
663
664
        if (!array_key_exists($key, $cachedAttributes)) {
665
            return null;
666
        } else {
667
            return $cachedAttributes[$key];
668
        }
669
    }
670
671
    /**
672
     * Convert related Entity's attributes to foreign keys
673
     *
674
     * @return array
675
     */
676
    protected function getForeignKeyAttributes()
677
    {
678
        $foreignKeys = [];
679
680
        foreach ($this->entityMap->getLocalRelationships() as $relation) {
681
            // check if relationship has been parsed, meaning it has an actual object
682
            // in the entity's attributes
683
            if ($this->isActualRelationships($relation)) {
684
                $foreignKeys = $foreignKeys + $this->getForeignKeyAttributesFromRelation($relation);
685
            }
686
        }
687
688
        if ( ! is_null($this->parent)) {
689
            $foreignKeys = $foreignKeys + $this->getForeignKeyAttributesFromParent();
690
        }
691
692
        return $foreignKeys;
693
    }
694
695
    /**
696
     * Return an associative array containing the key-value pair(s) from
697
     * the related entity.
698
     *
699
     * @param  string $relation
700
     * @return array
701
     */
702
    protected function getForeignKeyAttributesFromRelation($relation)
703
    {
704
        $localRelations = $this->entityMap->getLocalRelationships();
705
706
        if (in_array($relation, $localRelations)) {
707
            // Call Relationship's method
708
            $relationship = $this->entityMap->$relation($this->getEntityObject());
709
710
            $relatedAggregate = $this->relationships[$relation][0];
711
712
            return $relationship->getForeignKeyValuePair($relatedAggregate->getEntityObject());
713
        } else {
714
            return [];
715
        }
716
    }
717
718
    /**
719
     * Get foreign key attribute(s) from a parent entity in this
720
     * aggregate context
721
     *
722
     * @return array
723
     */
724
    protected function getForeignKeyAttributesFromParent()
725
    {
726
        $parentMap = $this->parent->getEntityMap();
727
728
        $parentForeignRelations = $parentMap->getForeignRelationships();
729
        $parentPivotRelations = $parentMap->getPivotRelationships();
730
731
        // The parentRelation is the name of the relationship
732
        // methods on the parent entity map
733
        $parentRelation = $this->parentRelationship;
734
735
        if (in_array($parentRelation, $parentForeignRelations)
736
            && !in_array($parentRelation, $parentPivotRelations)
737
        ) {
738
            $parentObject = $this->parent->getEntityObject();
739
740
            // Call Relationship's method on parent map
741
            $relationship = $parentMap->$parentRelation($parentObject);
742
743
            return $relationship->getForeignKeyValuePair();
744
        } else {
745
            return [];
746
        }
747
    }
748
749
    /**
750
     * Update Pivot records on loaded relationships, by comparing the
751
     * values from the Entity Cache to the actual relationship inside
752
     * the aggregated entity.
753
     *
754
     * @return void
755
     */
756
    public function updatePivotRecords()
757
    {
758
        $pivots = $this->entityMap->getPivotRelationships();
759
760
        foreach ($pivots as $pivot) {
761
            if (array_key_exists($pivot, $this->relationships)) {
762
                $this->updatePivotRelation($pivot);
763
            }
764
        }
765
    }
766
767
    /**
768
     * Update Single pivot relationship
769
     *
770
     * @param  string $relation
771
     * @return void
772
     */
773
    protected function updatePivotRelation($relation)
774
    {
775
        $hashes = $this->getEntityHashesFromRelation($relation);
776
777
        $cachedAttributes = $this->getCachedRawAttributes();
778
779
        if (array_key_exists($relation, $cachedAttributes)) {
780
            // Compare the two array of hashes to find out existing
781
            // pivot records, and the ones to be created.
782
            $new = array_diff($hashes, array_keys($cachedAttributes[$relation]));
783
            $existing = array_intersect($hashes, array_keys($cachedAttributes[$relation]));
784
        } else {
785
            $existing = [];
786
            $new = $hashes;
787
        }
788
789
        if (count($new) > 0) {
790
            $pivots = $this->getRelatedAggregatesFromHashes($new, $relation);
791
792
            $this->entityMap->$relation($this->getEntityObject())->createPivots($pivots);
793
        }
794
795
        if (count($existing) > 0) {
796
            foreach ($existing as $pivotHash) {
797
                $this->updatePivotIfDirty($pivotHash, $relation);
798
            }
799
        }
800
    }
801
802
    /**
803
     * Compare existing pivot record in cache and update it
804
     * if the pivot attributes are dirty
805
     *
806
     * @param  string $pivotHash
807
     * @param  string $relation
808
     * @return void
809
     */
810
    protected function updatePivotIfDirty($pivotHash, $relation)
811
    {
812
        $aggregate = $this->getRelatedAggregateFromHash($pivotHash, $relation);
813
814
        if ($aggregate->hasAttribute('pivot')) {
815
            $pivot = $aggregate->getEntityAttribute('pivot')->getEntityAttributes();
816
817
            $cachedPivotAttributes = $this->getPivotAttributesFromCache($pivotHash, $relation);
818
819
            $actualPivotAttributes = array_only($pivot, array_keys($cachedPivotAttributes));
820
821
            $dirty = $this->getDirtyAttributes($actualPivotAttributes, $cachedPivotAttributes);
822
823
            if (count($dirty) > 0) {
824
                $id = $aggregate->getEntityId();
825
826
                $this->entityMap->$relation($this->getEntityObject())->updateExistingPivot($id, $dirty);
827
            }
828
        }
829
    }
830
831
    /**
832
     * Compare two attributes array and return dirty attributes
833
     *
834
     * @param  array $actual
835
     * @param  array $cached
836
     * @return array
837
     */
838
    protected function getDirtyAttributes(array $actual, array $cached)
839
    {
840
        $dirty = [];
841
842
        foreach ($actual as $key => $value) {
843
            if (!$this->originalIsNumericallyEquivalent($value, $cached[$key])) {
844
                $dirty[$key] = $actual[$key];
845
            }
846
        }
847
848
        return $dirty;
849
    }
850
851
    /**
852
     *
853
     * @param  string $pivotHash
854
     * @param  string $relation
855
     * @return array
856
     */
857
    protected function getPivotAttributesFromCache($pivotHash, $relation)
858
    {
859
        $cachedAttributes = $this->getCachedRawAttributes();
860
861
        $cachedRelations = $cachedAttributes[$relation];
862
863
        foreach ($cachedRelations as $cachedRelation) {
864
            if ($cachedRelation == $pivotHash) {
865
                return $cachedRelation->getPivotAttributes();
866
            }
867
        }
868
    }
869
870
    /**
871
     * Returns an array of related Aggregates from its entity hashes
872
     *
873
     * @param  array  $hashes
874
     * @param  string $relation
875
     * @return array
876
     */
877
    protected function getRelatedAggregatesFromHashes(array $hashes, $relation)
878
    {
879
        $related = [];
880
881
        foreach ($hashes as $hash) {
882
            $aggregate = $this->getRelatedAggregateFromHash($hash, $relation);
883
884
            if (!is_null($aggregate)) {
885
                $related[] = $aggregate;
886
            }
887
        }
888
889
        return $related;
890
    }
891
892
    /**
893
     * Get related aggregate from its hash
894
     *
895
     * @param  string $hash
896
     * @param  string $relation
897
     * @return \Analogue\ORM\System\Aggregate|null
898
     */
899
    protected function getRelatedAggregateFromHash($hash, $relation)
900
    {
901
        foreach ($this->relationships[$relation] as $aggregate) {
902
            if ($aggregate->getEntityHash() == $hash) {
903
                return $aggregate;
904
            }
905
        }
906
        return null;
907
    }
908
909
    /**
910
     * Return an array of Entity Hashes from a specific relation
911
     *
912
     * @param  string $relation
913
     * @return array
914
     */
915
    protected function getEntityHashesFromRelation($relation)
916
    {
917
        return array_map(function ($aggregate) {
918
            return $aggregate->getEntityHash();
919
        }, $this->relationships[$relation]);
920
    }
921
922
    /**
923
     * Check the existence of an actual relationship
924
     *
925
     * @param  string $relation
926
     * @return boolean
927
     */
928
    protected function isActualRelationships($relation)
929
    {
930
        return array_key_exists($relation, $this->relationships)
931
            && count($this->relationships[$relation]) > 0;
932
    }
933
934
    /**
935
     * Return cache instance for the current entity type
936
     *
937
     * @return \Analogue\ORM\System\EntityCache
938
     */
939
    protected function getCache()
940
    {
941
        return $this->getMapper()->getEntityCache();
942
    }
943
944
    /**
945
     * Get Only Raw Entiy's attributes which have been modified
946
     * since last query
947
     *
948
     * @return array
949
     */
950
    public function getDirtyRawAttributes()
951
    {
952
        $attributes = $this->getRawAttributes();
953
        $cachedAttributes = $this->getCachedRawAttributes(array_keys($attributes));
954
955
        $dirty = [];
956
957
        foreach ($attributes as $key => $value) {
958
            if ($this->isRelation($key) || $key == 'pivot') {
959
                continue;
960
            }
961
962
            if (!array_key_exists($key, $cachedAttributes) && !$value instanceof Pivot) {
963
                $dirty[$key] = $value;
964
            } elseif ($value !== $cachedAttributes[$key] &&
965
                !$this->originalIsNumericallyEquivalent($value, $cachedAttributes[$key])) {
966
                $dirty[$key] = $value;
967
            }
968
        }
969
970
        return $dirty;
971
    }
972
973
    /**
974
     * @param $key
975
     * @return bool
976
     */
977
    protected function isRelation($key)
978
    {
979
        return in_array($key, $this->entityMap->getRelationships());
980
    }
981
982
    /**
983
     * Determine if the new and old values for a given key are numerically equivalent.
984
     *
985
     * @param $current
986
     * @param $original
987
     * @return boolean
988
     */
989
    protected function originalIsNumericallyEquivalent($current, $original)
990
    {
991
        return is_numeric($current) && is_numeric($original) && strcmp((string) $current, (string) $original) === 0;
992
    }
993
994
    /**
995
     * Get the underlying entity object
996
     *
997
     * @return mixed
998
     */
999
    public function getEntityObject()
1000
    {
1001
        return $this->wrappedEntity->getObject();
1002
    }
1003
1004
    /**
1005
     * Return the Mapper instance for the current Entity Type
1006
     *
1007
     * @return \Analogue\ORM\System\Mapper
1008
     */
1009
    public function getMapper()
1010
    {
1011
        return Manager::getMapper($this->class);
1012
    }
1013
1014
    /**
1015
     * Check that the entity already exists in the database, by checking
1016
     * if it has an EntityCache record
1017
     *
1018
     * @return boolean
1019
     */
1020
    public function exists()
1021
    {
1022
        return $this->getCache()->has($this->getEntityId());
1023
    }
1024
1025
    /**
1026
     * Set the object attribute raw values (hydration)
1027
     *
1028
     * @param array $attributes
1029
     */
1030
    public function setEntityAttributes(array $attributes)
1031
    {
1032
        $this->wrappedEntity->setEntityAttributes($attributes);
1033
    }
1034
1035
    /**
1036
     * Get the raw object's values.
1037
     *
1038
     * @return array
1039
     */
1040
    public function getEntityAttributes()
1041
    {
1042
        return $this->wrappedEntity->getEntityAttributes();
1043
    }
1044
1045
    /**
1046
     * Set the raw entity attributes
1047
     * @param string $key
1048
     * @param string $value
1049
     */
1050
    public function setEntityAttribute($key, $value)
1051
    {
1052
        $this->wrappedEntity->setEntityAttribute($key, $value);
1053
    }
1054
1055
    /**
1056
     * Return the entity's attribute
1057
     * @param  string $key
1058
     * @return mixed
1059
     */
1060
    public function getEntityAttribute($key)
1061
    {
1062
        return $this->wrappedEntity->getEntityAttribute($key);
1063
    }
1064
1065
    /**
1066
     * Does the attribute exists on the entity
1067
     *
1068
     * @param  string  $key
1069
     * @return boolean
1070
     */
1071
    public function hasAttribute($key)
1072
    {
1073
        return $this->wrappedEntity->hasAttribute($key);
1074
    }
1075
1076
    /**
1077
     * Set the lazyloading proxies on the wrapped entity
1078
     * 
1079
     * @return  void
1080
     */
1081
    public function setProxies()
1082
    {
1083
        $this->wrappedEntity->setProxies();
1084
    }
1085
1086
    /**  
1087
     * Hydrate the actual entity
1088
     * 
1089
     * @return void
1090
     */
1091
    public function hydrate()
1092
    {
1093
        $this->wrappedEntity->hydrate();
1094
    }
1095
}
1096