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