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

MetadataTrait::getEndpointName()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 0
Metric Value
eloc 6
c 0
b 0
f 0
dl 0
loc 10
ccs 0
cts 8
cp 0
rs 10
cc 3
nc 4
nop 0
crap 12
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