Completed
Branch master (2d39e9)
by Rémi
11:28
created

EntityMap::isPolymorphic()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 1
dl 0
loc 4
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace Analogue\ORM;
4
5
use Analogue\ORM\Exceptions\MappingException;
6
use Analogue\ORM\Relationships\BelongsTo;
7
use Analogue\ORM\Relationships\BelongsToMany;
8
use Analogue\ORM\Relationships\HasMany;
9
use Analogue\ORM\Relationships\HasManyThrough;
10
use Analogue\ORM\Relationships\HasOne;
11
use Analogue\ORM\Relationships\MorphMany;
12
use Analogue\ORM\Relationships\MorphOne;
13
use Analogue\ORM\Relationships\MorphTo;
14
use Analogue\ORM\Relationships\MorphToMany;
15
use Analogue\ORM\System\Manager;
16
use Analogue\ORM\System\Wrappers\Factory;
17
use Exception;
18
use ReflectionClass;
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
     * Polymorphic relationships.
145
     *
146
     * @var array
147
     */
148
    private $polymorphicRelations = [];
149
150
    /**
151
     * Dynamic relationships.
152
     *
153
     * @var array
154
     */
155
    private $dynamicRelationships = [];
156
157
    /**
158
     * Targetted class for the relationship method. value is set to `null` for
159
     * polymorphic relations.
160
     *
161
     * @var array
162
     */
163
    private $relatedClasses = [];
164
165
    /**
166
     * Some relation methods like embedded objects, or HasOne and MorphOne,
167
     * will never have a proxy loaded on them.
168
     *
169
     * @var array
170
     */
171
    private $nonProxyRelationships = [];
172
173
    /**
174
     * The number of models to return for pagination.
175
     *
176
     * @var int
177
     */
178
    protected $perPage = 15;
179
180
    /**
181
     * The relations to eager load on every query.
182
     *
183
     * @var array
184
     */
185
    protected $with = [];
186
187
    /**
188
     * The class name to be used in polymorphic relations.
189
     *
190
     * @var string
191
     */
192
    protected $morphClass;
193
194
    /**
195
     * Sequence name, to be used with postgreSql
196
     * defaults to %table_name%_id_seq.
197
     *
198
     * @var string|null
199
     */
200
    protected $sequence = null;
201
202
    /**
203
     * Indicates if the entity should be timestamped.
204
     *
205
     * @var bool
206
     */
207
    public $timestamps = false;
208
209
    /**
210
     * The name of the "created at" column.
211
     *
212
     * @var string
213
     */
214
    protected $createdAtColumn = 'created_at';
215
216
    /**
217
     * The name of the "updated at" column.
218
     *
219
     * @var string
220
     */
221
    protected $updatedAtColumn = 'updated_at';
222
223
    /**
224
     * Indicates if the entity uses softdeletes.
225
     *
226
     * @var bool
227
     */
228
    public $softDeletes = false;
229
230
    /**
231
     * The name of the "deleted at" column.
232
     *
233
     * @var string
234
     */
235
    protected $deletedAtColumn = 'deleted_at';
236
237
    /**
238
     * The date format to use with the current database connection.
239
     *
240
     * @var string
241
     */
242
    protected $dateFormat;
243
244
    /**
245
     * Set this property to true if the entity should be instantiated
246
     * using the IoC Container.
247
     *
248
     * @var bool
249
     */
250
    protected $dependencyInjection = false;
251
252
    /**
253
     * Set the usage of inheritance, possible values are :
254
     * "single_table"
255
     * null.
256
     *
257
     * @var string | null
258
     */
259
    protected $inheritanceType = null;
260
261
    /**
262
     * Discriminator column name.
263
     *
264
     * @var string
265
     */
266
    protected $discriminatorColumn = 'type';
267
268
    /**
269
     * Allow using a string to define which entity type should be instantiated.
270
     * If not set, analogue will uses entity's FQDN.
271
     *
272
     * @var array
273
     */
274
    protected $discriminatorColumnMap = [];
275
276
    /**
277
     * Return Domain class attributes, useful when mapping to a Plain PHP Object.
278
     *
279
     * @return array
280
     */
281
    public function getAttributes() : array
282
    {
283
        return $this->attributes;
284
    }
285
286
    /**
287
     * Set the domain class attributes.
288
     *
289
     * @param array $attributeNames
290
     */
291
    public function setAttributes(array $attributeNames)
292
    {
293
        $this->attributes = $attributeNames;
294
    }
295
296
    /**
297
     * Return true if the Entity has an 'attributes' array property.
298
     *
299
     * @return bool
300
     */
301
    public function usesAttributesArray() : bool
302
    {
303
        return $this->arrayName === null ? false : true;
304
    }
305
306
    /**
307
     * Return the name of the Entity's attributes property.
308
     *
309
     * @return string|null
310
     */
311
    public function getAttributesArrayName()
312
    {
313
        return $this->arrayName;
314
    }
