Passed
Pull Request — master (#153)
by Alex
07:18
created

MetadataTrait::getRelationshipsKnownPolyMorph()   B

Complexity

Conditions 9
Paths 129

Size

Total Lines 20
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 90

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 20
ccs 0
cts 0
cp 0
rs 7
cc 9
eloc 16
nc 129
nop 2
crap 90
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 AlgoWeb\PODataLaravel\Query\LaravelReadQuery;
12
use Illuminate\Database\Eloquent\Model;
13
use Illuminate\Database\Eloquent\Relations\BelongsTo;
14
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
15
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
16
use Illuminate\Database\Eloquent\Relations\MorphMany;
17
use Illuminate\Database\Eloquent\Relations\MorphOne;
18
use Illuminate\Database\Eloquent\Relations\MorphToMany;
19
use Illuminate\Database\Eloquent\Relations\Relation;
20
use Illuminate\Support\Facades\App;
21
22
trait MetadataTrait
23
{
24
    protected static $relationHooks = [];
25
    protected static $relationCategories = [];
26
    protected static $methodPrimary = [];
27
    protected static $methodAlternate = [];
28 3
    protected $loadEagerRelations = [];
29
    protected static $tableColumns = [];
30 3
    protected static $tableColumnsDoctrine = [];
31
    protected static $tableData = [];
32
    protected static $dontCastTypes = ['object', 'array', 'collection', 'int'];
33 2
34 2
    /*
35
     * Retrieve and assemble this model's metadata for OData packaging
36 2
     */
37
    public function metadata()
38 2
    {
39 1
        assert($this instanceof Model, get_class($this));
40
41
        // Break these out separately to enable separate reuse
42 1
        $connect = $this->getConnection();
43 1
        $builder = $connect->getSchemaBuilder();
44 1
45
        $table = $this->getTable();
46 1
47
        if (!$builder->hasTable($table)) {
48 1
            return [];
49 1
        }
50 1
        if (0 !== count(self::$tableData)) {
51
            return self::$tableData;
52 1
        }
53 1
54 1
        $columns = $this->getTableColumns();
55
        $mask = $this->metadataMask();
56 1
        $columns = array_intersect($columns, $mask);
57
58 1
        $tableData = [];
59 1
60 1
        $rawFoo = $this->getTableDoctrineColumns();
61 1
        $foo = [];
62 1
        $getters = $this->collectGetters();
63 1
        $getters = array_intersect($getters, $mask);
64 1
        $casts = $this->retrieveCasts();
65
66 1
        foreach ($rawFoo as $key => $val) {
67
            // Work around glitch in Doctrine when reading from MariaDB which added ` characters to root key value
68
            $key = trim($key, '`');
69
            $foo[$key] = $val;
70
        }
71
72
        foreach ($columns as $column) {
73 4
            // Doctrine schema manager returns columns with lowercased names
74
            $rawColumn = $foo[strtolower($column)];
75 4
            $nullable = !($rawColumn->getNotNull());
76
            $fillable = in_array($column, $this->getFillable());
77 4
            $rawType = $rawColumn->getType();
78 4
            $type = $rawType->getName();
79 4
            $default = $this->$column;
80 2
            $tableData[$column] = ['type' => $type,
81 4
                'nullable' => $nullable,
82 1
                'fillable' => $fillable,
83 1
                'default' => $default
84
            ];
85 4
        }
86
87
        foreach ($getters as $get) {
88
            if (isset($tableData[$get])) {
89
                continue;
90
            }
91 5
            $default = $this->$get;
92
            $tableData[$get] = ['type' => 'text', 'nullable' => true, 'fillable' => false, 'default' => $default];
93 5
        }
94 5
95 1
        // now, after everything's gathered up, apply Eloquent model's $cast array
96
        foreach ($casts as $key => $type) {
97
            $type = strtolower($type);
98 4
            if (array_key_exists($key, $tableData) && !in_array($type, self::$dontCastTypes)) {
99
                $tableData[$key]['type'] = $type;
100 4
            }
101
        }
102 4
103 4
        self::$tableData = $tableData;
104 4
        return $tableData;
105 4
    }
106 4
107 4
    /*
108
     * Return the set of fields that are permitted to be in metadata
109 4
     * - following same visible-trumps-hidden guideline as Laravel
110 1
     */
111 1
    public function metadataMask()
112 1
    {
113 1
        $attribs = array_keys($this->getAllAttributes());
114
115 4
        $visible = $this->getVisible();
116 4
        $hidden = $this->getHidden();
117
        if (0 < count($visible)) {
118 4
            assert(!empty($visible));
0 ignored issues
show
Bug introduced by
The call to assert() has too few arguments starting with description. ( Ignorable by Annotation )

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

118
            /** @scrutinizer ignore-call */ 
119
            assert(!empty($visible));

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
119
            $attribs = array_intersect($visible, $attribs);
120
        } elseif (0 < count($hidden)) {
121
            assert(!empty($hidden));
122
            $attribs = array_diff($attribs, $hidden);
123
        }
124
125
        return $attribs;
126
    }
127
128
    /*
129
     * Get the endpoint name being exposed
130
     *
131
     */
132
    public function getEndpointName()
133
    {
134
        $endpoint = isset($this->endpoint) ? $this->endpoint : null;
1 ignored issue
show
Bug Best Practice introduced by
The property endpoint does not exist on AlgoWeb\PODataLaravel\Models\MetadataTrait. Did you maybe forget to declare it?
Loading history...
135
136
        if (!isset($endpoint)) {
137
            $bitter = get_class();
138
            $name = substr($bitter, strrpos($bitter, '\\')+1);
139
            return ($name);
140
        }
141
        return ($endpoint);
142
    }
143
144
    /**
145
     * Get model's relationships.
146
     *
147
     * @return array
148
     */
149
    public function getRelationships()
150
    {
151
        if (empty(static::$relationHooks)) {
152
            $hooks = [];
153
154
            $rels = $this->getRelationshipsFromMethods(true);
155
156
            $this->getRelationshipsUnknownPolyMorph($rels, $hooks);
157
158
            $this->getRelationshipsKnownPolyMorph($rels, $hooks);
159
160
            $this->getRelationshipsHasOne($rels, $hooks);
161
162
            $this->getRelationshipsHasMany($rels, $hooks);
163
164
            static::$relationHooks = $hooks;
165
        }
166
167
        return static::$relationHooks;
168
    }
169
170
    protected function getAllAttributes()
171
    {
172
        // Adapted from http://stackoverflow.com/a/33514981
173
        // $columns = $this->getFillable();
174
        // Another option is to get all columns for the table like so:
175 3
        $columns = $this->getTableColumns();
176
        // but it's safer to just get the fillable fields
177 3
178
        $attributes = $this->getAttributes();
179 3
180 3
        foreach ($columns as $column) {
181 3
            if (!array_key_exists($column, $attributes)) {
182 3
                $attributes[$column] = null;
183 3
            }
184 3
        }
185 3
186 3
        $methods = $this->collectGetters();
187 3
188 3
        foreach ($methods as $method) {
189
            $attributes[$method] = null;
190 3
        }
191
192 3
        return $attributes;
193 3
    }
194 3
195 3
    /**
196 3
     * @param bool $biDir
197 3
     *
198 3
     * @return array
199 3
     */
200 3
    protected function getRelationshipsFromMethods($biDir = false)
201 3
    {
202
        $biDirVal = intval($biDir);
203 3
        $isCached = isset(static::$relationCategories[$biDirVal]) && !empty(static::$relationCategories[$biDirVal]);
204 3
        if (!$isCached) {
205 3
            $model = $this;
206 3
            $relationships = [
207 3
                'HasOne' => [],
208 3
                'UnknownPolyMorphSide' => [],
209 3
                'HasMany' => [],
210 3
                'KnownPolyMorphSide' => []
211
            ];
212 3
            $methods = get_class_methods($model);
213 3
            if (!empty($methods)) {
214 3
                foreach ($methods as $method) {
215
                    if (!method_exists('Illuminate\Database\Eloquent\Model', $method)
216 3
                        && !method_exists(Mock::class, $method)
0 ignored issues
show
Bug introduced by
The type AlgoWeb\PODataLaravel\Models\Mock was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
217 3
                        && !method_exists(MetadataTrait::class, $method)
218 3
                    ) {
219 3
                        //Use reflection to inspect the code, based on Illuminate/Support/SerializableClosure.php
220 3
                        $reflection = new \ReflectionMethod($model, $method);
221
                        $fileName = $reflection->getFileName();
222 1
223 3
                        $file = new \SplFileObject($fileName);
224
                        $file->seek($reflection->getStartLine()-1);
225 1
                        $code = '';
226
                        while ($file->key() < $reflection->getEndLine()) {
227 1
                            $code .= $file->current();
228
                            $file->next();
229 1
                        }
230
231 3
                        $code = trim(preg_replace('/\s\s+/', '', $code));
232 2
                        assert(
233 2
                            false !== stripos($code, 'function'),
234 3
                            'Function definition must have keyword \'function\''
235 3
                        );
236 3
                        $begin = strpos($code, 'function(');
237 3
                        $code = substr($code, $begin, strrpos($code, '}')-$begin+1);
0 ignored issues
show
Bug introduced by
It seems like $begin can also be of type false; however, parameter $start of substr() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

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

237
                        $code = substr($code, /** @scrutinizer ignore-type */ $begin, strrpos($code, '}')-$begin+1);
Loading history...
238 3
                        $lastCode = $code[strlen($code)-1];
0 ignored issues
show
Bug introduced by
It seems like $code can also be of type false; however, parameter $string of strlen() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

238
                        $lastCode = $code[strlen(/** @scrutinizer ignore-type */ $code)-1];
Loading history...
239 3
                        assert('}' == $lastCode, 'Final character of function definition must be closing brace');
240 3
                        foreach ([
241
                                     'hasMany',
242
                                     'hasManyThrough',
243
                                     'belongsToMany',
244
                                     'hasOne',
245
                                     'belongsTo',
246
                                     'morphOne',
247
                                     'morphTo',
248
                                     'morphMany',
249
                                     'morphToMany',
250
                                     'morphedByMany'
251
                                 ] as $relation) {
252
                            $search = '$this->' . $relation . '(';
253
                            if ($pos = stripos($code, $search)) {
0 ignored issues
show
Bug introduced by
It seems like $code can also be of type false; however, parameter $haystack of stripos() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

253
                            if ($pos = stripos(/** @scrutinizer ignore-type */ $code, $search)) {
Loading history...
Unused Code introduced by
The assignment to $pos is dead and can be removed.
Loading history...
254
                                //Resolve the relation's model to a Relation object.
255
                                $relationObj = $model->$method();
256
                                if ($relationObj instanceof Relation) {
257
                                    $relObject = $relationObj->getRelated();
258
                                    $relatedModel = '\\' . get_class($relObject);
259
                                    if (in_array(MetadataTrait::class, class_uses($relatedModel))) {
260
                                        $relations = [
261
                                            'hasManyThrough',
262
                                            'belongsToMany',
263
                                            'hasMany',
264
                                            'morphMany',
265
                                            'morphToMany',
266
                                            'morphedByMany'
267
                                        ];
268
                                        if (in_array($relation, $relations)) {
269
                                            //Collection or array of models (because Collection is Arrayable)
270
                                            $relationships['HasMany'][$method] = $biDir ? $relationObj : $relatedModel;
271
                                        } elseif ('morphTo' === $relation) {
272
                                            // Model isn't specified because relation is polymorphic
273
                                            $relationships['UnknownPolyMorphSide'][$method] =
274
                                                $biDir ? $relationObj : '\Illuminate\Database\Eloquent\Model|\Eloquent';
275
                                        } else {
276
                                            //Single model is returned
277
                                            $relationships['HasOne'][$method] = $biDir ? $relationObj : $relatedModel;
278
                                        }
279
                                        if (in_array($relation, ['morphMany', 'morphOne', 'morphToMany'])) {
280
                                            $relationships['KnownPolyMorphSide'][$method] =
281
                                                $biDir ? $relationObj : $relatedModel;
282
                                        }
283
                                        if (in_array($relation, ['morphedByMany'])) {
284
                                            $relationships['UnknownPolyMorphSide'][$method] =
285
                                                $biDir ? $relationObj : $relatedModel;
286
                                        }
287
                                    }
288
                                }
289
                            }
290
                        }
291
                    }
292
                }
293
            }
294
            static::$relationCategories[$biDirVal] = $relationships;
295
        }
296
        return static::$relationCategories[$biDirVal];
297
    }
298
299
    /**
300
     * Get the visible attributes for the model.
301
     *
302
     * @return array
303
     */
304
    abstract public function getVisible();
305
306
    /**
307
     * Get the hidden attributes for the model.
308
     *
309
     * @return array
310
     */
311
    abstract public function getHidden();
312
313
    /**
314
     * Get the primary key for the model.
315
     *
316
     * @return string
317
     */
318
    abstract public function getKeyName();
319
320
    /**
321
     * Get the current connection name for the model.
322
     *
323
     * @return string
324
     */
325
    abstract public function getConnectionName();
326
327
    /**
328
     * Get the database connection for the model.
329
     *
330
     * @return \Illuminate\Database\Connection
331
     */
332
    abstract public function getConnection();
333
334
    /**
335
     * Get all of the current attributes on the model.
336
     *
337
     * @return array
338
     */
339
    abstract public function getAttributes();
340
341
    /**
342
     * Get the table associated with the model.
343
     *
344
     * @return string
345
     */
346
    abstract public function getTable();
347
348
    /**
349
     * Get the fillable attributes for the model.
350
     *
351
     * @return array
352
     */
353
    abstract public function getFillable();
354
355
    /**
356
     * Dig up all defined getters on the model.
357
     *
358
     * @return array
359
     */
360
    protected function collectGetters()
361
    {
362
        $getterz = [];
363
        $methods = get_class_methods($this);
364
        foreach ($methods as $method) {
365
            if (12 < strlen($method) && 'get' == substr($method, 0, 3)) {
366
                if ('Attribute' == substr($method, -9)) {
367
                    $getterz[] = $method;
368
                }
369
            }
370
        }
371
        $methods = [];
372
373
        foreach ($getterz as $getter) {
374
            $residual = substr($getter, 3);
375
            $residual = substr($residual, 0, -9);
0 ignored issues
show
Bug introduced by
It seems like $residual can also be of type false; however, parameter $string of substr() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

375
            $residual = substr(/** @scrutinizer ignore-type */ $residual, 0, -9);
Loading history...
376
            $methods[] = $residual;
377
        }
378
        return $methods;
379
    }
380
381
    /**
382
     * @param       $foo
383
     * @param mixed $condition
384
     *
385
     * @return array
386
     */
387
    private function polyglotKeyMethodNames($foo, $condition = false)
388
    {
389
        $fkList = ['getQualifiedForeignKeyName', 'getForeignKey'];
390
        $rkList = ['getQualifiedRelatedKeyName', 'getOtherKey', 'getOwnerKey'];
391
392
        $fkMethodName = null;
393
        $rkMethodName = null;
394
        if ($condition) {
395
            if (array_key_exists(get_class($foo), static::$methodPrimary)) {
396
                $line = static::$methodPrimary[get_class($foo)];
397
                $fkMethodName = $line['fk'];
398
                $rkMethodName = $line['rk'];
399
            } else {
400
                $methodList = get_class_methods(get_class($foo));
401
                $fkMethodName = 'getQualifiedForeignPivotKeyName';
402
                foreach ($fkList as $option) {
403
                    if (in_array($option, $methodList)) {
404
                        $fkMethodName = $option;
405
                        break;
406
                    }
407
                }
408
                assert(
409
                    in_array($fkMethodName, $methodList),
410
                    'Selected method, ' . $fkMethodName . ', not in method list'
411
                );
412
                $rkMethodName = 'getQualifiedRelatedPivotKeyName';
413
                foreach ($rkList as $option) {
414
                    if (in_array($option, $methodList)) {
415
                        $rkMethodName = $option;
416
                        break;
417
                    }
418
                }
419
                assert(
420
                    in_array($rkMethodName, $methodList),
421
                    'Selected method, ' . $rkMethodName . ', not in method list'
422
                );
423
                $line = ['fk' => $fkMethodName, 'rk' => $rkMethodName];
424
                static::$methodPrimary[get_class($foo)] = $line;
425
            }
426
        }
427
        return [$fkMethodName, $rkMethodName];
428
    }
429
430
    private function polyglotKeyMethodBackupNames($foo, $condition = false)
431
    {
432
        $fkList = ['getForeignKey', 'getForeignKeyName', 'getQualifiedFarKeyName'];
433
        $rkList = ['getOtherKey', 'getQualifiedParentKeyName'];
434
435
        $fkMethodName = null;
436
        $rkMethodName = null;
437
        if ($condition) {
438
            if (array_key_exists(get_class($foo), static::$methodAlternate)) {
439
                $line = static::$methodAlternate[get_class($foo)];
440
                $fkMethodName = $line['fk'];
441
                $rkMethodName = $line['rk'];
442
            } else {
443
                $methodList = get_class_methods(get_class($foo));
444
                $fkCombo = array_values(array_intersect($fkList, $methodList));
445
                assert(1 <= count($fkCombo), 'Expected at least 1 element in foreign-key list, got ' . count($fkCombo));
446
                $fkMethodName = $fkCombo[0];
447
                assert(
448
                    in_array($fkMethodName, $methodList),
449
                    'Selected method, ' . $fkMethodName . ', not in method list'
450
                );
451
                $rkCombo = array_values(array_intersect($rkList, $methodList));
452
                assert(1 <= count($rkCombo), 'Expected at least 1 element in related-key list, got ' . count($rkCombo));
453
                $rkMethodName = $rkCombo[0];
454
                assert(
455
                    in_array($rkMethodName, $methodList),
456
                    'Selected method, ' . $rkMethodName . ', not in method list'
457
                );
458
                $line = ['fk' => $fkMethodName, 'rk' => $rkMethodName];
459
                static::$methodAlternate[get_class($foo)] = $line;
460
            }
461
        }
462
        return [$fkMethodName, $rkMethodName];
463
    }
464
465
    private function polyglotThroughKeyMethodNames(HasManyThrough $foo)
466
    {
467
        $thruList = ['getThroughKey', 'getQualifiedFirstKeyName'];
468
469
        $methodList = get_class_methods(get_class($foo));
470
        $thruCombo = array_values(array_intersect($thruList, $methodList));
471
        return $thruCombo[0];
472
    }
473
474
475
    /**
476
     * @param             $hooks
477
     * @param             $first
478
     * @param             $property
479
     * @param             $last
480
     * @param             $mult
481
     * @param             $targ
482
     * @param string|null $targ
483
     * @param null|mixed  $type
484
     */
485
    private function addRelationsHook(&$hooks, $first, $property, $last, $mult, $targ, $type = null, $through = null)
486
    {
487
        if (!isset($hooks[$first])) {
488
            $hooks[$first] = [];
489
        }
490
        if (!isset($hooks[$first][$targ])) {
491
            $hooks[$first][$targ] = [];
492
        }
493
        $hooks[$first][$targ][$property] = [
494
            'property' => $property,
495
            'local' => $last,
496
            'through' => $through,
497
            'multiplicity' => $mult,
498
            'type' => $type
499
        ];
500
    }
501
502
    /**
503
     * @param $rels
504
     * @param $hooks
505
     */
506
    private function getRelationshipsHasMany($rels, &$hooks)
507
    {
508
        foreach ($rels['HasMany'] as $property => $foo) {
509
            if ($foo instanceof MorphMany || $foo instanceof MorphToMany) {
510
                continue;
511
            }
512
            $mult = '*';
513
            $targ = get_class($foo->getRelated());
514
            $thruName = null;
515
            if ($foo instanceof HasManyThrough) {
516
                $isBelong = false;
517
                list($fkMethodAlternate, $rkMethodAlternate) = $this->polyglotKeyMethodBackupNames($foo, true);
518
                $thruName = $this->polyglotThroughKeyMethodNames($foo);
519
            } elseif ($foo instanceof BelongsToMany) {
520
                $isBelong = true;
521
                list($fkMethodName, $rkMethodName) = $this->polyglotKeyMethodNames($foo, $isBelong);
522
            } else {
523
                $isBelong = false;
524
                list($fkMethodAlternate, $rkMethodAlternate) = $this->polyglotKeyMethodBackupNames($foo, !$isBelong);
525
            }
526
527
            $keyRaw = $isBelong ? $foo->$fkMethodName() : $foo->$fkMethodAlternate();
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $fkMethodAlternate does not seem to be defined for all execution paths leading up to this point.
Loading history...
Comprehensibility Best Practice introduced by
The variable $fkMethodName does not seem to be defined for all execution paths leading up to this point.
Loading history...
528
            $keySegments = explode('.', $keyRaw);
529
            $keyName = $keySegments[count($keySegments)-1];
530
            $localRaw = $isBelong ? $foo->$rkMethodName() : $foo->$rkMethodAlternate();
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $rkMethodAlternate does not seem to be defined for all execution paths leading up to this point.
Loading history...
Comprehensibility Best Practice introduced by
The variable $rkMethodName does not seem to be defined for all execution paths leading up to this point.
Loading history...
531
            $localSegments = explode('.', $localRaw);
532
            $localName = $localSegments[count($localSegments)-1];
533
            if (null !== $thruName) {
534
                $thruRaw = $foo->$thruName();
535
                $thruSegments = explode('.', $thruRaw);
536
                $thruName = $thruSegments[count($thruSegments)-1];
537
            }
538
            $first = $keyName;
539
            $last = $localName;
540
            $this->addRelationsHook($hooks, $first, $property, $last, $mult, $targ, null, $thruName);
541
        }
542
    }
543
544
    /**
545
     * @param $rels
546
     * @param $hooks
547
     */
548
    private function getRelationshipsHasOne($rels, &$hooks)
549
    {
550
        foreach ($rels['HasOne'] as $property => $foo) {
551
            if ($foo instanceof MorphOne) {
552
                continue;
553
            }
554
            $isBelong = $foo instanceof BelongsTo;
555
            $mult = $isBelong ? '1' : '0..1';
556
            $targ = get_class($foo->getRelated());
557
558
            list($fkMethodName, $rkMethodName) = $this->polyglotKeyMethodNames($foo, $isBelong);
559
            list($fkMethodAlternate, $rkMethodAlternate) = $this->polyglotKeyMethodBackupNames($foo, !$isBelong);
560
561
            $keyName = $isBelong ? $foo->$fkMethodName() : $foo->$fkMethodAlternate();
562
            $keySegments = explode('.', $keyName);
563
            $keyName = $keySegments[count($keySegments)-1];
564
            $localRaw = $isBelong ? $foo->$rkMethodName() : $foo->$rkMethodAlternate();
565
            $localSegments = explode('.', $localRaw);
566
            $localName = $localSegments[count($localSegments)-1];
567
            $first = $isBelong ? $localName : $keyName;
568
            $last = $isBelong ? $keyName : $localName;
569
            $this->addRelationsHook($hooks, $first, $property, $last, $mult, $targ);
570
        }
571
    }
572
573
    /**
574
     * @param $rels
575
     * @param $hooks
576
     */
577
    private function getRelationshipsKnownPolyMorph($rels, &$hooks)
578
    {
579
        foreach ($rels['KnownPolyMorphSide'] as $property => $foo) {
580
            $isMany = $foo instanceof MorphToMany;
581
            $targ = get_class($foo->getRelated());
582
            $mult = $isMany ? '*' : $foo instanceof MorphMany ? '*' : '1';
583
            $mult = $foo instanceof MorphOne ? '0..1' : $mult;
584
585
            list($fkMethodName, $rkMethodName) = $this->polyglotKeyMethodNames($foo, $isMany);
586
            list($fkMethodAlternate, $rkMethodAlternate) = $this->polyglotKeyMethodBackupNames($foo, !$isMany);
587
588
            $keyRaw = $isMany ? $foo->$fkMethodName() : $foo->$fkMethodAlternate();
589
            $keySegments = explode('.', $keyRaw);
590
            $keyName = $keySegments[count($keySegments)-1];
591
            $localRaw = $isMany ? $foo->$rkMethodName() : $foo->$rkMethodAlternate();
592
            $localSegments = explode('.', $localRaw);
593
            $localName = $localSegments[count($localSegments)-1];
594
            $first = $isMany ? $keyName : $localName;
595
            $last = $isMany ? $localName : $keyName;
596
            $this->addRelationsHook($hooks, $first, $property, $last, $mult, $targ, 'unknown');
597
        }
598
    }
599
600
    /**
601
     * @param $rels
602
     * @param $hooks
603
     */
604
    private function getRelationshipsUnknownPolyMorph($rels, &$hooks)
605
    {
606
        foreach ($rels['UnknownPolyMorphSide'] as $property => $foo) {
607
            $isMany = $foo instanceof MorphToMany;
608
            $targ = get_class($foo->getRelated());
609
            $mult = $isMany ? '*' : '1';
610
611
            list($fkMethodName, $rkMethodName) = $this->polyglotKeyMethodNames($foo, $isMany);
612
            list($fkMethodAlternate, $rkMethodAlternate) = $this->polyglotKeyMethodBackupNames($foo, !$isMany);
613
614
            $keyRaw = $isMany ? $foo->$fkMethodName() : $foo->$fkMethodAlternate();
615
            $keySegments = explode('.', $keyRaw);
616
            $keyName = $keySegments[count($keySegments)-1];
617
            $localRaw = $isMany ? $foo->$rkMethodName() : $foo->$rkMethodAlternate();
618
            $localSegments = explode('.', $localRaw);
619
            $localName = $localSegments[count($localSegments)-1];
620
621
            $first = $keyName;
622
            $last = (isset($localName) && '' != $localName) ? $localName : $foo->getRelated()->getKeyName();
623
            $this->addRelationsHook($hooks, $first, $property, $last, $mult, $targ, 'known');
624
        }
625
    }
626
627
    /**
628
     * SUpplemental function to retrieve cast array for Laravel versions that do not supply hasCasts.
629
     *
630
     * @return array
631
     */
632
    public function retrieveCasts()
633
    {
634
        if (method_exists($this, 'getCasts')) {
635
            return $this->getCasts();
636
        }
637
        return $this->casts;
1 ignored issue
show
Bug Best Practice introduced by
The property casts does not exist on AlgoWeb\PODataLaravel\Models\MetadataTrait. Did you maybe forget to declare it?
Loading history...
638
    }
639
640
    /**
641
     * Return list of relations to be eager-loaded by Laravel query provider.
642
     *
643
     * @return array
644
     */
645
    public function getEagerLoad()
646
    {
647
        assert(is_array($this->loadEagerRelations));
0 ignored issues
show
Bug introduced by
The call to assert() has too few arguments starting with description. ( Ignorable by Annotation )

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

647
        /** @scrutinizer ignore-call */ 
648
        assert(is_array($this->loadEagerRelations));

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
648
        return $this->loadEagerRelations;
649
    }
650
651
    /**
652
     * Set list of relations to be eager-loaded.
653
     *
654
     * @param array $relations
655
     */
656
    public function setEagerLoad(array $relations)
657
    {
658
        $check = array_map('strval', $relations);
659
        assert($relations == $check, 'All supplied relations must be resolvable to strings');
660
        $this->loadEagerRelations = $relations;
661
    }
662
663
    /*
664
     * Is this model the known side of at least one polymorphic relation?
665
     */
666
    public function isKnownPolymorphSide()
667
    {
668
        // isKnownPolymorph needs to be checking KnownPolymorphSide results - if you're checking UnknownPolymorphSide,
669
        // you're turned around
670
        $rels = $this->getRelationshipsFromMethods();
671
        return !empty($rels['KnownPolyMorphSide']);
672
    }
673
674
    /*
675
     * Is this model on the unknown side of at least one polymorphic relation?
676
     */
677
    public function isUnknownPolymorphSide()
678
    {
679
        // isUnknownPolymorph needs to be checking UnknownPolymorphSide results - if you're checking KnownPolymorphSide,
680
        // you're turned around
681
        $rels = $this->getRelationshipsFromMethods();
682
        return !empty($rels['UnknownPolyMorphSide']);
683
    }
684
685
    /**
686
     * Extract entity gubbins detail for later downstream use.
687
     *
688
     * @return EntityGubbins
689
     */
690
    public function extractGubbins()
691
    {
692
        $multArray = [
693
            '*' => AssociationStubRelationType::MANY(),
694
            '1' => AssociationStubRelationType::ONE(),
695
            '0..1' => AssociationStubRelationType::NULL_ONE()
696
        ];
697
698
        $gubbins = new EntityGubbins();
699
        $gubbins->setName($this->getEndpointName());
0 ignored issues
show
Bug introduced by
It seems like $this->getEndpointName() can also be of type false; however, parameter $name of AlgoWeb\PODataLaravel\Mo...ntityGubbins::setName() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

699
        $gubbins->setName(/** @scrutinizer ignore-type */ $this->getEndpointName());
Loading history...
700
        $gubbins->setClassName(get_class($this));
701
702
        $lowerNames = [];
703
704
        $fields = $this->metadata();
705
        $entityFields = [];
706
        foreach ($fields as $name => $field) {
707
            if (in_array(strtolower($name), $lowerNames)) {
708
                $msg = 'Property names must be unique, without regard to case';
709
                throw new \Exception($msg);
710
            }
711
            $lowerNames[] = strtolower($name);
712
            $nuField = new EntityField();
713
            $nuField->setName($name);
714
            $nuField->setIsNullable($field['nullable']);
715
            $nuField->setReadOnly(false);
716
            $nuField->setCreateOnly(false);
717
            $nuField->setDefaultValue($field['default']);
718
            $nuField->setIsKeyField($this->getKeyName() == $name);
719
            $nuField->setFieldType(EntityFieldType::PRIMITIVE());
720
            $nuField->setPrimitiveType(new EntityFieldPrimitiveType($field['type']));
721
            $entityFields[$name] = $nuField;
722
        }
723
        $isEmpty = (0 === count($entityFields));
724
        if (!($isEmpty && $this->isRunningInArtisan())) {
725
            $gubbins->setFields($entityFields);
726
        }
727
728
        $rawRels = $this->getRelationships();
729
        $stubs = [];
730
        foreach ($rawRels as $key => $rel) {
731
            foreach ($rel as $rawName => $deets) {
732
                foreach ($deets as $relName => $relGubbins) {
733
                    if (in_array(strtolower($relName), $lowerNames)) {
734
                        $msg = 'Property names must be unique, without regard to case';
735
                        throw new \Exception($msg);
736
                    }
737
                    $lowerNames[] = strtolower($relName);
738
                    $gubbinsType = $relGubbins['type'];
739
                    $property = $relGubbins['property'];
740
                    $isPoly = isset($gubbinsType);
741
                    $targType = 'known' != $gubbinsType ? $rawName : null;
742
                    $stub = $isPoly ? new AssociationStubPolymorphic() : new AssociationStubMonomorphic();
743
                    $stub->setBaseType(get_class($this));
744
                    $stub->setRelationName($property);
745
                    $stub->setKeyField($relGubbins['local']);
746
                    $stub->setForeignField($targType ? $key : null);
747
                    $stub->setMultiplicity($multArray[$relGubbins['multiplicity']]);
748
                    $stub->setTargType($targType);
749
                    if (null !== $relGubbins['through']) {
750
                        $stub->setThroughField($relGubbins['through']);
751
                    }
752
                    assert($stub->isOk());
0 ignored issues
show
Bug introduced by
The call to assert() has too few arguments starting with description. ( Ignorable by Annotation )

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

752
                    /** @scrutinizer ignore-call */ 
753
                    assert($stub->isOk());

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
753
                    $stubs[$property] = $stub;
754
                }
755
            }
756
        }
757
        $gubbins->setStubs($stubs);
758
759
        return $gubbins;
760
    }
761
762
    public function synthLiteralPK()
763
    {
764
        if (!$this->isKnownPolymorphSide()) {
765
            return;
766
        }
767
        $fieldName = LaravelReadQuery::PK;
768
        $this->$fieldName = $this->getKey();
0 ignored issues
show
Bug introduced by
The method getKey() does not exist on AlgoWeb\PODataLaravel\Models\MetadataTrait. Did you maybe mean getKeyName()? ( Ignorable by Annotation )

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

768
        /** @scrutinizer ignore-call */ 
769
        $this->$fieldName = $this->getKey();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
769
    }
770
771
    public function isRunningInArtisan()
772
    {
773
        return App::runningInConsole() && !App::runningUnitTests();
774
    }
775
776
    protected function getTableColumns()
777
    {
778
        if (0 === count(self::$tableColumns)) {
779
            $table = $this->getTable();
780
            $connect = $this->getConnection();
781
            $builder = $connect->getSchemaBuilder();
782
            $columns = $builder->getColumnListing($table);
783
784
            self::$tableColumns = $columns;
785
        }
786
        return self::$tableColumns;
787
    }
788
789
    protected function getTableDoctrineColumns()
790
    {
791
        if (0 === count(self::$tableColumnsDoctrine)) {
792
            $table = $this->getTable();
793
            $connect = $this->getConnection();
794
            $columns = $connect->getDoctrineSchemaManager()->listTableColumns($table);
795
796
            self::$tableColumnsDoctrine = $columns;
797
        }
798
        return self::$tableColumnsDoctrine;
799
    }
800
801
    public function reset()
802
    {
803
        self::$tableData = [];
804
        self::$tableColumnsDoctrine = [];
805
        self::$tableColumns = [];
806
    }
807
}
808