Completed
Branch master (a17b64)
by Rémi
15:50
created

EntityMap::getLocalKeys()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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