315
316
    /**
317
     * Get all the attribute names for the class, including relationships, embeddables and primary key.
318
     *
319
     * @return array
320
     */
321
    public function getCompiledAttributes() : array
322
    {
323
        $key = $this->getKeyName();
324
325
        $embeddables = array_keys($this->getEmbeddables());
326
327
        $relationships = $this->getRelationships();
328
329
        $attributes = $this->getAttributes();
330
331
        return array_merge([$key], $embeddables, $relationships, $attributes);
332
    }
333
334
    /**
335
     * Set the date format to use with the current database connection.
336
     *
337
     * @param string $format
338
     */
339
    public function setDateFormat($format)
340
    {
341
        $this->dateFormat = $format;
342
    }
343
344
    /**
345
     * Get the date format to use with the current database connection.
346
     *
347
     *  @return string
348
     */
349
    public function getDateFormat() : string
350
    {
351
        return $this->dateFormat;
352
    }
353
354
    /**
355
     * Set the Driver for this mapping.
356
     *
357
     * @param string $driver
358
     */
359
    public function setDriver($driver)
360
    {
361
        $this->driver = $driver;
362
    }
363
364
    /**
365
     * Get the Driver for this mapping.
366
     *
367
     * @return string
368
     */
369
    public function getDriver() : string
370
    {
371
        return $this->driver;
372
    }
373
374
    /**
375
     * Set the db connection to use on the table.
376
     *
377
     * @param $connection
378
     */
379
    public function setConnection($connection)
380
    {
381
        $this->connection = $connection;
382
    }
383
384
    /**
385
     * Get the Database connection the Entity is stored on.
386
     *
387
     * @return string | null
388
     */
389
    public function getConnection()
390
    {
391
        return $this->connection;
392
    }
393
394
    /**
395
     * Get the table associated with the entity.
396
     *
397
     * @return string
398
     */
399
    public function getTable() : string
400
    {
401
        if (!is_null($this->table)) {
402
            return $this->table;
403
        }
404
405
        return str_replace('\\', '', snake_case(str_plural(class_basename($this->getClass()))));
406
    }
407
408
    /**
409
     * Set the database table name.
410
     *
411
     * @param string $table
412
     */
413
    public function setTable($table)
414
    {
415
        $this->table = $table;
416
    }
417
418
    /**
419
     * Get the pgSql sequence name.
420
     *
421
     * @return string
422
     */
423
    public function getSequence()
424
    {
425
        if (!is_null($this->sequence)) {
426
            return $this->sequence;
427
        } else {
428
            return $this->getTable().'_id_seq';
429
        }
430
    }
431
432
    /**
433
     * Get the custom entity class.
434
     *
435
     * @return string namespaced class name
436
     */
437
    public function getClass() : string
438
    {
439
        return isset($this->class) ? $this->class : null;
440
    }
441
442
    /**
443
     * Set the custom entity class.
444
     *
445
     * @param string $class namespaced class name
446
     */
447
    public function setClass($class)
448
    {
449
        $this->class = $class;
450
    }
451
452
    /**
453
     * Get the embedded Value Objects.
454
     *
455
     * @return array
456
     */
457
    public function getEmbeddables() : array
458
    {
459
        return $this->embeddables;
460
    }
461
462
    /**
463
     * Return attributes that should be mapped to class properties.
464
     *
465
     * @return array
466
     */
467
    public function getProperties() : array
468
    {
469
        return $this->properties;
470
    }
471
472
    /**
473
     * Return the array property in which will be mapped all attributes
474
     * that are not mapped to class properties.
475
     *
476
     * @return string
477
     */
478
    public function getAttributesPropertyName() : string
479
    {
480
    }
481
482
    /**
483
     * Set the embedded Value Objects.
484
     *
485
     * @param array $embeddables
486
     */
487
    public function setEmbeddables(array $embeddables)
488
    {
489
        $this->embeddables = $embeddables;
490
    }
491
492
    /**
493
     * Get the relationships to map on a custom domain
494
     * class.
495
     *
496
     * @return array
497
     */
498
    public function getRelationships() : array
499
    {
500
        return $this->relationships;
501
    }
502
503
    /**
504
     * Get the relationships that will not have a proxy
505
     * set on them.
506
     *
507
     * @return array
508
     */
509
    public function getRelationshipsWithoutProxy() : array
510
    {
511
        return $this->nonProxyRelationships;
512
    }
513
514
    /**
515
     * Relationships of the Entity type.
516
     *
517
     * @return array
518
     */
519
    public function getSingleRelationships() : array
520
    {
521
        return $this->singleRelations;
522
    }
523
524
    /**
525
     * Relationships of type Collection.
526
     *
527
     * @return array
528
     */
529
    public function getManyRelationships() : array
530
    {
531
        return $this->manyRelations;
532
    }
533
534
    /**
535
     * Relationships with foreign key in the mapped entity record.
536
     *
537
     * @return array
538
     */
539
    public function getLocalRelationships() : array
