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

EntityMap::addNonProxyRelation()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 3
nc 2
nop 1
dl 0
loc 6
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
namespace Analogue\ORM;
4
5
use Analogue\ORM\Exceptions\MappingException;
6
use Exception;
7
use ReflectionClass;
8
use Analogue\ORM\System\Manager;
9
use Analogue\ORM\System\Wrappers\Factory;
10
use Analogue\ORM\Relationships\BelongsTo;
11
use Analogue\ORM\Relationships\BelongsToMany;
12
use Analogue\ORM\Relationships\HasMany;
13
use Analogue\ORM\Relationships\HasManyThrough;
14
use Analogue\ORM\Relationships\HasOne;
15
use Analogue\ORM\Relationships\MorphMany;
16
use Analogue\ORM\Relationships\MorphOne;
17
use Analogue\ORM\Relationships\MorphTo;
18
use Analogue\ORM\Relationships\MorphToMany;
19
20
/**
21
 * The Entity Map defines the Mapping behaviour of an Entity,
22
 * including relationships.
23
 */
24
class EntityMap
25
{
26
    /**
27
     * The mapping driver to use with this entity
28
     *
29
     * @var  string
30
     */
31
    protected $driver = 'illuminate';
32
33
    /**
34
     * The Database Connection name for the model.
35
     *
36
     * @var string
37
     */
38
    protected $connection;
39
40
    /**
41
     * The table associated with the entity.
42
     *
43
     * @var string|null
44
     */
45
    protected $table = null;
46
47
    /**
48
     * The primary key for the model.
49
     *
50
     * @var string
51
     */
52
    protected $primaryKey = 'id';
53
54
    /**
55
     * Name of the entity's array property that should
56
     * contain the attributes. 
57
     * If set to null, analogue will only hydrate object's properties
58
     * 
59
     * @var string|null
60
     */
61
    protected $arrayName = 'attributes';
62
63
    /**
64
     * Array containing the list of database columns to be mapped
65
     * in the attributes array of the entity. 
66
     *
67
     * @var array
68
     */
69
    protected $attributes = [];
70
71
    /**
72
     * Array containing the list of database columns to be mapped
73
     * to the entity's class properties. 
74
     *
75
     * @var array
76
     */
77
    protected $properties = [];
78
79
    /**
80
     * The Custom Domain Class to use with this mapping
81
     *
82
     * @var string|null
83
     */
84
    protected $class = null;
85
86
    /**
87
     * Embedded Value Objects
88
     * 
89
     * @var array
90
     */
91
    protected $embeddables = [];
92
93
    /**
94
     * Determine the relationships method used on the entity.
95
     * If not set, mapper will autodetect them
96
     *
97
     * @var array
98
     */
99
    private $relationships = [];
100
101
    /**
102
     * Relationships that should be treated as collection.
103
     *
104
     * @var array
105
     */
106
    private $manyRelations = [];
107
108
    /**
109
     * Relationships that should be treated as single entity.
110
     *
111
     * @var array
112
     */
113
    private $singleRelations = [];
114
115
    /**
116
     * Relationships for which the key is stored in the Entity itself
117
     *
118
     * @var array
119
     */
120
    private $localRelations = [];
121
122
    /** 
123
     * List of local keys associated to local relation methods
124
     * 
125
     * @var array
126
     */
127
    private $localForeignKeys = [];
128
129
    /**
130
     * Relationships for which the key is stored in the Related Entity
131
     *
132
     * @var array
133
     */
134
    private $foreignRelations = [];
135
136
    /**
137
     * Relationships which use a pivot record.
138
     *
139
     * @var array
140
     */
141
    private $pivotRelations = [];
142
143
    /**
144
     * Dynamic relationships
145
     *
146
     * @var array
147
     */
148
    private $dynamicRelationships = [];
149
150
    /**
151
     * Targetted class for the relationship method. value is set to `null` for
152
     * polymorphic relations. 
153
     * 
154
     * @var array
155
     */
156
    private $relatedClasses = [];
157
158
    /** 
159
     * Some relation methods like embedded objects, or HasOne and MorphOne,
160
     * will never have a proxy loaded on them. 
161
     * 
162
     * @var  array
163
     */
164
    private $nonProxyRelationships = [];
165
166
    /**
167
     * The number of models to return for pagination.
168
     *
169
     * @var int
170
     */
171
    protected $perPage = 15;
172
173
    /**
174
     * The relations to eager load on every query.
175
     *
176
     * @var array
177
     */
178
    protected $with = [];
179
180
    /**
181
     * The class name to be used in polymorphic relations.
182
     *
183
     * @var string
184
     */
185
    protected $morphClass;
186
187
    /**
188
     * Sequence name, to be used with postgreSql
189
     * defaults to %table_name%_id_seq
190
     *
191
     * @var string|null
192
     */
193
    protected $sequence = null;
194
195
    /**
196
     * Indicates if the entity should be timestamped.
197
     *
198
     * @var bool
199
     */
200
    public $timestamps = false;
201
202
    /**
203
     * The name of the "created at" column.
204
     *
205
     * @var string
206
     */
207
    protected $createdAtColumn = 'created_at';
208
209
    /**
210
     * The name of the "updated at" column.
211
     *
212
     * @var string
213
     */
214
    protected $updatedAtColumn = 'updated_at';
215
216
    /**
217
     * Indicates if the entity uses softdeletes
218
     *
219
     * @var boolean
220
     */
221
    public $softDeletes = false;
222
223
    /**
224
     * The name of the "deleted at" column.
225
     *
226
     * @var string
227
     */
228
    protected $deletedAtColumn = 'deleted_at';
229
230
    /**
231
     * The date format to use with the current database connection
232
     *
233
     * @var string
234
     */
235
    protected $dateFormat;
236
237
    /**
238
     * Set this property to true if the entity should be instantiated
239
     * using the IoC Container
240
     * 
241
     * @var boolean
242
     */
243
    protected $dependencyInjection = false;
244
245
    /**
246
     * Set the usage of inheritance, possible values are :
247
     * "single_table"
248
     * null
249
     * 
250
     * @var string | null
251
     */
252
    protected $inheritanceType = null;
253
254
    /**
255
     * Discriminator column name
256
     * 
257
     * @var string
258
     */
259
    protected $discriminatorColumn = "type";
260
261
    /**
262
     * Allow using a string to define which entity type should be instantiated.
263
     * If not set, analogue will uses entity's FQDN
264
     * 
265
     * @var array
266
     */
267
    protected $discriminatorColumnMap = [];
268
269
    /**
270
     * Return Domain class attributes, useful when mapping to a Plain PHP Object
271
     *
272
     * @return array
273
     */
274
    public function getAttributes() : array
275
    {
276
        return $this->attributes;
277
    }
278
279
    /**
280
     * Set the domain class attributes
281
     *
282
     * @param array $attributeNames
283
     */
284
    public function setAttributes(array $attributeNames)
285
    {
286
        $this->attributes = $attributeNames;
287
    }
288
289
    /**  
290
     * Return true if the Entity has an 'attributes' array property
291
     * 
292
     * @return boolean
293
     */
294
    public function usesAttributesArray() : bool
295
    {
296
        return $this->arrayName === null ? false : true;
297
    }
298
299
    /**  
300
     * Return the name of the Entity's attributes property
301
     * 
302
     * @return string|null
303
     */
304
    public function getAttributesArrayName()
305
    {
306
        return $this->arrayName;
307
    }
308
309
    /**
310
     * Get all the attribute names for the class, including relationships, embeddables and primary key.
311
     *
312
     * @return array
313
     */
314
    public function getCompiledAttributes() : array
315
    {
316
        $key = $this->getKeyName();
317
318
        $embeddables = array_keys($this->getEmbeddables());
319
320
        $relationships = $this->getRelationships();
321
322
        $attributes = $this->getAttributes();
323
324
        return array_merge([$key], $embeddables, $relationships, $attributes);
325
    }
326
327
    /**
328
     * Set the date format to use with the current database connection
329
     *
330
     * @param string $format
331
     */
332
    public function setDateFormat($format)
333
    {
334
        $this->dateFormat = $format;
335
    }
336
337
    /**
338
     * Get the date format to use with the current database connection
339
     *
340
     *  @return string
341
     */
342
    public function getDateFormat() : string
343
    {
344
        return $this->dateFormat;
345
    }
346
347
    /**
348
     * Set the Driver for this mapping
349
     *
350
     * @param string $driver
351
     */
352
    public function setDriver($driver)
353
    {
354
        $this->driver = $driver;
355
    }
356
357
    /**
358
     * Get the Driver for this mapping.
359
     *
360
     * @return string
361
     */
362
    public function getDriver() : string
363
    {
364
        return $this->driver;
365
    }
366
367
    /**
368
     * Set the db connection to use on the table
369
     *
370
     * @param $connection
371
     */
372
    public function setConnection($connection)
373
    {
374
        $this->connection = $connection;
375
    }
376
377
    /**
378
     * Get the Database connection the Entity is stored on.
379
     *
380
     * @return string | null
381
     */
382
    public function getConnection()
383
    {
384
        return $this->connection;
385
    }
386
387
    /**
388
     * Get the table associated with the entity.
389
     *
390
     * @return string
391
     */
392
    public function getTable() : string
393
    {
394
        if (!is_null($this->table)) {
395
            return $this->table;
396
        }
397
398
        return str_replace('\\', '', snake_case(str_plural(class_basename($this->getClass()))));
399
    }
400
401
    /**
402
     * Set the database table name
403
     *
404
     * @param  string $table
405
     */
406
    public function setTable($table)
407
    {
408
        $this->table = $table;
409
    }
410
411
    /**
412
     * Get the pgSql sequence name
413
     *
414
     * @return string
415
     */
416
    public function getSequence() : string
417
    {
418
        if (!is_null($this->sequence)) {
419
            return $this->sequence;
420
        } else {
421
            return $this->getTable() . '_id_seq';
422
        }
423
    }
424
425
    /**
426
     * Get the custom entity class
427
     *
428
     * @return string namespaced class name
429
     */
430
    public function getClass() : string
431
    {
432
        return isset($this->class) ? $this->class : null;
433
    }
434
435
    /**
436
     * Set the custom entity class
437
     *
438
     * @param string $class namespaced class name
439
     */
440
    public function setClass($class)
441
    {
442
        $this->class = $class;
443
    }
444
445
    /**
446
     * Get the embedded Value Objects
447
     *
448
     * @return array
449
     */
450
    public function getEmbeddables() : array
451
    {
452
        return $this->embeddables;
453
    }
454
455
    /**  
456
     * Return attributes that should be mapped to class properties
457
     * 
458
     * @return array
459
     */
460
    public function getProperties() : array
461
    {
462
        return $this->properties;
463
    }
464
465
    /**  
466
     * Return the array property in which will be mapped all attributes
467
     * that are not mapped to class properties.
468
     * 
469
     * @return string
470
     */
471
    public function getAttributesPropertyName() : string
472
    {
473
474
    }
475
476
    /**
477
     * Set the embedded Value Objects
478
     *
479
     * @param array $embeddables
480
     */
481
    public function setEmbeddables(array $embeddables)
482
    {
483
        $this->embeddables = $embeddables;
484
    }
485
486
    /**
487
     * Get the relationships to map on a custom domain
488
     * class.
489
     *
490
     * @return array
491
     */
492
    public function getRelationships() : array
493
    {
494
        return $this->relationships;
495
    }
496
497
    /**  
498
     * Get the relationships that will not have a proxy
499
     * set on them
500
     * 
501
     * @return array
502
     */
503
    public function getRelationshipsWithoutProxy() : array
504
    {
505
        return $this->nonProxyRelationships;
506
    }
507
508
    /**
509
     * Relationships of the Entity type
510
     *
511
     * @return array
512
     */
513
    public function getSingleRelationships() : array
514
    {
515
        return $this->singleRelations;
516
    }
517
518
    /**
519
     * Relationships of type Collection
520
     *
521
     * @return array
522
     */
523
    public function getManyRelationships() : array
524
    {
525
        return $this->manyRelations;
526
    }
527
528
    /**
529
     * Relationships with foreign key in the mapped entity record.
530
     *
531
     * @return array
532
     */
533
    public function getLocalRelationships() : array
534
    {
535
        return $this->localRelations;
536
    }
537
538
    /**  
539
     * Return the local keys associated to the relationship
540
     * 
541
     * @param  string $relation
542
     * @return string | array | null
543
     */
544
    public function getLocalKeys($relation)
545
    {
546
        return isset($this->localForeignKeys[$relation]) ? $this->localForeignKeys[$relation] : null;
547
    }
548
549
    /**
550
     * Relationships with foreign key in the related Entity record
551
     *
552
     * @return array
553
     */
554
    public function getForeignRelationships() : array
555
    {
556
        return $this->foreignRelations;
557
    }
558
559
    /**
560
     * Relationships which keys are stored in a pivot record
561
     *
562
     * @return array
563
     */
564
    public function getPivotRelationships() : array
565
    {
566
        return $this->pivotRelations;
567
    }
568
569
    /**  
570
     * Get the targetted type for a relationship. Return null if polymorphic
571
     * 
572
     * @param  string  $relation
573
     * @return string | null
574
     */
575
    public function getTargettedClass($relation)
576
    {
577
        return $this->relatedClasses[$relation];
578
    }
579
580
    /**
581
     * Add a Dynamic Relationship method at runtime. This has to be done
582
     * by hooking the 'initializing' event, before entityMap is initialized.
583
     *
584
     * @param string  $name         Relation name
585
     * @param \Closure $relationship
586
     *
587
     * @return void
588
     */
589
    public function addRelationshipMethod($name, \Closure $relationship)
590
    {
591
        $this->dynamicRelationships[$name] = $relationship;
592
    }
593
594
    /**
595
     * Get the dynamic relationship method names.
596
     *
597
     * @return array
598
     */
599
    public function getDynamicRelationships() : array
600
    {
601
        return array_keys($this->dynamicRelationships);
602
    }
603
604
    /**
605
     * Get the relationships that have to be eager loaded
606
     * on each request.
607
     *
608
     * @return array
609
     */
610
    public function getEagerloadedRelationships() : array
611
    {
612
        return $this->with;
613
    }
614
615
    /**
616
     * Get the primary key attribute for the entity.
617
     *
618
     * @return string
619
     */
620
    public function getKeyName() : string
621
    {
622
        return $this->primaryKey;
623
    }
624
625
    /**
626
     * Set the primary key for the entity.
627
     *
628
     * @param $key
629
     * @return void
630
     */
631
    public function setKeyName($key)
632
    {
633
        $this->primaryKey = $key;
634
    }
635
636
    /**
637
     * Get the table qualified key name.
638
     *
639
     * @return string
640
     */
641
    public function getQualifiedKeyName() : string
642
    {
643
        return $this->getTable() . '.' . $this->getKeyName();
644
    }
645
646
    /**
647
     * Get the number of models to return per page.
648
     *
649
     * @return int
650
     */
651
    public function getPerPage() : int
652
    {
653
        return $this->perPage;
654
    }
655
656
    /**
657
     * Set the number of models to return per page.
658
     *
659
     * @param  int $perPage
660
     * @return void
661
     */
662
    public function setPerPage($perPage)
663
    {
664
        $this->perPage = $perPage;
665
    }
666
667
    /**
668
     * Determine if the entity uses get.
669
     *
670
     * @return bool
671
     */
672
    public function usesTimestamps() : bool
673
    {
674
        return $this->timestamps;
675
    }
676
677
    /**
678
     * Determine if the entity uses soft deletes
679
     *
680
     * @return bool
681
     */
682
    public function usesSoftDeletes() : bool
683
    {
684
        return $this->softDeletes;
685
    }
686
687
    /**
688
     * Get the 'created_at' column name
689
     *
690
     * @return string
691
     */
692
    public function getCreatedAtColumn() : string
693
    {
694
        return $this->createdAtColumn;
695
    }
696
697
    /**
698
     * Get the 'updated_at' column name
699
     *
700
     * @return string
701
     */
702
    public function getUpdatedAtColumn() : string
703
    {
704
        return $this->updatedAtColumn;
705
    }
706
707
    /**
708
     * Get the deleted_at column
709
     *
710
     * @return string
711
     */
712
    public function getQualifiedDeletedAtColumn() : string
713
    {
714
        return $this->deletedAtColumn;
715
    }
716
717
    /**
718
     * Get the default foreign key name for the model.
719
     *
720
     * @return string
721
     */
722
    public function getForeignKey() : string
723
    {
724
        return snake_case(class_basename($this->getClass())) . '_id';
725
    }
726
727
    /**
728
     * Return the inheritance type used by the entity.
729
     *
730
     * @return string|null
731
     */
732
    public function getInheritanceType()
733
    {
734
        return $this->inheritanceType;
735
    }
736
737
    /**
738
     * Return the discriminator column name on the entity that's
739
     * used for table inheritance.
740
     *
741
     * @return string
742
     */
743
    public function getDiscriminatorColumn() : string
744
    {
745
        return $this->discriminatorColumn;
746
    }
747
748
    /**
749
     * Return the mapping of discriminator column values to
750
     * entity class names that are used for table inheritance.
751
     *
752
     * @return array
753
     */
754
    public function getDiscriminatorColumnMap() : array
755
    {
756
        return $this->discriminatorColumnMap;
757
    }
758
759
    /**
760
     * Return true if the entity should be instanciated using
761
     * the IoC Container
762
     * 
763
     * @return boolean
764
     */
765
    public function useDependencyInjection() : bool
766
    {
767
        return $this->dependencyInjection;
768
    }
769
770
    /**  
771
     * Add a single relation method name once
772
     * 
773
     * @param string  $relation
774
     */
775
    protected function addSingleRelation($relation)
776
    {
777
        if(! in_array($relation, $this->singleRelations)) {
778
            $this->singleRelations[] = $relation;
779
        }
780
    }
781
782
    /**  
783
     * Add a foreign relation method name once
784
     * 
785
     * @param string  $relation
786
     */
787
    protected function addForeignRelation($relation)
788
    {
789
        if(! in_array($relation, $this->foreignRelations)) {
790
            $this->foreignRelations[] = $relation;
791
        }
792
    }
793
794
    /**  
795
     * Add a non proxy relation method name once
796
     * 
797
     * @param string  $relation
798
     */
799
    protected function addNonProxyRelation($relation)
800
    {
801
        if(! in_array($relation, $this->nonProxyRelationships)) {
802
            $this->nonProxyRelationships[] = $relation;
803
        }
804
    }
805
806
    /**  
807
     * Add a local relation method name once
808
     * 
809
     * @param string  $relation
810
     */
811
    protected function addLocalRelation($relation)
812
    {
813
        if(! in_array($relation, $this->localRelations)) {
814
            $this->localRelations[] = $relation;
815
        }
816
    }
817
818
     /**  
819
     * Add a many relation method name once
820
     * 
821
     * @param string  $relation
822
     */
823
    protected function addManyRelation($relation)
824
    {
825
        if(! in_array($relation, $this->manyRelations)) {
826
            $this->manyRelations[] = $relation;
827
        }
828
    }
829
830
     /**  
831
     * Add a pivot relation method name once
832
     * 
833
     * @param string  $relation
834
     */
835
    protected function addPivotRelation($relation)
836
    {
837
        if(! in_array($relation, $this->pivotRelations)) {
838
            $this->pivotRelations[] = $relation;
839
        }
840
    }
841
842
    /**
843
     * Define a one-to-one relationship.
844
     *
845
     * @param  mixed  $entity
846
     * @param  string $related entity class
847
     * @param  string $foreignKey
848
     * @param  string $localKey
849
     * @throws MappingException
850
     * @return \Analogue\ORM\Relationships\HasOne
851
     */
852
    public function hasOne($entity, $related, $foreignKey = null, $localKey = null)
853
    {
854
        $foreignKey = $foreignKey ?: $this->getForeignKey();
855
856
        $relatedMapper = Manager::getInstance()->mapper($related);
857
858
        $relatedMap = $relatedMapper->getEntityMap();
859
860
        $localKey = $localKey ?: $this->getKeyName();
861
862
        // Add the relation to the definition in map
863
        list(, $caller) = debug_backtrace(false);
864
        $relation = $caller['function'];
865
        $this->relatedClasses[$relation] = $related;
866
        
867
        $this->addSingleRelation($relation);
868
        $this->addForeignRelation($relation);
869
        $this->addNonProxyRelation($relation);
870
871
        // This relationship will always be eager loaded, as proxying it would
872
        // mean having an object that doesn't actually exists.
873
        if(! in_array($relation, $this->with)) {
874
            $this->with[] = $relation;
875
        }
876
877
        return new HasOne($relatedMapper, $entity, $relatedMap->getTable() . '.' . $foreignKey, $localKey);
878
    }
879
880
    /**
881
     * Define a polymorphic one-to-one relationship.
882
     *
883
     * @param  mixed       $entity
884
     * @param  string      $related
885
     * @param  string      $name
886
     * @param  string|null $type
887
     * @param  string|null $id
888
     * @param  string|null $localKey
889
     * @throws MappingException
890
     * @return \Analogue\ORM\Relationships\MorphOne
891
     */
892
    public function morphOne($entity, $related, $name, $type = null, $id = null, $localKey = null)
893
    {
894
        list($type, $id) = $this->getMorphs($name, $type, $id);
895
896
        $localKey = $localKey ?: $this->getKeyName();
897
898
        $relatedMapper = Manager::getInstance()->mapper($related);
899
900
        $table = $relatedMapper->getEntityMap()->getTable();
901
902
        // Add the relation to the definition in map
903
        list(, $caller) = debug_backtrace(false);
904
        $relation = $caller['function'];
905
        $this->relatedClasses[$relation] = $related;
906
        
907
        $this->addSingleRelation($relation);
908
        $this->addForeignRelation($relation);
909
        $this->addNonProxyRelation($relation);
910
911
        // This relationship will always be eager loaded, as proxying it would
912
        // mean having an object that doesn't actually exists.
913
        if(! in_array($relation, $this->with)) {
914
            $this->with[] = $relation;
915
        }
916
        
917
        return new MorphOne($relatedMapper, $entity, $table . '.' . $type, $table . '.' . $id, $localKey);
918
    }
919
920
    /**
921
     * Define an inverse one-to-one or many relationship.
922
     *
923
     * @param  mixed       $entity
924
     * @param  string      $related
925
     * @param  string|null $foreignKey
926
     * @param  string|null $otherKey
927
     * @param  string|null $relation
0 ignored issues
show
Bug introduced by
There is no parameter named $relation. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
928
     * @throws MappingException
929
     * @return \Analogue\ORM\Relationships\BelongsTo
930
     */
931
    public function belongsTo($entity, $related, $foreignKey = null, $otherKey = null)
932
    {
933
        // Add the relation to the definition in map
934
        list(, $caller) = debug_backtrace(false);
935
        $relation = $caller['function'];
936
        $this->relatedClasses[$relation] = $related;
937
        
938
        $this->addSingleRelation($relation);
939
        $this->addLocalRelation($relation);
940
941
        // If no foreign key was supplied, we can use a backtrace to guess the proper
942
        // foreign key name by using the name of the relationship function, which
943
        // when combined with an "_id" should conventionally match the columns.
944
        if (is_null($foreignKey)) {
945
            $foreignKey = snake_case($relation) . '_id';
946
        }
947
948
        $this->localForeignKeys[$relation] = $foreignKey;
949
950
        $relatedMapper = Manager::getInstance()->mapper($related);
951
952
        $otherKey = $otherKey ?: $relatedMapper->getEntityMap()->getKeyName();
953
954
        return new BelongsTo($relatedMapper, $entity, $foreignKey, $otherKey, $relation);
955
    }
956
957
    /**
958
     * Define a polymorphic, inverse one-to-one or many relationship.
959
     *
960
     * @param  mixed       $entity
961
     * @param  string|null $name
962
     * @param  string|null $type
963
     * @param  string|null $id
964
     * @throws MappingException
965
     * @return \Analogue\ORM\Relationships\MorphTo
966
     */
967
    public function morphTo($entity, $name = null, $type = null, $id = null)
968
    {
969
        // If no name is provided, we will use the backtrace to get the function name
970
        // since that is most likely the name of the polymorphic interface. We can
971
        // use that to get both the class and foreign key that will be utilized.
972
        if (is_null($name)) {
973
            list(, $caller) = debug_backtrace(false);
974
975
            $name = snake_case($caller['function']);
976
        }
977
        $this->addSingleRelations($name);
0 ignored issues
show
Documentation Bug introduced by
The method addSingleRelations does not exist on object<Analogue\ORM\EntityMap>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
978
        $this->addLocalRelation($name);
979
        $this->addForeignRelations($name);
0 ignored issues
show
Documentation Bug introduced by
The method addForeignRelations does not exist on object<Analogue\ORM\EntityMap>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
980
        $this->relatedClass[$relation] = null;
0 ignored issues
show
Bug introduced by
The property relatedClass does not seem to exist. Did you mean relatedClasses?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
Bug introduced by
The variable $relation does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
981
982
        list($type, $id) = $this->getMorphs($name, $type, $id);
983
984
        // Store the foreign key in the entity map. 
985
        // We might want to store the (key, type) as we might need it
986
        // to build a MorphTo proxy
987
        $this->localForeignKeys[$name] = [
988
            "id" => $id,
989
            "type" => $type
990
        ];
991
        
992
993
        $mapper = Manager::getInstance()->mapper(get_class($entity));
994
995
        // If the type value is null it is probably safe to assume we're eager loading
996
        // the relationship. When that is the case we will pass in a dummy query as
997
        // there are multiple types in the morph and we can't use single queries.
998
        $factory = new Factory;
999
        $wrapper = $factory->make($entity);
1000
1001
        if (is_null($class = $wrapper->getEntityAttribute($type))) {
1002
            return new MorphTo(
1003
                $mapper, $entity, $id, null, $type, $name
1004
            );
1005
        }
1006
1007
        // If we are not eager loading the relationship we will essentially treat this
1008
        // as a belongs-to style relationship since morph-to extends that class and
1009
        // we will pass in the appropriate values so that it behaves as expected.
1010
        else {
1011
            $class = Manager::getInstance()->getInverseMorphMap($class);
1012
            $relatedMapper = Manager::getInstance()->mapper($class);
1013
1014
            $foreignKey = $relatedMapper->getEntityMap()->getKeyName();
1015
1016
            return new MorphTo(
1017
                $relatedMapper, $entity, $id, $foreignKey, $type, $name
1018
            );
1019
        }
1020
    }
1021
1022
    /**
1023
     * Define a one-to-many relationship.
1024
     *
1025
     * @param  mixed       $entity
1026
     * @param  string      $related
1027
     * @param  string|null $foreignKey
1028
     * @param  string|null $localKey
1029
     * @throws MappingException
1030
     * @return \Analogue\ORM\Relationships\HasMany
1031
     */
1032
    public function hasMany($entity, $related, $foreignKey = null, $localKey = null)
1033
    {
1034
        $foreignKey = $foreignKey ?: $this->getForeignKey();
1035
1036
        $relatedMapper = Manager::getInstance()->mapper($related);
1037
1038
        $table = $relatedMapper->getEntityMap()->getTable() . '.' . $foreignKey;
1039
1040
        $localKey = $localKey ?: $this->getKeyName();
1041
1042
        // Add the relation to the definition in map
1043
        list(, $caller) = debug_backtrace(false);
1044
        $relation = $caller['function'];
1045
        $this->relatedClasses[$relation] = $related;
1046
        
1047
        $this->addManyRelation($relation);
1048
        $this->addForeignRelation($relation);
1049
1050
        return new HasMany($relatedMapper, $entity, $table, $localKey);
1051
    }
1052
1053
    /**
1054
     * Define a has-many-through relationship.
1055
     *
1056
     * @param  mixed       $entity
1057
     * @param  string      $related
1058
     * @param  string      $through
1059
     * @param  string|null $firstKey
1060
     * @param  string|null $secondKey
1061
     * @throws MappingException
1062
     * @return \Analogue\ORM\Relationships\HasManyThrough
1063
     */
1064
    public function hasManyThrough($entity, $related, $through, $firstKey = null, $secondKey = null)
1065
    {
1066
        $relatedMapper = Manager::getInstance()->mapper($related);
1067
1068
        $throughMapper = Manager::getInstance()->mapper($through);
1069
1070
1071
        $firstKey = $firstKey ?: $this->getForeignKey();
1072
1073
        $throughMap = $throughMapper->getEntityMap();
1074
1075
        $secondKey = $secondKey ?: $throughMap->getForeignKey();
1076
1077
        // Add the relation to the definition in map
1078
        list(, $caller) = debug_backtrace(false);
1079
        $relation = $caller['function'];
1080
        $this->relatedClasses[$relation] = $related;
1081
        
1082
        $this->addManyRelation($relation);
1083
        $this->addForeignRelation($relation);
1084
1085
        return new HasManyThrough($relatedMapper, $entity, $throughMap, $firstKey, $secondKey);
1086
    }
1087
1088
    /**
1089
     * Define a polymorphic one-to-many relationship.
1090
     *
1091
     * @param  mixed       $entity
1092
     * @param  string      $related
1093
     * @param  string      $name
1094
     * @param  string|null $type
1095
     * @param  string|null $id
1096
     * @param  string|null $localKey
1097
     * @return \Analogue\ORM\Relationships\MorphMany
1098
     */
1099
    public function morphMany($entity, $related, $name, $type = null, $id = null, $localKey = null)
1100
    {
1101
        // Here we will gather up the morph type and ID for the relationship so that we
1102
        // can properly query the intermediate table of a relation. Finally, we will
1103
        // get the table and create the relationship instances for the developers.
1104
        list($type, $id) = $this->getMorphs($name, $type, $id);
1105
1106
        $relatedMapper = Manager::getInstance()->mapper($related);
1107
1108
        $table = $relatedMapper->getEntityMap()->getTable();
1109
1110
        $localKey = $localKey ?: $this->getKeyName();
1111
1112
        // Add the relation to the definition in map
1113
        list(, $caller) = debug_backtrace(false);
1114
        $relation = $caller['function'];
1115
        $this->relatedClasses[$relation] = $related;
1116
        
1117
        $this->addManyRelation($relation);
1118
        $this->addForeignRelation($relation);
1119
1120
        return new MorphMany($relatedMapper, $entity, $table . '.' . $type, $table . '.' . $id, $localKey);
1121
    }
1122
1123
    /**
1124
     * Define a many-to-many relationship.
1125
     *
1126
     * @param  mixed       $entity
1127
     * @param  string      $relatedClass
0 ignored issues
show
Documentation introduced by
There is no parameter named $relatedClass. Did you maybe mean $related?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function. It has, however, found a similar but not annotated parameter which might be a good fit.

Consider the following example. The parameter $ireland is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $ireland
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was changed, but the annotation was not.

Loading history...
1128
     * @param  string|null $table
1129
     * @param  string|null $foreignKey
1130
     * @param  string|null $otherKey
1131
     * @param  string|null $relation
0 ignored issues
show
Bug introduced by
There is no parameter named $relation. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
1132
     * @throws MappingException
1133
     * @return \Analogue\ORM\Relationships\BelongsToMany
1134
     */
1135
    public function belongsToMany($entity, $related, $table = null, $foreignKey = null, $otherKey = null)
1136
    {
1137
        // Add the relation to the definition in map
1138
        list(, $caller) = debug_backtrace(false);
1139
        $relation = $caller['function'];
1140
        $this->relatedClasses[$relation] = $related;
1141
1142
        $this->addManyRelation($relation);
1143
        $this->addForeignRelation($relation);
1144
        $this->addPivotRelation($relation);
1145
1146
        // First, we'll need to determine the foreign key and "other key" for the
1147
        // relationship. Once we have determined the keys we'll make the query
1148
        // instances as well as the relationship instances we need for this.
1149
        $foreignKey = $foreignKey ?: $this->getForeignKey();
1150
1151
        $relatedMapper = Manager::getInstance()->mapper($related);
1152
1153
        $relatedMap = $relatedMapper->getEntityMap();
1154
1155
        $otherKey = $otherKey ?: $relatedMap->getForeignKey();
1156
1157
        // If no table name was provided, we can guess it by concatenating the two
1158
        // models using underscores in alphabetical order. The two model names
1159
        // are transformed to snake case from their default CamelCase also.
1160
        if (is_null($table)) {
1161
            $table = $this->joiningTable($relatedMap);
1162
        }
1163
1164
        return new BelongsToMany($relatedMapper, $entity, $table, $foreignKey, $otherKey, $relation);
1165
    }
1166
1167
    /**
1168
     * Define a polymorphic many-to-many relationship.
1169
     *
1170
     * @param  mixed       $entity
1171
     * @param  string      $related
1172
     * @param  string      $name
1173
     * @param  string|null $table
1174
     * @param  string|null $foreignKey
1175
     * @param  string|null $otherKey
1176
     * @param  bool        $inverse
1177
     * @throws MappingException
1178
     * @return \Analogue\ORM\Relationships\MorphToMany
1179
     */
1180
    public function morphToMany($entity, $related, $name, $table = null, $foreignKey = null, $otherKey = null, $inverse = false)
1181
    {
1182
        // Add the relation to the definition in map
1183
        list(, $caller) = debug_backtrace(false);
1184
        $relation = $caller['function'];
1185
        $this->relatedClasses[$relation] = $related;
1186
1187
        $this->addManyRelation($relation);
1188
        $this->addForeignRelation($relation);
1189
        $this->addPivotRelation($relation);
1190
1191
        // First, we will need to determine the foreign key and "other key" for the
1192
        // relationship. Once we have determined the keys we will make the query
1193
        // instances, as well as the relationship instances we need for these.
1194
        $foreignKey = $foreignKey ?: $name . '_id';
1195
1196
        $relatedMapper = Manager::getInstance()->mapper($related);
1197
1198
        $otherKey = $otherKey ?: $relatedMapper->getEntityMap()->getForeignKey();
1199
1200
        $table = $table ?: str_plural($name);
1201
1202
        return new MorphToMany($relatedMapper, $entity, $name, $table, $foreignKey, $otherKey, $caller, $inverse);
1203
    }
1204
1205
    /**
1206
     * Define a polymorphic, inverse many-to-many relationship.
1207
     *
1208
     * @param  mixed       $entity
1209
     * @param  string      $related
1210
     * @param  string      $name
1211
     * @param  string|null $table
1212
     * @param  string|null $foreignKey
1213
     * @param  string|null $otherKey
1214
     * @throws MappingException
1215
     * @return \Analogue\ORM\Relationships\MorphToMany
1216
     */
1217
    public function morphedByMany($entity, $related, $name, $table = null, $foreignKey = null, $otherKey = null)
1218
    {
1219
        // Add the relation to the definition in map
1220
        list(, $caller) = debug_backtrace(false);
1221
        $relation = $caller['function'];
1222
        $this->relatedClasses[$relation] = $related;
1223
1224
        $this->addManyRelation($relation);
1225
        $this->addForeignRelation($relation);
1226
1227
        $foreignKey = $foreignKey ?: $this->getForeignKey();
1228
1229
        // For the inverse of the polymorphic many-to-many relations, we will change
1230
        // the way we determine the foreign and other keys, as it is the opposite
1231
        // of the morph-to-many method since we're figuring out these inverses.
1232
        $otherKey = $otherKey ?: $name . '_id';
1233
1234
        return $this->morphToMany($entity, $related, $name, $table, $foreignKey, $otherKey, true);
1235
    }
1236
1237
    /**
1238
     * Get the joining table name for a many-to-many relation.
1239
     *
1240
     * @param  EntityMap $relatedMap
1241
     * @return string
1242
     */
1243
    public function joiningTable($relatedMap)
1244
    {
1245
        // The joining table name, by convention, is simply the snake cased models
1246
        // sorted alphabetically and concatenated with an underscore, so we can
1247
        // just sort the models and join them together to get the table name.
1248
        $base = $this->getTable();
1249
1250
        $related = $relatedMap->getTable();
1251
1252
        $tables = [$related, $base];
1253
1254
        // Now that we have the model names in an array we can just sort them and
1255
        // use the implode function to join them together with an underscores,
1256
        // which is typically used by convention within the database system.
1257
        sort($tables);
1258
1259
        return strtolower(implode('_', $tables));
1260
    }
1261
1262
    /**
1263
     * Get the polymorphic relationship columns.
1264
     *
1265
     * @param  string $name
1266
     * @param  string $type
1267
     * @param  string $id
1268
     * @return string[]
1269
     */
1270
    protected function getMorphs($name, $type, $id)
1271
    {
1272
        $type = $type ?: $name . '_type';
1273
1274
        $id = $id ?: $name . '_id';
1275
1276
        return [$type, $id];
1277
    }
1278
1279
    /**
1280
     * Get the class name for polymorphic relations.
1281
     *
1282
     * @return string
1283
     */
1284
    public function getMorphClass()
1285
    {
1286
        $morphClass = Manager::getInstance()->getMorphMap($this->getClass());
1287
        return $this->morphClass ?: $morphClass;
1288
    }
1289
1290
    /**
1291
     * Create a new Entity Collection instance.
1292
     *
1293
     * @param  array $entities
1294
     * @return \Analogue\ORM\EntityCollection
1295
     */
1296
    public function newCollection(array $entities = [])
1297
    {
1298
        $collection = new EntityCollection($entities, $this);
1299
        return $collection->keyBy($this->getKeyName());
1300
    }
1301
1302
    /**
1303
     * Process EntityMap parsing at initialization time
1304
     *
1305
     * @return void
1306
     */
1307
    public function initialize()
1308
    {
1309
        $userMethods = $this->getCustomMethods();
1310
1311
        // Parse EntityMap for method based relationship
1312
        if (count($userMethods) > 0) {
1313
            $this->relationships = $this->parseMethodsForRelationship($userMethods);
1314
        }
1315
1316
        // Parse EntityMap for dynamic relationships
1317
        if (count($this->dynamicRelationships) > 0) {
1318
            $this->relationships = $this->relationships + $this->getDynamicRelationships();
1319
        }
1320
    }
1321
1322
    /**
1323
     * Parse every relationships on the EntityMap and sort
1324
     * them by type.
1325
     *
1326
     * @return void
1327
     */
1328
    public function boot()
1329
    {
1330
        if (count($this->relationships > 0)) {
1331
            $this->sortRelationshipsByType();
1332
        }
1333
    }
1334
1335
    /**
1336
     * Get Methods that has been added in the child class.
1337
     *
1338
     * @return array
1339
     */
1340
    protected function getCustomMethods()
1341
    {
1342
        $mapMethods = get_class_methods($this);
1343
1344
        $parentsMethods = get_class_methods('Analogue\ORM\EntityMap');
1345
1346
        return array_diff($mapMethods, $parentsMethods);
1347
    }
1348
1349
    /**
1350
     * Parse user's class methods for relationships
1351
     *
1352
     * @param  array $customMethods
1353
     * @return array
1354
     */
1355
    protected function parseMethodsForRelationship(array $customMethods)
1356
    {
1357
        $relationships = [];
1358
1359
        $class = new ReflectionClass(get_class($this));
1360
1361
        // Get the mapped Entity class, as we will detect relationships
1362
        // methods by testing that the first argument is type-hinted to
1363
        // the same class as the mapped Entity.
1364
        $entityClass = $this->getClass();
1365
1366
        foreach ($customMethods as $methodName) {
1367
            $method = $class->getMethod($methodName);
1368
1369
            if ($method->getNumberOfParameters() > 0) {
1370
                $params = $method->getParameters();
1371
1372
                if ($params[0]->getClass() && ($params[0]->getClass()->name == $entityClass || is_subclass_of($entityClass, $params[0]->getClass()->name))) {
0 ignored issues
show
Bug introduced by
Due to PHP Bug #53727, is_subclass_of might return inconsistent results on some PHP versions if $params[0]->getClass()->name can be an interface. If so, you could instead use ReflectionClass::implementsInterface.
Loading history...
1373
                    $relationships[] = $methodName;
1374
                }
1375
            }
1376
        }
1377
1378
        return $relationships;
1379
    }
1380
1381
    /**
1382
     * Sort Relationships methods by type
1383
     * 
1384
     * TODO : replace this by direclty setting these value
1385
     * in the corresponding methods, so we won't need
1386
     * the correpondancy tabble
1387
     * 
1388
     * @return void
1389
     */
1390
    protected function sortRelationshipsByType()
1391
    {
1392
        $entityClass = $this->getClass();
1393
1394
        // Instantiate a dummy entity which we will pass to relationship methods.
1395
        $entity = unserialize(sprintf('O:%d:"%s":0:{}', strlen($entityClass), $entityClass));
1396
1397
        foreach ($this->relationships as $relation) {
1398
            $this->$relation($entity);
1399
        }
1400
    }
1401
1402
    /**
1403
     * Override this method for custom entity instantiation
1404
     *
1405
     * @return null
1406
     */
1407
    public function activator()
1408
    {
1409
        return null;
1410
    }
1411
1412
    /**
1413
     * Magic call to dynamic relationships
1414
     *
1415
     * @param  string $method
1416
     * @param  array  $parameters
1417
     * @throws Exception
1418
     * @return mixed
1419
     */
1420
    public function __call($method, $parameters)
1421
    {
1422
        if (!array_key_exists($method, $this->dynamicRelationships)) {
1423
            throw new Exception(get_class($this) . " has no method $method");
1424
        }
1425
1426
        // Add $this to parameters so the closure can call relationship method on the map.
1427
        $parameters[] = $this;
1428
1429
        return  call_user_func_array([$this->dynamicRelationships[$method], $parameters]);
1430
    }
1431
}
1432