Test Setup Failed
Pull Request — master (#48)
by Alex
03:24
created

MetadataTrait::polyglotKeyMethodBackupNames()   C

Complexity

Conditions 7
Paths 11

Size

Total Lines 36
Code Lines 27

Duplication

Lines 27
Ratio 75 %

Code Coverage

Tests 0
CRAP Score 56

Importance

Changes 0
Metric Value
dl 27
loc 36
ccs 0
cts 0
cp 0
rs 6.7272
c 0
b 0
f 0
cc 7
eloc 27
nc 11
nop 2
crap 56
1
<?php
2
namespace AlgoWeb\PODataLaravel\Models;
3
4
use Illuminate\Database\Eloquent\Relations\BelongsTo;
5
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
6
use Illuminate\Database\Eloquent\Relations\MorphMany;
7
use Illuminate\Database\Eloquent\Relations\MorphOne;
8
use Illuminate\Database\Eloquent\Relations\MorphToMany;
9
use Illuminate\Support\Facades\App as App;
10
use Illuminate\Database\Eloquent\Relations\Relation;
11
use POData\Providers\Metadata\ResourceStreamInfo;
12
use POData\Providers\Metadata\Type\EdmPrimitiveType;
13
use Illuminate\Database\Eloquent\Model;
14
15
trait MetadataTrait
16
{
17
    protected static $methodPrimary = [];
18
    protected static $methodAlternate = [];
19
20
    /*
21
     * Array to record mapping between doctrine types and OData types
22
     */
23
    protected $mapping = [
24
        'integer' => EdmPrimitiveType::INT32,
25
        'string' => EdmPrimitiveType::STRING,
26
        'datetime' => EdmPrimitiveType::DATETIME,
27
        'float' => EdmPrimitiveType::SINGLE,
28 3
        'decimal' => EdmPrimitiveType::DECIMAL,
29
        'text' => EdmPrimitiveType::STRING,
30 3
        'boolean' => EdmPrimitiveType::BOOLEAN,
31
        'blob' => "stream"
32
    ];
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
51
        $columns = $builder->getColumnListing($table);
52 1
        $mask = $this->metadataMask();
53 1
        $columns = array_intersect($columns, $mask);
54 1
55
        $tableData = [];
56 1
57
        $rawFoo = $connect->getDoctrineSchemaManager()->listTableColumns($table);
58 1
        $foo = [];
59 1
        $getters = $this->collectGetters();
60 1
61 1
        foreach ($rawFoo as $key => $val) {
62 1
            // Work around glitch in Doctrine when reading from MariaDB which added ` characters to root key value
63 1
            $key = trim($key, '`');
64 1
            $foo[$key] = $val;
65
        }
66 1
67
        foreach ($columns as $column) {
68
            // Doctrine schema manager returns columns with lowercased names
69
            $rawColumn = $foo[strtolower($column)];
70
            $nullable = !($rawColumn->getNotNull());
71
            $fillable = in_array($column, $this->getFillable());
72
            $rawType = $rawColumn->getType();
73 4
            $type = $rawType->getName();
74
            $tableData[$column] = ['type' => $type, 'nullable' => $nullable, 'fillable' => $fillable];
75 4
        }
76
77 4
        foreach ($getters as $get) {
78 4
            $tableData[$get] = ['type' => 'text', 'nullable' => false, 'fillable' => false];
79 4
        }
80 2
81 4
        return $tableData;
82 1
    }
83 1
84
    /*
85 4
     * Return the set of fields that are permitted to be in metadata
86
     * - following same visible-trumps-hidden guideline as Laravel
87
     */
88
    public function metadataMask()
89
    {
90
        $attribs = array_keys($this->getAllAttributes());
91 5
92
        $visible = $this->getVisible();
93 5
        $hidden = $this->getHidden();
94 5
        if (0 < count($visible)) {
95 1
            assert(!empty($visible));
96
            $attribs = array_intersect($visible, $attribs);
97
        } elseif (0 < count($hidden)) {
98 4
            assert(!empty($hidden));
99
            $attribs = array_diff($attribs, $hidden);
100 4
        }
101
102 4
        return $attribs;
103 4
    }
104 4
105 4
    /*
106 4
     * Get the endpoint name being exposed
107 4
     *
108
     */
109 4
    public function getEndpointName()
110 1
    {
111 1
        $endpoint = isset($this->endpoint) ? $this->endpoint : null;
112 1
113 1
        if (!isset($endpoint)) {
114
            $bitter = get_class();
115 4
            $name = substr($bitter, strrpos($bitter, '\\')+1);
116 4
            return strtolower($name);
117
        }
118 4
        return strtolower($endpoint);
119
    }
120
121
    /*
122
     * Assemble this model's OData metadata as xml schema
123
     */
124
    public function getXmlSchema($MetaNamespace = "Data")
125
    {
126
        $raw = $this->metadata();
127
        if ([] == $raw) {
128
            return null;
129
        }
130
131
        $metadata = App::make('metadata');
132
133
        $rf = new \ReflectionClass(get_class($this));
134
        $complex = $metadata->addEntityType($rf, $rf->getShortName(), $MetaNamespace);
135
        $keyName = $this->getKeyName();
136
        if (null != $keyName) {
137
            $metadata->addKeyProperty($complex, $keyName, $this->mapping[$raw[$keyName]['type']]);
138
        }
139
140
        foreach ($raw as $key => $secret) {
141
            if ($key == $keyName) {
142
                continue;
143
            }
144
            if ($secret['type'] == "blob") {
145
                $complex->setMediaLinkEntry(true);
146
                $streamInfo = new ResourceStreamInfo($key);
147
                assert($complex->isMediaLinkEntry());
148
                $complex->addNamedStream($streamInfo);
149
                continue;
150
            }
151
            $metadata->addPrimitiveProperty($complex, $key, $this->mapping[$secret['type']]); // tag as isBag?
152
        }
153
154
        return $complex;
155
    }
156
157
    public function hookUpRelationships($entityTypes, $resourceSets)
158
    {
159
        assert(is_array($entityTypes) && is_array($resourceSets), "Both entityTypes and resourceSets must be arrays");
160
        $metadata = App::make('metadata');
161
        $rel = $this->getRelationshipsFromMethods();
162
        $thisClass = get_class($this);
163
        $thisInTypes = array_key_exists($thisClass, $entityTypes);
164
        $thisInSets = array_key_exists($thisClass, $resourceSets);
165
166
        if (!($thisInSets && $thisInTypes)) {
167
            return $rel;
168
        }
169
170
        $resourceType = $entityTypes[$thisClass];
171
        // if $r is in $combined keys, then its in keyspaces of both $entityTypes and $resourceSets
172
        $combinedKeys = array_intersect(array_keys($entityTypes), array_keys($resourceSets));
173 View Code Duplication
        foreach ($rel["HasOne"] as $n => $r) {
174
            $r = trim($r, '\\');
175 3
            if (in_array($r, $combinedKeys)) {
176
                $targResourceSet = $resourceSets[$r];
177 3
                $metadata->addResourceReferenceProperty($resourceType, $n, $targResourceSet);
178
            }
179 3
        }
180 3 View Code Duplication
        foreach ($rel["HasMany"] as $n => $r) {
1 ignored issue
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
181 3
            $r = trim($r, '\\');
182 3
            if (in_array($r, $combinedKeys)) {
183 3
                $targResourceSet = $resourceSets[$r];
184 3
                $metadata->addResourceSetReferenceProperty($resourceType, $n, $targResourceSet);
185 3
            }
186 3
        }
187 3
        return $rel;
188 3
    }
189
190 3
    public function getRelationships()
191
    {
192 3
        $hooks = [];
193 3
194 3
        $rels = $this->getRelationshipsFromMethods(true);
195 3
196 3
        $this->getRelationshipsUnknownPolyMorph($rels, $hooks);
197 3
198 3
        $this->getRelationshipsKnownPolyMorph($rels, $hooks);
199 3
200 3
        $this->getRelationshipsHasOne($rels, $hooks);
201 3
202
        $this->getRelationshipsHasMany($rels, $hooks);
203 3
204 3
        return $hooks;
205 3
    }
206 3
207 3
    protected function getAllAttributes()
208 3
    {
209 3
        // Adapted from http://stackoverflow.com/a/33514981
210 3
        // $columns = $this->getFillable();
211
        // Another option is to get all columns for the table like so:
212 3
        $builder = $this->getConnection()->getSchemaBuilder();
213 3
        $columns = $builder->getColumnListing($this->getTable());
214 3
        // but it's safer to just get the fillable fields
215
216 3
        $attributes = $this->getAttributes();
217 3
218 3
        foreach ($columns as $column) {
219 3
            if (!array_key_exists($column, $attributes)) {
220 3
                $attributes[$column] = null;
221
            }
222 1
        }
223 3
224
        $methods = $this->collectGetters();
225 1
226
        foreach ($methods as $method) {
227 1
            $attributes[$method] = null;
228
        }
229 1
230
        return $attributes;
231 3
    }
232 2
233 2
    protected function getRelationshipsFromMethods($biDir = false)
234 3
    {
235 3
        $model = $this;
236 3
        $relationships = array(
237 3
            "HasOne" => array(),
238 3
            "UnknownPolyMorphSide"=>array(),
239 3
            "HasMany"=>array(),
240 3
            "KnownPolyMorphSide"=>array()
241
        );
242
        $methods = get_class_methods($model);
243
        if (!empty($methods)) {
244
            foreach ($methods as $method) {
245
                if (!method_exists('Illuminate\Database\Eloquent\Model', $method)
246
                ) {
247
                    //Use reflection to inspect the code, based on Illuminate/Support/SerializableClosure.php
248
                    $reflection = new \ReflectionMethod($model, $method);
249
250
                    $file = new \SplFileObject($reflection->getFileName());
251
                    $file->seek($reflection->getStartLine()-1);
252
                    $code = '';
253
                    while ($file->key() < $reflection->getEndLine()) {
254
                        $code .= $file->current();
255
                        $file->next();
256
                    }
257
258
                    $code = trim(preg_replace('/\s\s+/', '', $code));
259
                    assert(false !== stripos($code, 'function'), 'Function definition must have keyword \'function\'');
260
                    $begin = strpos($code, 'function(');
261
                    $code = substr($code, $begin, strrpos($code, '}')-$begin+1);
262
                    $lastCode = $code[strlen($code)-1];
263
                    assert("}" == $lastCode, "Final character of function definition must be closing brace");
264
                    foreach (array(
265
                                'hasMany',
266
                                'hasManyThrough',
267
                                'belongsToMany',
268
                                'hasOne',
269
                                'belongsTo',
270
                                'morphOne',
271
                                'morphTo',
272
                                'morphMany',
273
                                'morphToMany',
274
                                'morphedByMany'
275
                                ) as $relation) {
276
                        $search = '$this->'.$relation.'(';
277
                        if ($pos = stripos($code, $search)) {
278
                            //Resolve the relation's model to a Relation object.
279
                            $relationObj = $model->$method();
280
                            if ($relationObj instanceof Relation) {
281
                                $relatedModel = '\\'.get_class($relationObj->getRelated());
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 ($relation === "morphTo") {
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", "morphedByMany"])) {
302
                                    $relationships["KnownPolyMorphSide"][$method] =
303
                                        $biDir ? $relationObj : $relatedModel;
304
                                }
305
                                if (in_array($relation, ["morphToMany"])) {
306
                                    $relationships["UnknownPolyMorphSide"][$method] =
307
                                        $biDir ? $relationObj : $relatedModel;
308
                                }
309
                            }
310
                        }
311
                    }
312
                }
313
            }
314
        }
315
        return $relationships;
316
    }
317
318
    /**
319
     * Get the visible attributes for the model.
320
     *
321
     * @return array
322
     */
323
    public abstract function getVisible();
324
325
    /**
326
     * Get the hidden attributes for the model.
327
     *
328
     * @return array
329
     */
330
    public abstract function getHidden();
331
332
    /**
333
     * Get the primary key for the model.
334
     *
335
     * @return string
336
     */
337
    public abstract function getKeyName();
338
339
    /**
340
     * Get the current connection name for the model.
341
     *
342
     * @return string
343
     */
344
    public abstract function getConnectionName();
345
346
    /**
347
     * Get the database connection for the model.
348
     *
349
     * @return \Illuminate\Database\Connection
350
     */
351
    public abstract function getConnection();
352
353
    /**
354
     * Get all of the current attributes on the model.
355
     *
356
     * @return array
357
     */
358
    public abstract function getAttributes();
359
360
    /**
361
     * Get the table associated with the model.
362
     *
363
     * @return string
364
     */
365
    public abstract function getTable();
366
367
    /**
368
     * Get the fillable attributes for the model.
369
     *
370
     * @return array
371
     */
372
    public abstract function getFillable();
373
374
    /**
375
     * Dig up all defined getters on the model
376
     *
377
     * @return array
378
     */
379
    protected function collectGetters()
380
    {
381
        $getterz = [];
382
        $methods = get_class_methods($this);
383
        foreach ($methods as $method) {
384
            if (starts_with($method, 'get') && ends_with($method, 'Attribute') && 'getAttribute' != $method) {
385
                $getterz[] = $method;
386
            }
387
        }
388
        $methods = [];
389
390
        foreach ($getterz as $getter) {
391
            $residual = substr($getter, 3);
392
            $residual = substr($residual, 0, -9);
393
            $methods[] = $residual;
394
        }
395
        return $methods;
396
    }
397
398
    /**
399
     * @param $foo
400
     * @return array
401
     */
402
    private function polyglotKeyMethodNames($foo, $condition = false)
403
    {
404
        $fkList = ['getQualifiedForeignKeyName', 'getForeignKey'];
405
        $rkList = ['getQualifiedRelatedKeyName', 'getOtherKey', 'getOwnerKey'];
406
407
        $fkMethodName = null;
408
        $rkMethodName = null;
409 View Code Duplication
        if ($condition) {
410
            if (array_key_exists(get_class($foo), static::$methodPrimary)) {
411
                $line = static::$methodPrimary[get_class($foo)];
412
                $fkMethodName = $line['fk'];
413
                $rkMethodName = $line['rk'];
414
            } else {
415
                $methodList = get_class_methods(get_class($foo));
416
                $fkMethodName = 'getQualifiedForeignPivotKeyName';
417
                foreach ($fkList as $option) {
418
                    if (in_array($option, $methodList)) {
419
                        $fkMethodName = $option;
420
                        break;
421
                    }
422
                }
423
                assert(in_array($fkMethodName, $methodList), "Selected method, ".$fkMethodName.", not in method list");
424
                $rkMethodName = 'getQualifiedRelatedPivotKeyName';
425
                foreach ($rkList as $option) {
426
                    if (in_array($option, $methodList)) {
427
                        $rkMethodName = $option;
428
                        break;
429
                    }
430
                }
431
                assert(in_array($rkMethodName, $methodList), "Selected method, ".$rkMethodName.", not in method list");
432
                $line = ['fk' => $fkMethodName, 'rk' => $rkMethodName];
433
                static::$methodPrimary[get_class($foo)] = $line;
434
            }
435
436
        }
437
        return array($fkMethodName, $rkMethodName);
438
    }
439
440
    private function polyglotKeyMethodBackupNames($foo, $condition = false)
441
    {
442
        $fkList = ['getForeignKey'];
443
        $rkList = ['getOtherKey'];
444
445
        $fkMethodName = null;
446
        $rkMethodName = null;
447 View Code Duplication
        if ($condition) {
448
            if (array_key_exists(get_class($foo), static::$methodAlternate)) {
449
                $line = static::$methodAlternate[get_class($foo)];
450
                $fkMethodName = $line['fk'];
451
                $rkMethodName = $line['rk'];
452
            } else {
453
                $methodList = get_class_methods(get_class($foo));
454
                $fkMethodName = 'getForeignKeyName';
455
                foreach ($fkList as $option) {
456
                    if (in_array($option, $methodList)) {
457
                        $fkMethodName = $option;
458
                        break;
459
                    }
460
                }
461
                assert(in_array($fkMethodName, $methodList), "Selected method, ".$fkMethodName.", not in method list");
462
                $rkMethodName = 'getQualifiedParentKeyName';
463
                foreach ($rkList as $option) {
464
                    if (in_array($option, $methodList)) {
465
                        $rkMethodName = $option;
466
                        break;
467
                    }
468
                }
469
                assert(in_array($rkMethodName, $methodList), "Selected method, ".$rkMethodName.", not in method list");
470
                $line = ['fk' => $fkMethodName, 'rk' => $rkMethodName];
471
                static::$methodAlternate[get_class($foo)] = $line;
472
            }
473
        }
474
        return array($fkMethodName, $rkMethodName);
475
    }
476
477
    /**
478
     * @param $hooks
479
     * @param $first
480
     * @param $property
481
     * @param $last
482
     * @param $mult
483
     * @param $targ
484
     */
485
    private function addRelationsHook(&$hooks, $first, $property, $last, $mult, $targ)
486
    {
487
        if (!isset($hooks[$first])) {
488
            $hooks[$first] = [];
489
        }
490
        $hooks[$first][$targ] = [
491
            'property' => $property,
492
            'local' => $last,
493
            'multiplicity' => $mult
494
        ];
495
    }
496
497
    /**
498
     * @param $rels
499
     * @param $hooks
500
     */
501
    private function getRelationshipsHasMany($rels, &$hooks)
502
    {
503
        foreach ($rels['HasMany'] as $property => $foo) {
504
            if ($foo instanceof MorphMany || $foo instanceof MorphToMany) {
505
                continue;
506
            }
507
            $isBelong = $foo instanceof BelongsToMany;
508
            $mult = '*';
509
            $targ = get_class($foo->getRelated());
510
511
            list($fkMethodName, $rkMethodName) = $this->polyglotKeyMethodNames($foo, $isBelong);
512
            list($fkMethodAlternate, $rkMethodAlternate) = $this->polyglotKeyMethodBackupNames($foo, !$isBelong);
513
514
            $keyRaw = $isBelong ? $foo->$fkMethodName() : $foo->$fkMethodAlternate();
515
            $keySegments = explode('.', $keyRaw);
516
            $keyName = $keySegments[count($keySegments) - 1];
517
            $localRaw = $isBelong ? $foo->$rkMethodName() : $foo->$rkMethodAlternate();
518
            $localSegments = explode('.', $localRaw);
519
            $localName = $localSegments[count($localSegments) - 1];
520
            $first = $keyName;
521
            $last = $localName;
522
            $this->addRelationsHook($hooks, $first, $property, $last, $mult, $targ);
523
        }
524
    }
525
526
    /**
527
     * @param $rels
528
     * @param $hooks
529
     */
530
    private function getRelationshipsHasOne($rels, &$hooks)
531
    {
532
        foreach ($rels['HasOne'] as $property => $foo) {
533
            if ($foo instanceof MorphOne) {
534
                continue;
535
            }
536
            $isBelong = $foo instanceof BelongsTo;
537
            $mult = $isBelong ? '1' : '0..1';
538
            $targ = get_class($foo->getRelated());
539
540
            list($fkMethodName, $rkMethodName) = $this->polyglotKeyMethodNames($foo, $isBelong);
541
            list($fkMethodAlternate, $rkMethodAlternate) = $this->polyglotKeyMethodBackupNames($foo, !$isBelong);
542
543
            $keyName = $isBelong ? $foo->$fkMethodName() : $foo->$fkMethodAlternate();
544
            $localRaw = $isBelong ? $foo->$rkMethodName() : $foo->$rkMethodAlternate();
545
            $localSegments = explode('.', $localRaw);
546
            $localName = $localSegments[count($localSegments) - 1];
547
            $first = $isBelong ? $localName : $keyName;
548
            $last = $isBelong ? $keyName : $localName;
549
            $this->addRelationsHook($hooks, $first, $property, $last, $mult, $targ);
550
        }
551
    }
552
553
    /**
554
     * @param $rels
555
     * @param $hooks
556
     */
557
    private function getRelationshipsKnownPolyMorph($rels, &$hooks)
558
    {
559
        foreach ($rels['KnownPolyMorphSide'] as $property => $foo) {
560
            $isMany = $foo instanceof MorphToMany;
561
            $targ = get_class($foo->getRelated());
562
            $mult = $isMany ? '*' : $foo instanceof MorphMany ? '*' : '1';
563
            $mult = $foo instanceof MorphOne ? '0..1' : $mult;
564
565
            list($fkMethodName, $rkMethodName) = $this->polyglotKeyMethodNames($foo, $isMany);
566
            list($fkMethodAlternate, $rkMethodAlternate) = $this->polyglotKeyMethodBackupNames($foo, !$isMany);
567
568
            $keyRaw = $isMany ? $foo->$fkMethodName() : $foo->$fkMethodAlternate();
569
            $keySegments = explode('.', $keyRaw);
570
            $keyName = $keySegments[count($keySegments) - 1];
571
            $localRaw = $isMany ? $foo->$rkMethodName() : $foo->$rkMethodAlternate();
572
            $localSegments = explode('.', $localRaw);
573
            $localName = $localSegments[count($localSegments) - 1];
574
            $first = $isMany ? $keyName : $localName;
575
            $last = $isMany ? $localName : $keyName;
576
            $this->addRelationsHook($hooks, $first, $property, $last, $mult, $targ);
577
        }
578
    }
579
580
    /**
581
     * @param $rels
582
     * @param $hooks
583
     */
584
    private function getRelationshipsUnknownPolyMorph($rels, &$hooks)
585
    {
586
        foreach ($rels['UnknownPolyMorphSide'] as $property => $foo) {
587
            $isMany = $foo instanceof MorphToMany;
588
            $targ = get_class($foo->getRelated());
589
            $mult = $isMany ? '*' : '1';
590
591
            list($fkMethodName, $rkMethodName) = $this->polyglotKeyMethodNames($foo, $isMany);
592
            list($fkMethodAlternate, $rkMethodAlternate) = $this->polyglotKeyMethodBackupNames($foo, !$isMany);
593
594
            $keyRaw = $isMany ? $foo->$fkMethodName() : $foo->$fkMethodAlternate();
595
            $keySegments = explode('.', $keyRaw);
596
            $keyName = $keySegments[count($keySegments) - 1];
597
            $localRaw = $isMany ? $foo->$rkMethodName() : $foo->$rkMethodAlternate();
598
            $localSegments = explode('.', $localRaw);
599
            $localName = $localSegments[count($localSegments) - 1];
600
601
            $first = $keyName;
602
            $last = $localName;
603
            $this->addRelationsHook($hooks, $first, $property, $last, $mult, $targ);
604
        }
605
    }
606
}
607