540
    {
541
        return $this->localRelations;
542
    }
543
544
    /**
545
     * Return the local keys associated to the relationship.
546
     *
547
     * @param string $relation
548
     *
549
     * @return string | array | null
550
     */
551
    public function getLocalKeys($relation)
552
    {
553
        return isset($this->localForeignKeys[$relation]) ? $this->localForeignKeys[$relation] : null;
554
    }
555
556
    /**
557
     * Relationships with foreign key in the related Entity record.
558
     *
559
     * @return array
560
     */
561
    public function getForeignRelationships() : array
562
    {
563
        return $this->foreignRelations;
564
    }
565
566
    /**
567
     * Relationships which keys are stored in a pivot record.
568
     *
569
     * @return array
570
     */
571
    public function getPivotRelationships() : array
572
    {
573
        return $this->pivotRelations;
574
    }
575
576
    /**
577
     * Return true if the relationship method is polymorphic.
578
     *
579
     * @param string $relation
580
     *
581
     * @return bool
582
     */
583
    public function isPolymorphic($relation) : bool
584
    {
585
        return in_array($relation, $this->polymorphicRelations);
586
    }
587
588
    /**
589
     * Get the targetted type for a relationship. Return null if polymorphic.
590
     *
591
     * @param string $relation
592
     *
593
     * @return string | null
594
     */
595
    public function getTargettedClass($relation)
596
    {
597
        if (!array_key_exists($relation, $this->relatedClasses)) {
598
            return;
599
        }
600
601
        return $this->relatedClasses[$relation];
602
    }
603
604
    /**
605
     * Add a Dynamic Relationship method at runtime. This has to be done
606
     * by hooking the 'initializing' event, before entityMap is initialized.
607
     *
608
     * @param string   $name         Relation name
609
     * @param \Closure $relationship
610
     *
611
     * @return void
612
     */
613
    public function addRelationshipMethod($name, \Closure $relationship)
614
    {
615
        $this->dynamicRelationships[$name] = $relationship;
616
    }
617
618
    /**
619
     * Get the dynamic relationship method names.
620
     *
621
     * @return array
622
     */
623
    public function getDynamicRelationships() : array
624
    {
625
        return array_keys($this->dynamicRelationships);
626
    }
627
628
    /**
629
     * Get the relationships that have to be eager loaded
630
     * on each request.
631
     *
632
     * @return array
633
     */
634
    public function getEagerloadedRelationships() : array
635
    {
636
        return $this->with;
637
    }
638
639
    /**
640
     * Get the primary key attribute for the entity.
641
     *
642
     * @return string
643
     */
644
    public function getKeyName() : string
645
    {
646
        return $this->primaryKey;
647
    }
648
649
    /**
650
     * Set the primary key for the entity.
651
     *
652
     * @param $key
653
     *
654
     * @return void
655
     */
656
    public function setKeyName($key)
657
    {
658
        $this->primaryKey = $key;
659
    }
660
661
    /**
662
     * Get the table qualified key name.
663
     *
664
     * @return string
665
     */
666
    public function getQualifiedKeyName() : string
667
    {
668
        return $this->getTable().'.'.$this->getKeyName();
669
    }
670
671
    /**
672
     * Get the number of models to return per page.
673
     *
674
     * @return int
675
     */
676
    public function getPerPage() : int
677
    {
678
        return $this->perPage;
679
    }
680
681
    /**
682
     * Set the number of models to return per page.
683
     *
684
     * @param int $perPage
685
     *
686
     * @return void
687
     */
688
    public function setPerPage($perPage)
689
    {
690
        $this->perPage = $perPage;
691
    }
692
693
    /**
694
     * Determine if the entity uses get.
695
     *
696
     * @return bool
697
     */
698
    public function usesTimestamps() : bool
699
    {
700
        return $this->timestamps;
701
    }
702
703
    /**
704
     * Determine if the entity uses soft deletes.
705
     *
706
     * @return bool
707
     */
708
    public function usesSoftDeletes() : bool
709
    {
710
        return $this->softDeletes;
711
    }
712
713
    /**
714
     * Get the 'created_at' column name.
715
     *
716
     * @return string
717
     */
718
    public function getCreatedAtColumn() : string
719
    {
720
        return $this->createdAtColumn;
721
    }
722
723
    /**
724
     * Get the 'updated_at' column name.
725
     *
726
     * @return string
727
     */
728
    public function getUpdatedAtColumn() : string
729
    {
730
        return $this->updatedAtColumn;
731
    }
732
733
    /**
734
     * Get the deleted_at column.
735
     *
736
     * @return string
737
     */
738
    public function getQualifiedDeletedAtColumn() : string
739
    {
740
        return $this->deletedAtColumn;
741
    }
742
743
    /**
744
     * Get the default foreign key name for the model.
745
     *
746
     * @return string
747
     */
748
    public function getForeignKey() : string
749
    {
750
        return snake_case(class_basename($this->getClass())).'_id';
751
    }
