Passed
Pull Request — master (#201)
by Alex
06:15
created

MetadataTrait::addRelationsHook()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 14
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 10
c 1
b 0
f 0
dl 0
loc 14
ccs 0
cts 0
cp 0
rs 9.9332
cc 3
nc 4
nop 8
crap 12

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
<?php
2
namespace AlgoWeb\PODataLaravel\Models;
3
4
use AlgoWeb\PODataLaravel\Models\ObjectMap\Entities\Associations\AssociationStubMonomorphic;
5
use AlgoWeb\PODataLaravel\Models\ObjectMap\Entities\Associations\AssociationStubPolymorphic;
6
use AlgoWeb\PODataLaravel\Models\ObjectMap\Entities\Associations\AssociationStubRelationType;
7
use AlgoWeb\PODataLaravel\Models\ObjectMap\Entities\EntityField;
8
use AlgoWeb\PODataLaravel\Models\ObjectMap\Entities\EntityFieldPrimitiveType;
9
use AlgoWeb\PODataLaravel\Models\ObjectMap\Entities\EntityFieldType;
10
use AlgoWeb\PODataLaravel\Models\ObjectMap\Entities\EntityGubbins;
11
use Illuminate\Database\Eloquent\Model;
12
use Illuminate\Database\Eloquent\Relations\BelongsTo;
13
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
14
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
15
use Illuminate\Database\Eloquent\Relations\MorphMany;
16
use Illuminate\Database\Eloquent\Relations\MorphOne;
17
use Illuminate\Database\Eloquent\Relations\MorphToMany;
18
use Illuminate\Database\Eloquent\Relations\Relation;
19
use Illuminate\Support\Facades\App;
20
use Mockery\Mock;
21
use POData\Common\InvalidOperationException;
22
use POData\Providers\Metadata\Type\IType;
23
24
trait MetadataTrait
25
{
26
    protected static $relationHooks = [];
27
    protected static $relationCategories = [];
28 3
    protected static $methodPrimary = [];
29
    protected static $methodAlternate = [];
30 3
    protected $loadEagerRelations = [];
31
    protected static $tableColumns = [];
32
    protected static $tableColumnsDoctrine = [];
33 2
    protected static $tableData = [];
34 2
    protected static $dontCastTypes = ['object', 'array', 'collection', 'int'];
35
36 2
    /**
37
     * Retrieve and assemble this model's metadata for OData packaging
38 2
     * @return array
39 1
     * @throws InvalidOperationException
40
     * @throws \Doctrine\DBAL\DBALException
41
     */
42 1
    public function metadata()
43 1
    {
44 1
        if (!$this instanceof Model) {
45
            throw new InvalidOperationException(get_class($this));
46 1
        }
47
48 1
        if (0 !== count(self::$tableData)) {
49 1
            return self::$tableData;
50 1
        } elseif (isset($this->odata)) {
51
            return self::$tableData = $this->odata;
52 1
        }
53 1
54 1
        // Break these out separately to enable separate reuse
55
        $connect = $this->getConnection();
56 1
        $builder = $connect->getSchemaBuilder();
57
58 1
        $table = $this->getTable();
59 1
60 1
        if (!$builder->hasTable($table)) {
61 1
            return self::$tableData = [];
62 1
        }
63 1
64 1
        /** @var array $columns */
65
        $columns = $this->getTableColumns();
66 1
        /** @var array $mask */
67
        $mask = $this->metadataMask();
68
        $columns = array_intersect($columns, $mask);
69
70
        $tableData = [];
71
72
        $rawFoo = $this->getTableDoctrineColumns();
73 4
        $foo = [];
74
        /** @var array $getters */
75 4
        $getters = $this->collectGetters();
76
        $getters = array_intersect($getters, $mask);
77 4
        $casts = $this->retrieveCasts();
78 4
79 4
        foreach ($rawFoo as $key => $val) {
80 2
            // Work around glitch in Doctrine when reading from MariaDB which added ` characters to root key value
81 4
            $key = trim($key, '`"');
82 1
            $foo[$key] = $val;
83 1
        }
84
85 4
        foreach ($columns as $column) {
86
            // Doctrine schema manager returns columns with lowercased names
87
            $rawColumn = $foo[strtolower($column)];
88
            /** @var IType $rawType */
89
            $rawType = $rawColumn->getType();
90
            $type = $rawType->getName();
91 5
            $default = $this->$column;
92
            $tableData[$column] = ['type' => $type,
93 5
                'nullable' => !($rawColumn->getNotNull()),
94 5
                'fillable' => in_array($column, $this->getFillable()),
95 1
                'default' => $default
96
            ];
97
        }
98 4
99
        foreach ($getters as $get) {
100 4
            if (isset($tableData[$get])) {
101
                continue;
102 4
            }
103 4
            $default = $this->$get;
104 4
            $tableData[$get] = ['type' => 'text', 'nullable' => true, 'fillable' => false, 'default' => $default];
105 4
        }
106 4
107 4
        // now, after everything's gathered up, apply Eloquent model's $cast array
108
        foreach ($casts as $key => $type) {
109 4
            $type = strtolower($type);
110 1
            if (array_key_exists($key, $tableData) && !in_array($type, self::$dontCastTypes)) {
111 1
                $tableData[$key]['type'] = $type;
112 1
            }
113 1
        }
114
115 4
        self::$tableData = $tableData;
116 4
        return $tableData;
117
    }
118 4
119
    /**
120
     * Return the set of fields that are permitted to be in metadata
121
     * - following same visible-trumps-hidden guideline as Laravel
122
     *
123
     * @return array
124
     */
125
    public function metadataMask()
126
    {
127
        $attribs = array_keys($this->getAllAttributes());
128
129
        $visible = $this->getVisible();
130
        $hidden = $this->getHidden();
131
        if (0 < count($visible)) {
132
            $attribs = array_intersect($visible, $attribs);
133
        } elseif (0 < count($hidden)) {
134
            $attribs = array_diff($attribs, $hidden);
135
        }
136
137
        return $attribs;
138
    }
139
140
    /*
141
     * Get the endpoint name being exposed
142
     *
143
     * @return null|string;
144
     */
145
    public function getEndpointName()
146
    {
147
        $endpoint = isset($this->endpoint) ? $this->endpoint : null;
148
149
        if (!isset($endpoint)) {
150
            $bitter = get_class($this);
151
            $name = substr($bitter, strrpos($bitter, '\\')+1);
152
            return ($name);
153
        }
154
        return ($endpoint);
155
    }
156
157
    /**
158
     * Get model's relationships.
159
     *
160
     * @return array
161
     * @throws InvalidOperationException
162
     * @throws \ReflectionException
163
     */
164
    public function getRelationships()
165
    {
166
        if (empty(static::$relationHooks)) {
167
            $hooks = [];
168
169
            $rels = $this->getRelationshipsFromMethods(true);
170
171
            $this->getRelationshipsUnknownPolyMorph($rels, $hooks);
172
173
            $this->getRelationshipsKnownPolyMorph($rels, $hooks);
174
175 3
            $this->getRelationshipsHasOne($rels, $hooks);
176
177 3
            $this->getRelationshipsHasMany($rels, $hooks);
178
179 3
            static::$relationHooks = $hooks;
180 3
        }
181 3
182 3
        return static::$relationHooks;
183 3
    }
184 3
185 3
    protected function getAllAttributes()
186 3
    {
187 3
        // Adapted from http://stackoverflow.com/a/33514981
188 3
        // $columns = $this->getFillable();
189
        // Another option is to get all columns for the table like so:
190 3
        $columns = $this->getTableColumns();
191
        // but it's safer to just get the fillable fields
192 3
193 3
        $attributes = $this->getAttributes();
194 3
195 3
        foreach ($columns as $column) {
196 3
            if (!array_key_exists($column, $attributes)) {
197 3
                $attributes[$column] = null;
198 3
            }
199 3
        }
200 3
201 3
        $methods = $this->collectGetters();
202
203 3
        foreach ($methods as $method) {
204 3
            $attributes[$method] = null;
205 3
        }
206 3
207 3
        return $attributes;
208 3
    }
209 3
210 3
    /**
211
     * @param bool $biDir
212 3
     *
213 3
     * @return array
214 3
     * @throws InvalidOperationException
215
     * @throws \ReflectionException
216 3
     */
217 3
    protected function getRelationshipsFromMethods($biDir = false)
218 3
    {
219 3
        $biDirVal = intval($biDir);
220 3
        $isCached = isset(static::$relationCategories[$biDirVal]) && !empty(static::$relationCategories[$biDirVal]);
221
        if (!$isCached) {
222 1
            $model = $this;
223 3
            $relationships = [
224
                'HasOne' => [],
225 1
                'UnknownPolyMorphSide' => [],
226
                'HasMany' => [],
227 1
                'KnownPolyMorphSide' => []
228
            ];
229 1
            $methods = $this->getClassMethods($model);
0 ignored issues
show
Bug introduced by
$model of type AlgoWeb\PODataLaravel\Models\MetadataTrait is incompatible with the type Illuminate\Database\Eloquent\Model expected by parameter $model of AlgoWeb\PODataLaravel\Mo...rait::getClassMethods(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

229
            $methods = $this->getClassMethods(/** @scrutinizer ignore-type */ $model);
Loading history...
230
            foreach ($methods as $method) {
231 3
                //Use reflection to inspect the code, based on Illuminate/Support/SerializableClosure.php
232 2
                $reflection = new \ReflectionMethod($model, $method);
233 2
                $fileName = $reflection->getFileName();
234 3
235 3
                $file = new \SplFileObject($fileName);
236 3
                $file->seek($reflection->getStartLine()-1);
237 3
                $code = '';
238 3
                while ($file->key() < $reflection->getEndLine()) {
239 3
                    $code .= $file->current();
240 3
                    $file->next();
241
                }
242
243
                $code = trim(preg_replace('/\s\s+/', '', $code));
244
                if (false === stripos($code, 'function')) {
245
                    $msg = 'Function definition must have keyword \'function\'';
246
                    throw new InvalidOperationException($msg);
247
                }
248
                $begin = strpos($code, 'function(');
249
                $code = substr($code, /** @scrutinizer ignore-type */$begin, strrpos($code, '}')-$begin+1);
250
                $lastCode = $code[strlen(/** @scrutinizer ignore-type */$code)-1];
251
                if ('}' != $lastCode) {
252
                    $msg = 'Final character of function definition must be closing brace';
253
                    throw new InvalidOperationException($msg);
254
                }
255
                foreach ([
256
                             'hasMany',
257
                             'hasManyThrough',
258
                             'belongsToMany',
259
                             'hasOne',
260
                             'belongsTo',
261
                             'morphOne',
262
                             'morphTo',
263
                             'morphMany',
264
                             'morphToMany',
265
                             'morphedByMany'
266
                         ] as $relation) {
267
                    $search = '$this->' . $relation . '(';
268
                    if (stripos(/** @scrutinizer ignore-type */$code, $search)) {
269
                        //Resolve the relation's model to a Relation object.
270
                        $relationObj = $model->$method();
271
                        if ($relationObj instanceof Relation) {
272
                            $relObject = $relationObj->getRelated();
273
                            $relatedModel = '\\' . get_class($relObject);
274
                            if (in_array(MetadataTrait::class, class_uses($relatedModel))) {
275
                                $relations = [
276
                                    'hasManyThrough',
277
                                    'belongsToMany',
278
                                    'hasMany',
279
                                    'morphMany',
280
                                    'morphToMany',
281
                                    'morphedByMany'
282
                                ];
283
                                if (in_array($relation, $relations)) {
284
                                    //Collection or array of models (because Collection is Arrayable)
285
                                    $relationships['HasMany'][$method] = $biDir ? $relationObj : $relatedModel;
286
                                } elseif ('morphTo' === $relation) {
287
                                    // Model isn't specified because relation is polymorphic
288
                                    $relationships['UnknownPolyMorphSide'][$method] =
289
                                        $biDir ? $relationObj : '\Illuminate\Database\Eloquent\Model|\Eloquent';
290
                                } else {
291
                                    //Single model is returned
292
                                    $relationships['HasOne'][$method] = $biDir ? $relationObj : $relatedModel;
293
                                }
294
                                if (in_array($relation, ['morphMany', 'morphOne', 'morphToMany'])) {
295
                                    $relationships['KnownPolyMorphSide'][$method] =
296
                                        $biDir ? $relationObj : $relatedModel;
297
                                }
298
                                if (in_array($relation, ['morphedByMany'])) {
299
                                    $relationships['UnknownPolyMorphSide'][$method] =
300
                                        $biDir ? $relationObj : $relatedModel;
301
                                }
302
                            }
303
                        }
304
                    }
305
                }
306
            }
307
            static::$relationCategories[$biDirVal] = $relationships;
308
        }
309
        return static::$relationCategories[$biDirVal];
310
    }
311
312
    /**
313
     * Get the visible attributes for the model.
314
     *
315
     * @return array
316
     */
317
    abstract public function getVisible();
318
319
    /**
320
     * Get the hidden attributes for the model.
321
     *
322
     * @return array
323
     */
324
    abstract public function getHidden();
325
326
    /**
327
     * Get the primary key for the model.
328
     *
329
     * @return string
330
     */
331
    abstract public function getKeyName();
332
333
    /**
334
     * Get the current connection name for the model.
335
     *
336
     * @return string
337
     */
338
    abstract public function getConnectionName();
339
340
    /**
341
     * Get the database connection for the model.
342
     *
343
     * @return \Illuminate\Database\Connection
344
     */
345
    abstract public function getConnection();
346
347
    /**
348
     * Get all of the current attributes on the model.
349
     *
350
     * @return array
351
     */
352
    abstract public function getAttributes();
353
354
    /**
355
     * Get the table associated with the model.
356
     *
357
     * @return string
358
     */
359
    abstract public function getTable();
360
361
    /**
362
     * Get the fillable attributes for the model.
363
     *
364
     * @return array
365
     */
366
    abstract public function getFillable();
367
368
    /**
369
     * Dig up all defined getters on the model.
370
     *
371
     * @return array
372
     */
373
    protected function collectGetters()
374
    {
375
        $getterz = [];
376
        $methods = get_class_methods($this);
377
        foreach ($methods as $method) {
378
            if (12 < strlen($method) && 'get' == substr($method, 0, 3)) {
379
                if ('Attribute' == substr($method, -9)) {
380
                    $getterz[] = $method;
381
                }
382
            }
383
        }
384
        $methods = [];
385
386
        foreach ($getterz as $getter) {
387
            $residual = substr($getter, 3);
388
            $residual = substr(/** @scrutinizer ignore-type */$residual, 0, -9);
389
            $methods[] = $residual;
390
        }
391
        return $methods;
392
    }
393
394
    /**
395
     * @param       $foo
396
     * @param mixed $condition
397
     *
398
     * @return array
399
     * @throws InvalidOperationException
400
     */
401
    private function polyglotKeyMethodNames($foo, $condition = false)
402
    {
403
        // if $condition is falsy, return quickly - don't muck around
404
        if (!$condition) {
405
            return [null, null];
406
        }
407
408
        $fkList = ['getQualifiedForeignKeyName', 'getForeignKey'];
409
        $rkList = ['getQualifiedRelatedKeyName', 'getOtherKey', 'getOwnerKey', 'getQualifiedOwnerKeyName'];
410
411
        $fkMethodName = null;
412
        $rkMethodName = null;
413
414
        if (array_key_exists(get_class($foo), static::$methodPrimary)) {
415
            $line = static::$methodPrimary[get_class($foo)];
416
            $fkMethodName = $line['fk'];
417
            $rkMethodName = $line['rk'];
418
        } else {
419
            $methodList = get_class_methods(get_class($foo));
420
            $fkMethodName = 'getQualifiedForeignPivotKeyName';
421
            $fkIntersect = array_values(array_intersect($fkList, $methodList));
422
            $fkMethodName = (0 < count($fkIntersect)) ? $fkIntersect[0] : $fkMethodName;
423
            if (!(in_array($fkMethodName, $methodList))) {
424
                $msg = 'Selected method, ' . $fkMethodName . ', not in method list';
425
                throw new InvalidOperationException($msg);
426
            }
427
            $rkMethodName = 'getQualifiedRelatedPivotKeyName';
428
            $rkIntersect = array_values(array_intersect($rkList, $methodList));
429
            $rkMethodName = (0 < count($rkIntersect)) ? $rkIntersect[0] : $rkMethodName;
430
            if (!(in_array($rkMethodName, $methodList))) {
431
                $msg = 'Selected method, ' . $rkMethodName . ', not in method list';
432
                throw new InvalidOperationException($msg);
433
            }
434
            $line = ['fk' => $fkMethodName, 'rk' => $rkMethodName];
435
            static::$methodPrimary[get_class($foo)] = $line;
436
        }
437
        return [$fkMethodName, $rkMethodName];
438
    }
439
440
    /**
441
     * @param Model|Relation $foo
442
     * @param bool $condition
443
     * @return array
444
     * @throws InvalidOperationException
445
     */
446
    private function polyglotKeyMethodBackupNames($foo, $condition = false)
447
    {
448
        // if $condition is falsy, return quickly - don't muck around
449
        if (!$condition) {
450
            return [null, null];
451
        }
452
453
        $fkList = ['getForeignKey', 'getForeignKeyName', 'getQualifiedFarKeyName'];
454
        $rkList = ['getOtherKey', 'getQualifiedParentKeyName'];
455
456
        $fkMethodName = null;
457
        $rkMethodName = null;
458
459
        if (array_key_exists(get_class($foo), static::$methodAlternate)) {
460
            $line = static::$methodAlternate[get_class($foo)];
461
            $fkMethodName = $line['fk'];
462
            $rkMethodName = $line['rk'];
463
        } else {
464
            $methodList = get_class_methods(get_class($foo));
465
            $fkCombo = array_values(array_intersect($fkList, $methodList));
466
            if (!(1 <= count($fkCombo))) {
467
                $msg = 'Expected at least 1 element in foreign-key list, got ' . count($fkCombo);
468
                throw new InvalidOperationException($msg);
469
            }
470
            $fkMethodName = $fkCombo[0];
471
            if (!(in_array($fkMethodName, $methodList))) {
472
                $msg = 'Selected method, ' . $fkMethodName . ', not in method list';
473
                throw new InvalidOperationException($msg);
474
            }
475
            $rkCombo = array_values(array_intersect($rkList, $methodList));
476
            if (!(1 <= count($rkCombo))) {
477
                $msg = 'Expected at least 1 element in related-key list, got ' . count($rkCombo);
478
                throw new InvalidOperationException($msg);
479
            }
480
            $rkMethodName = $rkCombo[0];
481
            if (!(in_array($rkMethodName, $methodList))) {
482
                $msg = 'Selected method, ' . $rkMethodName . ', not in method list';
483
                throw new InvalidOperationException($msg);
484
            }
485
            $line = ['fk' => $fkMethodName, 'rk' => $rkMethodName];
486
            static::$methodAlternate[get_class($foo)] = $line;
487
        }
488
        return [$fkMethodName, $rkMethodName];
489
    }
490
491
    private function polyglotThroughKeyMethodNames(HasManyThrough $foo)
492
    {
493
        $thruList = ['getThroughKey', 'getQualifiedFirstKeyName'];
494
495
        $methodList = get_class_methods(get_class($foo));
496
        $thruCombo = array_values(array_intersect($thruList, $methodList));
497
        return $thruCombo[0];
498
    }
499
500
501
    /**
502
     * @param             $hooks
503
     * @param             $first
504
     * @param             $property
505
     * @param             $last
506
     * @param             $mult
507
     * @param string|null $targ
508
     * @param mixed|null  $type
509
     * @param mixed|null  $through
510
     */
511
    private function addRelationsHook(&$hooks, $first, $property, $last, $mult, $targ, $type = null, $through = null)
512
    {
513
        if (!isset($hooks[$first])) {
514
            $hooks[$first] = [];
515
        }
516
        if (!isset($hooks[$first][$targ])) {
517
            $hooks[$first][$targ] = [];
518
        }
519
        $hooks[$first][$targ][$property] = [
520
            'property' => $property,
521
            'local' => $last,
522
            'through' => $through,
523
            'multiplicity' => $mult,
524
            'type' => $type
525
        ];
526
    }
527
528
    /**
529
     * @param $rels
530
     * @param $hooks
531
     * @throws InvalidOperationException
532
     */
533
    private function getRelationshipsHasMany($rels, &$hooks)
534
    {
535
        /**
536
         * @var string $property
537
         * @var Relation $foo
538
         */
539
        foreach ($rels['HasMany'] as $property => $foo) {
540
            if ($foo instanceof MorphMany || $foo instanceof MorphToMany) {
541
                continue;
542
            }
543
            $mult = '*';
544
            $targ = get_class($foo->getRelated());
545
            list($thruName, $fkMethodName, $rkMethodName) = $this->getRelationsHasManyKeyNames($foo);
546
547
            $keyRaw = $foo->$fkMethodName();
548
            $keySegments = explode('.', $keyRaw);
549
            $keyName = $keySegments[count($keySegments)-1];
550
            $localRaw = $foo->$rkMethodName();
551
            $localSegments = explode('.', $localRaw);
552
            $localName = $localSegments[count($localSegments)-1];
553
            if (null !== $thruName) {
554
                $thruRaw = $foo->$thruName();
555
                $thruSegments = explode('.', $thruRaw);
556
                $thruName = $thruSegments[count($thruSegments)-1];
557
            }
558
            $first = $keyName;
559
            $last = $localName;
560
            $this->addRelationsHook($hooks, $first, $property, $last, $mult, $targ, null, $thruName);
561
        }
562
    }
563
564
    /**
565
     * @param $rels
566
     * @param $hooks
567
     * @throws InvalidOperationException
568
     */
569
    private function getRelationshipsHasOne($rels, &$hooks)
570
    {
571
        /**
572
         * @var string $property
573
         * @var Relation $foo
574
         */
575
        foreach ($rels['HasOne'] as $property => $foo) {
576
            if ($foo instanceof MorphOne) {
577
                continue;
578
            }
579
            $isBelong = $foo instanceof BelongsTo;
580
            $mult = $isBelong ? '1' : '0..1';
581
            $targ = get_class($foo->getRelated());
582
583
            list($fkMethodName, $rkMethodName) = $this->polyglotKeyMethodNames($foo, $isBelong);
584
            list($fkMethodAlternate, $rkMethodAlternate) = $this->polyglotKeyMethodBackupNames($foo, !$isBelong);
585
586
            $keyName = $isBelong ? $foo->$fkMethodName() : $foo->$fkMethodAlternate();
587
            $keySegments = explode('.', $keyName);
588
            $keyName = $keySegments[count($keySegments)-1];
589
            $localRaw = $isBelong ? $foo->$rkMethodName() : $foo->$rkMethodAlternate();
590
            $localSegments = explode('.', $localRaw);
591
            $localName = $localSegments[count($localSegments)-1];
592
            $first = $isBelong ? $localName : $keyName;
593
            $last = $isBelong ? $keyName : $localName;
594
            $this->addRelationsHook($hooks, $first, $property, $last, $mult, $targ);
595
        }
596
    }
597
598
    /**
599
     * @param $rels
600
     * @param $hooks
601
     * @throws InvalidOperationException
602
     */
603
    private function getRelationshipsKnownPolyMorph($rels, &$hooks)
604
    {
605
        /**
606
         * @var string $property
607
         * @var Relation $foo
608
         */
609
        foreach ($rels['KnownPolyMorphSide'] as $property => $foo) {
610
            $isMany = $foo instanceof MorphToMany;
611
            $targ = get_class($foo->getRelated());
612
            $mult = $isMany ? '*' : ($foo instanceof MorphMany ? '*' : '1');
613
            $mult = $foo instanceof MorphOne ? '0..1' : $mult;
614
615
            list($fkMethodName, $rkMethodName) = $this->polyglotKeyMethodNames($foo, $isMany);
616
            list($fkMethodAlternate, $rkMethodAlternate) = $this->polyglotKeyMethodBackupNames($foo, !$isMany);
617
618
            $keyRaw = $isMany ? $foo->$fkMethodName() : $foo->$fkMethodAlternate();
619
            $keySegments = explode('.', $keyRaw);
620
            $keyName = $keySegments[count($keySegments)-1];
621
            $localRaw = $isMany ? $foo->$rkMethodName() : $foo->$rkMethodAlternate();
622
            $localSegments = explode('.', $localRaw);
623
            $localName = $localSegments[count($localSegments)-1];
624
            $first = $isMany ? $keyName : $localName;
625
            $last = $isMany ? $localName : $keyName;
626
            $this->addRelationsHook($hooks, $first, $property, $last, $mult, $targ, 'unknown');
627
        }
628
    }
629
630
    /**
631
     * @param $rels
632
     * @param $hooks
633
     * @throws InvalidOperationException
634
     */
635
    private function getRelationshipsUnknownPolyMorph($rels, &$hooks)
636
    {
637
        /**
638
         * @var string $property
639
         * @var Relation $foo
640
         */
641
        foreach ($rels['UnknownPolyMorphSide'] as $property => $foo) {
642
            $isMany = $foo instanceof MorphToMany;
643
            $targ = get_class($foo->getRelated());
644
            $mult = $isMany ? '*' : '1';
645
646
            list($fkMethodName, $rkMethodName) = $this->polyglotKeyMethodNames($foo, $isMany);
647
            list($fkMethodAlternate, $rkMethodAlternate) = $this->polyglotKeyMethodBackupNames($foo, !$isMany);
648
649
            $keyRaw = $isMany ? $foo->$fkMethodName() : $foo->$fkMethodAlternate();
650
            $keySegments = explode('.', $keyRaw);
651
            $keyName = $keySegments[count($keySegments)-1];
652
            $localRaw = $isMany ? $foo->$rkMethodName() : $foo->$rkMethodAlternate();
653
            $localSegments = explode('.', $localRaw);
654
            $localName = $localSegments[count($localSegments)-1];
655
656
            $first = $keyName;
657
            $last = (isset($localName) && '' != $localName) ? $localName : $foo->getRelated()->getKeyName();
658
            $this->addRelationsHook($hooks, $first, $property, $last, $mult, $targ, 'known');
659
        }
660
    }
661
662
    /**
663
     * Supplemental function to retrieve cast array for Laravel versions that do not supply hasCasts.
664
     *
665
     * @return array
666
     */
667
    public function retrieveCasts()
668
    {
669
        $exists = method_exists($this, 'getCasts');
670
        return $exists ? (array)$this->getCasts() : (array)$this->casts;
671
    }
672
673
    /**
674
     * Return list of relations to be eager-loaded by Laravel query provider.
675
     *
676
     * @return array
677
     */
678
    public function getEagerLoad()
679
    {
680
        return $this->loadEagerRelations;
681
    }
682
683
    /**
684
     * Set list of relations to be eager-loaded.
685
     *
686
     * @param array $relations
687
     */
688
    public function setEagerLoad(array $relations)
689
    {
690
        $this->loadEagerRelations = array_map('strval', $relations);
691
    }
692
693
    /**
694
     * Is this model the known side of at least one polymorphic relation?
695
     *
696
     * @throws InvalidOperationException
697
     * @throws \ReflectionException
698
     */
699
    public function isKnownPolymorphSide()
700
    {
701
        // isKnownPolymorph needs to be checking KnownPolymorphSide results - if you're checking UnknownPolymorphSide,
702
        // you're turned around
703
        $rels = $this->getRelationshipsFromMethods();
704
        return !empty($rels['KnownPolyMorphSide']);
705
    }
706
707
    /**
708
     * Is this model on the unknown side of at least one polymorphic relation?
709
     *
710
     * @throws InvalidOperationException
711
     * @throws \ReflectionException
712
     */
713
    public function isUnknownPolymorphSide()
714
    {
715
        // isUnknownPolymorph needs to be checking UnknownPolymorphSide results - if you're checking KnownPolymorphSide,
716
        // you're turned around
717
        $rels = $this->getRelationshipsFromMethods();
718
        return !empty($rels['UnknownPolyMorphSide']);
719
    }
720
721
    /**
722
     * Extract entity gubbins detail for later downstream use.
723
     *
724
     * @return EntityGubbins
725
     * @throws InvalidOperationException
726
     * @throws \ReflectionException
727
     * @throws \Doctrine\DBAL\DBALException
728
     * @throws \Exception
729
     */
730
    public function extractGubbins()
731
    {
732
        $multArray = [
733
            '*' => AssociationStubRelationType::MANY(),
734
            '1' => AssociationStubRelationType::ONE(),
735
            '0..1' => AssociationStubRelationType::NULL_ONE()
736
        ];
737
738
        $gubbins = new EntityGubbins();
739
        $gubbins->setName($this->getEndpointName());
740
        $gubbins->setClassName(get_class($this));
741
742
        $lowerNames = [];
743
744
        $fields = $this->metadata();
745
        $entityFields = [];
746
        foreach ($fields as $name => $field) {
747
            if (in_array(strtolower($name), $lowerNames)) {
748
                $msg = 'Property names must be unique, without regard to case';
749
                throw new \Exception($msg);
750
            }
751
            $lowerNames[] = strtolower($name);
752
            $nuField = new EntityField();
753
            $nuField->setName($name);
754
            $nuField->setIsNullable($field['nullable']);
755
            $nuField->setReadOnly(false);
756
            $nuField->setCreateOnly(false);
757
            $nuField->setDefaultValue($field['default']);
758
            $nuField->setIsKeyField($this->getKeyName() == $name);
759
            $nuField->setFieldType(EntityFieldType::PRIMITIVE());
760
            $nuField->setPrimitiveType(new EntityFieldPrimitiveType($field['type']));
761
            $entityFields[$name] = $nuField;
762
        }
763
        $isEmpty = (0 === count($entityFields));
764
        if (!($isEmpty && $this->isRunningInArtisan())) {
765
            $gubbins->setFields($entityFields);
766
        }
767
768
        $rawRels = $this->getRelationships();
769
        $stubs = [];
770
        foreach ($rawRels as $key => $rel) {
771
            foreach ($rel as $rawName => $deets) {
772
                foreach ($deets as $relName => $relGubbins) {
773
                    if (in_array(strtolower($relName), $lowerNames)) {
774
                        $msg = 'Property names must be unique, without regard to case';
775
                        throw new \Exception($msg);
776
                    }
777
                    $lowerNames[] = strtolower($relName);
778
                    $gubbinsType = $relGubbins['type'];
779
                    $property = $relGubbins['property'];
780
                    $isPoly = isset($gubbinsType);
781
                    $targType = 'known' != $gubbinsType ? $rawName : null;
782
                    $stub = $isPoly ? new AssociationStubPolymorphic() : new AssociationStubMonomorphic();
783
                    $stub->setBaseType(get_class($this));
784
                    $stub->setRelationName($property);
785
                    $stub->setKeyField($relGubbins['local']);
786
                    $stub->setForeignField($targType ? $key : null);
787
                    $stub->setMultiplicity($multArray[$relGubbins['multiplicity']]);
788
                    $stub->setTargType($targType);
789
                    if (null !== $relGubbins['through']) {
790
                        $stub->setThroughField($relGubbins['through']);
791
                    }
792
                    if (!$stub->isOk()) {
793
                        throw new InvalidOperationException('Generated stub not consistent');
794
                    }
795
                    $stubs[$property] = $stub;
796
                }
797
            }
798
        }
799
        $gubbins->setStubs($stubs);
800
801
        return $gubbins;
802
    }
803
804
    public function isRunningInArtisan()
805
    {
806
        return App::runningInConsole() && !App::runningUnitTests();
807
    }
808
809
    /**
810
     * Get columns for selected table
811
     *
812
     * @return array
813
     */
814
    protected function getTableColumns()
815
    {
816
        if (0 === count(self::$tableColumns)) {
817
            $table = $this->getTable();
818
            $connect = $this->getConnection();
819
            $builder = $connect->getSchemaBuilder();
820
            $columns = $builder->getColumnListing($table);
821
822
            self::$tableColumns = (array)$columns;
823
        }
824
        return self::$tableColumns;
825
    }
826
827
    /**
828
     * Get Doctrine columns for selected table
829
     *
830
     * @return array
831
     * @throws \Doctrine\DBAL\DBALException
832
     */
833
    protected function getTableDoctrineColumns()
834
    {
835
        if (0 === count(self::$tableColumnsDoctrine)) {
836
            $table = $this->getTable();
837
            $connect = $this->getConnection();
838
            $columns = $connect->getDoctrineSchemaManager()->listTableColumns($table);
839
840
            self::$tableColumnsDoctrine = $columns;
841
        }
842
        return self::$tableColumnsDoctrine;
843
    }
844
845
    public function reset()
846
    {
847
        self::$tableData = [];
848
        self::$tableColumnsDoctrine = [];
849
        self::$tableColumns = [];
850
    }
851
852
    /**
853
     * @param Relation $foo
854
     * @return array|null
855
     * @throws InvalidOperationException
856
     */
857
    private function getRelationsHasManyKeyNames($foo)
858
    {
859
        $thruName = null;
860
        if ($foo instanceof HasManyThrough) {
861
            list($fkMethodName, $rkMethodName) = $this->polyglotKeyMethodBackupNames($foo, true);
862
            $thruName = $this->polyglotThroughKeyMethodNames($foo);
863
            return [$thruName, $fkMethodName, $rkMethodName];
864
        }
865
        if ($foo instanceof BelongsToMany) {
866
            list($fkMethodName, $rkMethodName) = $this->polyglotKeyMethodNames($foo, true);
867
            return [$thruName, $fkMethodName, $rkMethodName];
868
        }
869
        list($fkMethodName, $rkMethodName) = $this->polyglotKeyMethodBackupNames($foo, true);
870
        return [$thruName, $fkMethodName, $rkMethodName];
871
    }
872
873
    /**
874
     * @param Model $model
875
     * @return array
876
     */
877
    protected function getClassMethods(Model $model)
878
    {
879
        $methods = get_class_methods($model);
880
        $filter = function ($method) {
881
            return (!method_exists('Illuminate\Database\Eloquent\Model', $method)
882
                    && !method_exists(Mock::class, $method)
883
                    && !method_exists(MetadataTrait::class, $method)
884
            );
885
        };
886
        $methods = array_filter($methods, $filter);
887
888
        return $methods;
889
    }
890
}
891