752
753
    /**
754
     * Return the inheritance type used by the entity.
755
     *
756
     * @return string|null
757
     */
758
    public function getInheritanceType()
759
    {
760
        return $this->inheritanceType;
761
    }
762
763
    /**
764
     * Return the discriminator column name on the entity that's
765
     * used for table inheritance.
766
     *
767
     * @return string
768
     */
769
    public function getDiscriminatorColumn() : string
770
    {
771
        return $this->discriminatorColumn;
772
    }
773
774
    /**
775
     * Return the mapping of discriminator column values to
776
     * entity class names that are used for table inheritance.
777
     *
778
     * @return array
779
     */
780
    public function getDiscriminatorColumnMap() : array
781
    {
782
        return $this->discriminatorColumnMap;
783
    }
784
785
    /**
786
     * Return true if the entity should be instanciated using
787
     * the IoC Container.
788
     *
789
     * @return bool
790
     */
791
    public function useDependencyInjection() : bool
792
    {
793
        return $this->dependencyInjection;
794
    }
795
796
    /**
797
     * Add a single relation method name once.
798
     *
799
     * @param string $relation
800
     */
801
    protected function addSingleRelation($relation)
802
    {
803
        if (!in_array($relation, $this->singleRelations)) {
804
            $this->singleRelations[] = $relation;
805
        }
806
    }
807
808
    /**
809
     * Add a foreign relation method name once.
810
     *
811
     * @param string $relation
812
     */
813
    protected function addForeignRelation($relation)
814
    {
815
        if (!in_array($relation, $this->foreignRelations)) {
816
            $this->foreignRelations[] = $relation;
817
        }
818
    }
819
820
    /**
821
     * Add a polymorphic relation method name once.
822
     *
823
     * @param string $relation
824
     */
825
    protected function addPolymorphicRelation($relation)
826
    {
827
        if (!in_array($relation, $this->polymorphicRelations)) {
828
            $this->polymorphicRelations[] = $relation;
829
        }
830
    }
831
832
    /**
833
     * Add a non proxy relation method name once.
834
     *
835
     * @param string $relation
836
     */
837
    protected function addNonProxyRelation($relation)
838
    {
839
        if (!in_array($relation, $this->nonProxyRelationships)) {
840
            $this->nonProxyRelationships[] = $relation;
841
        }
842
    }
843
844
    /**
845
     * Add a local relation method name once.
846
     *
847
     * @param string $relation
848
     */
849
    protected function addLocalRelation($relation)
850
    {
851
        if (!in_array($relation, $this->localRelations)) {
852
            $this->localRelations[] = $relation;
853
        }
854
    }
855
856
    /**
857
     * Add a many relation method name once.
858
     *
859
     * @param string $relation
860
     */
861
    protected function addManyRelation($relation)
862
    {
863
        if (!in_array($relation, $this->manyRelations)) {
864
            $this->manyRelations[] = $relation;
865
        }
866
    }
867
868
    /**
869
     * Add a pivot relation method name once.
870
     *
871
     * @param string $relation
872
     */
873
    protected function addPivotRelation($relation)
874
    {
875
        if (!in_array($relation, $this->pivotRelations)) {
876
            $this->pivotRelations[] = $relation;
877
        }
878
    }
879
880
    /**
881
     * Define a one-to-one relationship.
882
     *
883
     * @param mixed  $entity
884
     * @param string $related    entity class
885
     * @param string $foreignKey
886
     * @param string $localKey
887
     *
888
     * @throws MappingException
889
     *
890
     * @return \Analogue\ORM\Relationships\HasOne
891
     */
892
    public function hasOne($entity, $related, $foreignKey = null, $localKey = null)
893
    {
894
        $foreignKey = $foreignKey ?: $this->getForeignKey();
895
896
        $relatedMapper = Manager::getInstance()->mapper($related);
897
898
        $relatedMap = $relatedMapper->getEntityMap();
899
900
        $localKey = $localKey ?: $this->getKeyName();
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 HasOne($relatedMapper, $entity, $relatedMap->getTable().'.'.$foreignKey, $localKey);
918
    }
919
920
    /**
921
     * Define a polymorphic one-to-one relationship.
922
     *
923
     * @param mixed       $entity
924
     * @param string      $related
925
     * @param string      $name
926
     * @param string|null $type
927
     * @param string|null $id
928
     * @param string|null $localKey
929
     *
930
     * @throws MappingException
931
     *
932
     * @return \Analogue\ORM\Relationships\MorphOne
933
     */
934
    public function morphOne($entity, $related, $name, $type = null, $id = null, $localKey = null)
935
    {
936
        list($type, $id) = $this->getMorphs($name, $type, $id);
937
938
        $localKey = $localKey ?: $this->getKeyName();
939
940
        $relatedMapper = Manager::getInstance()->mapper($related);
941
942
        $table = $relatedMapper->getEntityMap()->getTable();
943
944
        // Add the relation to the definition in map
945
        list(, $caller) = debug_backtrace(false);
946
        $relation = $caller['function'];
947
        $this->relatedClasses[$relation] = $related;
948
949
        $this->addSingleRelation($relation);
950
        $this->addForeignRelation($relation);
951
        $this->addNonProxyRelation($relation);
952
953
        // This relationship will always be eager loaded, as proxying it would
954
        // mean having an object that doesn't actually exists.
955
        if (!in_array($relation, $this->with)) {
956
            $this->with[] = $relation;
957
        }
958
959
        return new MorphOne($relatedMapper, $entity, $table.'.'.$type, $table.'.'.$id, $localKey);
960
    }
961
962
    /**
963
     * Define an inverse one-to-one or many relationship.
964
     *
965
     * @param mixed       $entity
966
     * @param string      $related
967
     * @param string|null $foreignKey
968
     * @param string|null $otherKey
969
     * @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...
970
     *
971
     * @throws MappingException
972
     *
973
     * @return \Analogue\ORM\Relationships\BelongsTo
974
     */
975
    public function belongsTo($entity, $related, $foreignKey = null, $otherKey = null)
976
    {
977
        // Add the relation to the definition in map
978
        list(, $caller) = debug_backtrace(false);
979
        $relation = $caller['function'];
980
        $this->relatedClasses[$relation] = $related;
981
982
        $this->addSingleRelation($relation);
983
        $this->addLocalRelation($relation);
984
985
        // If no foreign key was supplied, we can use a backtrace to guess the proper
986
        // foreign key name by using the name of the relationship function, which
987
        // when combined with an "_id" should conventionally match the columns.
988
        if (is_null($foreignKey)) {
989
            $foreignKey = snake_case($relation).'_id';
990
        }
991
992
        $this->localForeignKeys[$relation] = $foreignKey;
993
994
        $relatedMapper = Manager::getInstance()->mapper($related);
995
996
        $otherKey = $otherKey ?: $relatedMapper->getEntityMap()->getKeyName();
997
998
        return new BelongsTo($relatedMapper, $entity, $foreignKey, $otherKey, $relation);
999
    }
1000
1001
    /**
1002
     * Define a polymorphic, inverse one-to-one or many relationship.
1003
     *
1004
     * @param mixed       $entity
1005
     * @param string|null $name
1006
     * @param string|null $type
1007
     * @param string|null $id
1008
     *
1009
     * @throws MappingException
1010
     *
1011
     * @return \Analogue\ORM\Relationships\MorphTo
1012
     */
1013
    public function morphTo($entity, $name = null, $type = null, $id = null)
1014
    {
1015
        // If no name is provided, we will use the backtrace to get the function name
1016
        // since that is most likely the name of the polymorphic interface. We can
1017
        // use that to get both the class and foreign key that will be utilized.
1018
        if (is_null($name)) {
1019
            list(, $caller) = debug_backtrace(false);
1020
1021
            $name = snake_case($caller['function']);
1022
        }
1023
        $this->addSingleRelation($name);
1024
        $this->addLocalRelation($name);
1025
        $this->addPolymorphicRelation($name);
1026
1027
        $this->relatedClass[$name] = 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...
1028
1029
        list($type, $id) = $this->getMorphs($name, $type, $id);
1030
1031
        // Store the foreign key in the entity map.
1032
        // We might want to store the (key, type) as we might need it
1033
        // to build a MorphTo proxy
1034
        $this->localForeignKeys[$name] = [
1035
            'id'   => $id,
1036
            'type' => $type,
1037
        ];
1038
1039
        $mapper = Manager::getInstance()->mapper(get_class($entity));
1040
1041
        // If the type value is null it is probably safe to assume we're eager loading
1042
        // the relationship. When that is the case we will pass in a dummy query as
1043
        // there are multiple types in the morph and we can't use single queries.
1044
        $factory = new Factory();
1045
        $wrapper = $factory->make($entity);
1046
1047
        if (is_null($class = $wrapper->getEntityAttribute($type))) {
1048
            return new MorphTo(
1049
                $mapper, $entity, $id, null, $type, $name
1050
            );
1051
        }
1052
1053
        // If we are not eager loading the relationship we will essentially treat this
1054
        // as a belongs-to style relationship since morph-to extends that class and
1055
        // we will pass in the appropriate values so that it behaves as expected.
1056
        else {
1057
            $class = Manager::getInstance()->getInverseMorphMap($class);
1058
            $relatedMapper = Manager::getInstance()->mapper($class);
1059
1060
            $foreignKey = $relatedMapper->getEntityMap()->getKeyName();
1061
1062
            return new MorphTo(
1063
                $relatedMapper, $entity, $id, $foreignKey, $type, $name
1064
            );
1065
        }
1066
    }
1067
1068
    /**
1069
     * Define a one-to-many relationship.
1070
     *
1071
     * @param mixed       $entity
1072
     * @param string      $related
1073
     * @param string|null $foreignKey
1074
     * @param string|null $localKey
1075
     *
1076
     * @throws MappingException
1077
     *
1078
     * @return \Analogue\ORM\Relationships\HasMany
1079
     */
1080
    public function hasMany($entity, $related, $foreignKey = null, $localKey = null)
1081
    {
1082
        $foreignKey = $foreignKey ?: $this->getForeignKey();
1083
1084
        $relatedMapper = Manager::getInstance()->mapper($related);
1085
1086
        $table = $relatedMapper->getEntityMap()->getTable().'.'.$foreignKey;
1087
1088
        $localKey = $localKey ?: $this->getKeyName();
1089
1090
        // Add the relation to the definition in map
1091
        list(, $caller) = debug_backtrace(false);
1092
        $relation = $caller['function'];
1093
        $this->relatedClasses[$relation] = $related;
1094
1095
        $this->addManyRelation($relation);
1096
        $this->addForeignRelation($relation);
1097
1098
        return new HasMany($relatedMapper, $entity, $table, $localKey);
1099
    }
1100
1101
    /**
1102
     * Define a has-many-through relationship.
1103
     *
1104
     * @param mixed       $entity
1105
     * @param string      $related
1106
     * @param string      $through
1107
     * @param string|null $firstKey
1108
     * @param string|null $secondKey
1109
     *
1110
     * @throws MappingException
1111
     *
1112
     * @return \Analogue\ORM\Relationships\HasManyThrough
1113
     */
1114
    public function hasManyThrough($entity, $related, $through, $firstKey = null, $secondKey = null)
1115
    {
1116
        $relatedMapper = Manager::getInstance()->mapper($related);
1117
1118
        $throughMapper = Manager::getInstance()->mapper($through);
1119
1120
        $firstKey = $firstKey ?: $this->getForeignKey();
1121
1122
        $throughMap = $throughMapper->getEntityMap();
1123
1124
        $secondKey = $secondKey ?: $throughMap->getForeignKey();
1125
1126
        // Add the relation to the definition in map
1127
        list(, $caller) = debug_backtrace(false);
1128
        $relation = $caller['function'];
1129
        $this->relatedClasses[$relation] = $related;
1130
1131
        $this->addManyRelation($relation);
1132
        $this->addForeignRelation($relation);
1133
1134
        return new HasManyThrough($relatedMapper, $entity, $throughMap, $firstKey, $secondKey);
1135
    }
1136
1137
    /**
1138
     * Define a polymorphic one-to-many relationship.
1139
     *
1140
     * @param mixed       $entity
1141
     * @param string      $related
1142
     * @param string      $name
1143
     * @param string|null $type
1144
     * @param string|null $id
1145
     * @param string|null $localKey
1146
     *
1147
     * @return \Analogue\ORM\Relationships\MorphMany
1148
     */
1149
    public function morphMany($entity, $related, $name, $type = null, $id = null, $localKey = null)
1150
    {
1151
        // Here we will gather up the morph type and ID for the relationship so that we
1152
        // can properly query the intermediate table of a relation. Finally, we will
1153
        // get the table and create the relationship instances for the developers.
1154
        list($type, $id) = $this->getMorphs($name, $type, $id);
1155
1156
        $relatedMapper = Manager::getInstance()->mapper($related);
1157
1158
        $table = $relatedMapper->getEntityMap()->getTable();
1159
1160
        $localKey = $localKey ?: $this->getKeyName();
1161
1162
        // Add the relation to the definition in map
1163
        list(, $caller) = debug_backtrace(false);
1164
        $relation = $caller['function'];
1165
        $this->relatedClasses[$relation] = $related;
1166
1167
        $this->addManyRelation($relation);
1168
        $this->addForeignRelation($relation);
1169
1170
        return new MorphMany($relatedMapper, $entity, $table.'.'.$type, $table.'.'.$id, $localKey);
1171
    }
1172
1173
    /**
1174
     * Define a many-to-many relationship.
1175
     *
1176
     * @param mixed       $entity
1177
     * @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...
1178
     * @param string|null $table
1179
     * @param string|null $foreignKey
1180
     * @param string|null $otherKey
1181
     * @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...
1182
     *
1183
     * @throws MappingException
1184
     *
1185
     * @return \Analogue\ORM\Relationships\BelongsToMany
1186
     */
1187
    public function belongsToMany($entity, $related, $table = null, $foreignKey = null, $otherKey = null)
1188
    {
1189
        // Add the relation to the definition in map
1190
        list(, $caller) = debug_backtrace(false);
1191
        $relation = $caller['function'];
1192
        $this->relatedClasses[$relation] = $related;
1193
1194
        $this->addManyRelation($relation);
1195
        $this->addForeignRelation($relation);
1196
        $this->addPivotRelation($relation);
1197
1198
        // First, we'll need to determine the foreign key and "other key" for the
1199
        // relationship. Once we have determined the keys we'll make the query
1200
        // instances as well as the relationship instances we need for this.
1201
        $foreignKey = $foreignKey ?: $this->getForeignKey();
1202
1203
        $relatedMapper = Manager::getInstance()->mapper($related);
1204
1205
        $relatedMap = $relatedMapper->getEntityMap();
1206
1207
        $otherKey = $otherKey ?: $relatedMap->getForeignKey();
1208
1209
        // If no table name was provided, we can guess it by concatenating the two
1210
        // models using underscores in alphabetical order. The two model names
1211
        // are transformed to snake case from their default CamelCase also.
1212
        if (is_null($table)) {
1213
            $table = $this->joiningTable($relatedMap);
1214
        }
1215
1216
        return new BelongsToMany($relatedMapper, $entity, $table, $foreignKey, $otherKey, $relation);
1217
    }
1218
1219
    /**
1220
     * Define a polymorphic many-to-many relationship.
1221
     *
1222
     * @param mixed       $entity
1223
     * @param string      $related
1224
     * @param string      $name
1225
     * @param string|null $table
1226
     * @param string|null $foreignKey
1227
     * @param string|null $otherKey
1228
     * @param bool        $inverse
1229
     *
1230
     * @throws MappingException
1231
     *
1232
     * @return \Analogue\ORM\Relationships\MorphToMany
1233
     */
1234
    public function morphToMany($entity, $related, $name, $table = null, $foreignKey = null, $otherKey = null, $inverse = false)
1235
    {
1236
        // Add the relation to the definition in map
1237
        list(, $caller) = debug_backtrace(false);
1238
        $relation = $caller['function'];
1239
        $this->relatedClasses[$relation] = $related;
1240
1241
        $this->addManyRelation($relation);
1242
        $this->addForeignRelation($relation);
1243
        $this->addPivotRelation($relation);
1244
1245
        // First, we will need to determine the foreign key and "other key" for the
1246
        // relationship. Once we have determined the keys we will make the query
1247
        // instances, as well as the relationship instances we need for these.
1248
        $foreignKey = $foreignKey ?: $name.'_id';
1249
1250
        $relatedMapper = Manager::getInstance()->mapper($related);
1251
1252
        $otherKey = $otherKey ?: $relatedMapper->getEntityMap()->getForeignKey();
1253
1254
        $table = $table ?: str_plural($name);
1255
1256
        return new MorphToMany($relatedMapper, $entity, $name, $table, $foreignKey, $otherKey, $caller, $inverse);
1257
    }
1258
1259
    /**
1260
     * Define a polymorphic, inverse many-to-many relationship.
1261
     *
1262
     * @param mixed       $entity
1263
     * @param string      $related
1264
     * @param string      $name
1265
     * @param string|null $table
1266
     * @param string|null $foreignKey
1267
     * @param string|null $otherKey
1268
     *
1269
     * @throws MappingException
1270
     *
1271
     * @return \Analogue\ORM\Relationships\MorphToMany
1272
     */
1273
    public function morphedByMany($entity, $related, $name, $table = null, $foreignKey = null, $otherKey = null)
1274
    {
1275
        // Add the relation to the definition in map
1276
        list(, $caller) = debug_backtrace(false);
1277
        $relation = $caller['function'];
1278
        $this->relatedClasses[$relation] = $related;
1279
1280
        $this->addManyRelation($relation);
1281
        $this->addForeignRelation($relation);
1282
1283
        $foreignKey = $foreignKey ?: $this->getForeignKey();
1284
1285
        // For the inverse of the polymorphic many-to-many relations, we will change
1286
        // the way we determine the foreign and other keys, as it is the opposite
1287
        // of the morph-to-many method since we're figuring out these inverses.
1288
        $otherKey = $otherKey ?: $name.'_id';
1289
1290
        return $this->morphToMany($entity, $related, $name, $table, $foreignKey, $otherKey, true);
1291
    }
1292
1293
    /**
1294
     * Get the joining table name for a many-to-many relation.
1295
     *
1296
     * @param EntityMap $relatedMap
1297
     *
1298
     * @return string
1299
     */
1300
    public function joiningTable($relatedMap)
1301
    {
1302
        // The joining table name, by convention, is simply the snake cased models
1303
        // sorted alphabetically and concatenated with an underscore, so we can
1304
        // just sort the models and join them together to get the table name.
1305
        $base = $this->getTable();
1306
1307
        $related = $relatedMap->getTable();
1308
1309
        $tables = [$related, $base];
1310
1311
        // Now that we have the model names in an array we can just sort them and
1312
        // use the implode function to join them together with an underscores,
1313
        // which is typically used by convention within the database system.
1314
        sort($tables);
1315
1316
        return strtolower(implode('_', $tables));
1317
    }
1318
1319
    /**
1320
     * Get the polymorphic relationship columns.
1321
     *
1322
     * @param string $name
1323
     * @param string $type
1324
     * @param string $id
1325
     *
1326
     * @return string[]
1327
     */
1328
    protected function getMorphs($name, $type, $id)
1329
    {
1330
        $type = $type ?: $name.'_type';
1331
1332
        $id = $id ?: $name.'_id';
1333
1334
        return [$type, $id];
1335
    }
1336
1337
    /**
1338
     * Get the class name for polymorphic relations.
1339
     *
1340
     * @return string
1341
     */
1342
    public function getMorphClass()
1343
    {
1344
        $morphClass = Manager::getInstance()->getMorphMap($this->getClass());
1345
1346
        return $this->morphClass ?: $morphClass;
1347
    }
1348
1349
    /**
1350
     * Create a new Entity Collection instance.
1351
     *
1352
     * @param array $entities
1353
     *
1354
     * @return \Analogue\ORM\EntityCollection
1355
     */
1356
    public function newCollection(array $entities = [])
1357
    {
1358
        $collection = new EntityCollection($entities, $this);
1359
1360
        return $collection->keyBy($this->getKeyName());
1361
    }
1362
1363
    /**
1364
     * Process EntityMap parsing at initialization time.
1365
     *
1366
     * @return void
1367
     */
1368
    public function initialize()
1369
    {
1370
        $userMethods = $this->getCustomMethods();
1371
1372
        // Parse EntityMap for method based relationship
1373
        if (count($userMethods) > 0) {
1374
            $this->relationships = $this->parseMethodsForRelationship($userMethods);
1375
        }
1376
1377
        // Parse EntityMap for dynamic relationships
1378
        if (count($this->dynamicRelationships) > 0) {
1379
            $this->relationships = $this->relationships + $this->getDynamicRelationships();
1380
        }
1381
    }
1382
1383
    /**
1384
     * Parse every relationships on the EntityMap and sort
1385
     * them by type.
1386
     *
1387
     * @return void
1388
     */
1389
    public function boot()
1390
    {
1391
        if (count($this->relationships > 0)) {
1392
            $this->sortRelationshipsByType();
1393
        }
1394
    }
1395
1396
    /**
1397
     * Get Methods that has been added in the child class.
1398
     *
1399
     * @return array
1400
     */
1401
    protected function getCustomMethods()
1402
    {
1403
        $mapMethods = get_class_methods($this);
1404
1405
        $parentsMethods = get_class_methods('Analogue\ORM\EntityMap');
1406
1407
        return array_diff($mapMethods, $parentsMethods);
1408
    }
1409
1410
    /**
1411
     * Parse user's class methods for relationships.
1412
     *
1413
     * @param array $customMethods
1414
     *
1415
     * @return array
1416
     */
1417
    protected function parseMethodsForRelationship(array $customMethods)
1418
    {
1419
        $relationships = [];
1420
1421
        $class = new ReflectionClass(get_class($this));
1422
1423
        // Get the mapped Entity class, as we will detect relationships
1424
        // methods by testing that the first argument is type-hinted to
1425
        // the same class as the mapped Entity.
1426
        $entityClass = $this->getClass();
1427
1428
        foreach ($customMethods as $methodName) {
1429
            $method = $class->getMethod($methodName);
1430
1431
            if ($method->getNumberOfParameters() > 0) {
1432
                $params = $method->getParameters();
1433
1434
                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...
1435
                    $relationships[] = $methodName;
1436
                }
1437
            }
1438
        }
1439
1440
        return $relationships;
1441
    }
1442
1443
    /**
1444
     * Sort Relationships methods by type.
1445
     *
1446
     * TODO : replace this by direclty setting these value
1447
     * in the corresponding methods, so we won't need
1448
     * the correpondancy tabble
1449
     *
1450
     * @return void
1451
     */
1452
    protected function sortRelationshipsByType()
1453
    {
1454
        $entityClass = $this->getClass();
1455
1456
        // Instantiate a dummy entity which we will pass to relationship methods.
1457
        $entity = unserialize(sprintf('O:%d:"%s":0:{}', strlen($entityClass), $entityClass));
1458
1459
        foreach ($this->relationships as $relation) {
1460
            $this->$relation($entity);
1461
        }
1462
    }
1463
1464
    /**
1465
     * Override this method for custom entity instantiation.
1466
     *
1467
     * @return null
1468
     */
1469
    public function activator()
1470
    {
1471
    }
1472
1473
    /**
1474
     * Magic call to dynamic relationships.
1475
     *
1476
     * @param string $method
1477
     * @param array  $parameters
1478
     *
1479
     * @throws Exception
1480
     *
1481
     * @return mixed
1482
     */
1483
    public function __call($method, $parameters)
1484
    {
1485
        if (!array_key_exists($method, $this->dynamicRelationships)) {
1486
            throw new Exception(get_class($this)." has no method $method");
1487
        }
1488
1489
        // Add $this to parameters so the closure can call relationship method on the map.
1490
        $parameters[] = $this;
1491
1492
        return  call_user_func_array([$this->dynamicRelationships[$method], $parameters]);
1493
    }
1494
}
1495