Test Setup Failed
Pull Request — master (#46)
by Alex
03:05
created

MetadataTrait::getRelationshipsFromMethods()   D

Complexity

Conditions 17
Paths 2

Size

Total Lines 84
Code Lines 62

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 306

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 84
ccs 0
cts 0
cp 0
rs 4.8425
cc 17
eloc 62
nc 2
nop 1
crap 306

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
    /*
18
     * Array to record mapping between doctrine types and OData types
19
     */
20
    protected $mapping = [
21
        'integer' => EdmPrimitiveType::INT32,
22
        'string' => EdmPrimitiveType::STRING,
23
        'datetime' => EdmPrimitiveType::DATETIME,
24
        'float' => EdmPrimitiveType::SINGLE,
25
        'decimal' => EdmPrimitiveType::DECIMAL,
26
        'text' => EdmPrimitiveType::STRING,
27
        'boolean' => EdmPrimitiveType::BOOLEAN,
28 3
        'blob' => "stream"
29
    ];
30 3
31
    /*
32
     * Retrieve and assemble this model's metadata for OData packaging
33 2
     */
34 2
    public function metadata()
35
    {
36 2
        assert($this instanceof Model, get_class($this));
37
38 2
        // Break these out separately to enable separate reuse
39 1
        $connect = $this->getConnection();
40
        $builder = $connect->getSchemaBuilder();
41
42 1
        $table = $this->getTable();
43 1
44 1
        if (!$builder->hasTable($table)) {
45
            return [];
46 1
        }
47
48 1
        $columns = $builder->getColumnListing($table);
49 1
        $mask = $this->metadataMask();
50 1
        $columns = array_intersect($columns, $mask);
51
52 1
        $tableData = [];
53 1
54 1
        $rawFoo = $connect->getDoctrineSchemaManager()->listTableColumns($table);
55
        $foo = [];
56 1
        $getters = $this->collectGetters();
57
58 1
        foreach ($rawFoo as $key => $val) {
59 1
            // Work around glitch in Doctrine when reading from MariaDB which added ` characters to root key value
60 1
            $key = trim($key, '`');
61 1
            $foo[$key] = $val;
62 1
        }
63 1
64 1
        foreach ($columns as $column) {
65
            // Doctrine schema manager returns columns with lowercased names
66 1
            $rawColumn = $foo[strtolower($column)];
67
            $nullable = !($rawColumn->getNotNull());
68
            $fillable = in_array($column, $this->getFillable());
69
            $rawType = $rawColumn->getType();
70
            $type = $rawType->getName();
71
            $tableData[$column] = ['type' => $type, 'nullable' => $nullable, 'fillable' => $fillable];
72
        }
73 4
74
        foreach ($getters as $get) {
75 4
            $tableData[$get] = ['type' => 'text', 'nullable' => false, 'fillable' => false];
76
        }
77 4
78 4
        return $tableData;
79 4
    }
80 2
81 4
    /*
82 1
     * Return the set of fields that are permitted to be in metadata
83 1
     * - following same visible-trumps-hidden guideline as Laravel
84
     */
85 4
    public function metadataMask()
86
    {
87
        $attribs = array_keys($this->getAllAttributes());
88
89
        $visible = $this->getVisible();
90
        $hidden = $this->getHidden();
91 5
        if (0 < count($visible)) {
92
            assert(!empty($visible));
93 5
            $attribs = array_intersect($visible, $attribs);
94 5
        } elseif (0 < count($hidden)) {
95 1
            assert(!empty($hidden));
96
            $attribs = array_diff($attribs, $hidden);
97
        }
98 4
99
        return $attribs;
100 4
    }
101
102 4
    /*
103 4
     * Get the endpoint name being exposed
104 4
     *
105 4
     */
106 4
    public function getEndpointName()
107 4
    {
108
        $endpoint = isset($this->endpoint) ? $this->endpoint : null;
109 4
110 1
        if (!isset($endpoint)) {
111 1
            $bitter = get_class();
112 1
            $name = substr($bitter, strrpos($bitter, '\\')+1);
113 1
            return strtolower($name);
114
        }
115 4
        return strtolower($endpoint);
116 4
    }
117
118 4
    /*
119
     * Assemble this model's OData metadata as xml schema
120
     */
121
    public function getXmlSchema($MetaNamespace = "Data")
122
    {
123
        $raw = $this->metadata();
124
        if ([] == $raw) {
125
            return null;
126
        }
127
128
        $metadata = App::make('metadata');
129
130
        $rf = new \ReflectionClass(get_class($this));
131
        $complex = $metadata->addEntityType($rf, $rf->getShortName(), $MetaNamespace);
132
        $keyName = $this->getKeyName();
133
        if (null != $keyName) {
134
            $metadata->addKeyProperty($complex, $keyName, $this->mapping[$raw[$keyName]['type']]);
135
        }
136
137
        foreach ($raw as $key => $secret) {
138
            if ($key == $keyName) {
139
                continue;
140
            }
141
            if ($secret['type'] == "blob") {
142
                $complex->setMediaLinkEntry(true);
143
                $streamInfo = new ResourceStreamInfo($key);
144
                assert($complex->isMediaLinkEntry());
145
                $complex->addNamedStream($streamInfo);
146
                continue;
147
            }
148
            $metadata->addPrimitiveProperty($complex, $key, $this->mapping[$secret['type']]); // tag as isBag?
149
        }
150
151
        return $complex;
152
    }
153
154
    public function hookUpRelationships($entityTypes, $resourceSets)
155
    {
156
        assert(is_array($entityTypes) && is_array($resourceSets), "Both entityTypes and resourceSets must be arrays");
157
        $metadata = App::make('metadata');
158
        $rel = $this->getRelationshipsFromMethods();
159
        $thisClass = get_class($this);
160
        $thisInTypes = array_key_exists($thisClass, $entityTypes);
161
        $thisInSets = array_key_exists($thisClass, $resourceSets);
162
163
        if (!($thisInSets && $thisInTypes)) {
164
            return $rel;
165
        }
166
167
        $resourceType = $entityTypes[$thisClass];
168
        // if $r is in $combined keys, then its in keyspaces of both $entityTypes and $resourceSets
169
        $combinedKeys = array_intersect(array_keys($entityTypes), array_keys($resourceSets));
170 View Code Duplication
        foreach ($rel["HasOne"] as $n => $r) {
171
            $r = trim($r, '\\');
172
            if (in_array($r, $combinedKeys)) {
173
                $targResourceSet = $resourceSets[$r];
174
                $metadata->addResourceReferenceProperty($resourceType, $n, $targResourceSet);
175 3
            }
176
        }
177 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...
178
            $r = trim($r, '\\');
179 3
            if (in_array($r, $combinedKeys)) {
180 3
                $targResourceSet = $resourceSets[$r];
181 3
                $metadata->addResourceSetReferenceProperty($resourceType, $n, $targResourceSet);
182 3
            }
183 3
        }
184 3
        return $rel;
185 3
    }
186 3
187 3
    public function getRelationships()
188 3
    {
189
        $hooks = [];
190 3
191
        $rels = $this->getRelationshipsFromMethods(true);
192 3
193 3
        foreach ($rels['UnknownPolyMorphSide'] as $property => $foo) {
194 3
            $isMany = $foo instanceof MorphToMany;
195 3
            $targ = get_class($foo->getRelated());
196 3
            $mult = $isMany ? '*' : '1';
197 3
198 3
            if ($isMany) {
199 3
                $fkMethodName = method_exists($foo, 'getQualifiedForeignKeyName')
200 3
                    ? 'getQualifiedForeignKeyName' : 'getQualifiedForeignPivotKeyName';
201 3
                $rkMethodName = method_exists($foo, 'getQualifiedRelatedKeyName')
202
                    ? 'getQualifiedRelatedKeyName' : 'getQualifiedRelatedPivotKeyName';
203 3
            }
204 3
205 3
            $keyRaw = $isMany ? $foo->$fkMethodName() : $foo->getForeignKey();
1 ignored issue
show
Bug introduced by
The variable $fkMethodName does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
206 3
            $keySegments = explode('.', $keyRaw);
207 3
            $keyName = $keySegments[count($keySegments) - 1];
208 3
            $localRaw = $isMany ? $foo->$rkMethodName() : $foo->getQualifiedParentKeyName();
1 ignored issue
show
Bug introduced by
The variable $rkMethodName does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
209 3
            $localSegments = explode('.', $localRaw);
210 3
            $localName = $localSegments[count($localSegments) - 1];
211
212 3
            $first = $keyName;
213 3
            $last = $localName;
214 3
            if (!isset($hooks[$first])) {
215
                $hooks[$first] = [];
216 3
            }
217 3
            $hooks[$first][$targ] = [
218 3
                'property' => $property,
219 3
                'local' => $last,
220 3
                'multiplicity' => $mult
221
            ];
222 1
        }
223 3
224
        foreach ($rels['KnownPolyMorphSide'] as $property => $foo) {
225 1
            $isMany = $foo instanceof MorphToMany;
226
            $targ = get_class($foo->getRelated());
227 1
            $mult = $isMany ? '*' : $foo instanceof MorphMany ? '*' : '1';
228
            $mult = $foo instanceof MorphOne ? '0..1' : $mult;
229 1
230
            $keyRaw = $isMany ? $foo->getQualifiedForeignKeyName() : $foo->getForeignKeyName();
0 ignored issues
show
Bug introduced by
The method getQualifiedForeignKeyName() does not exist on Illuminate\Database\Eloquent\Relations\MorphToMany. Did you maybe mean getQualifiedForeignPivotKeyName()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
231 3
            $keySegments = explode('.', $keyRaw);
232 2
            $keyName = $keySegments[count($keySegments) - 1];
233 2
            $localRaw = $isMany ? $foo->getQualifiedRelatedKeyName() : $foo->getQualifiedParentKeyName();
0 ignored issues
show
Bug introduced by
The method getQualifiedRelatedKeyName() does not exist on Illuminate\Database\Eloquent\Relations\MorphToMany. Did you maybe mean getQualifiedRelatedPivotKeyName()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
234 3
            $localSegments = explode('.', $localRaw);
235 3
            $localName = $localSegments[count($localSegments) - 1];
236 3
            $first = $isMany ? $keyName : $localName;
237 3
            $last = $isMany ? $localName : $keyName;
238 3
            if (!isset($hooks[$first])) {
239 3
                $hooks[$first] = [];
240 3
            }
241
            $hooks[$first][$targ] = [
242
                'property' => $property,
243
                'local' => $last,
244
                'multiplicity' => $mult
245
            ];
246
        }
247
248
        foreach ($rels['HasOne'] as $property => $foo) {
249
            if ($foo instanceof MorphOne) {
250
                continue;
251
            }
252
            $isBelong = $foo instanceof BelongsTo;
253
            $mult = $isBelong ? '1' : '0..1';
254
            $targ = get_class($foo->getRelated());
255
            $keyName = $isBelong ? $foo->getForeignKey() : $foo->getForeignKeyName();
256
            $localRaw = $isBelong ? $foo->getOwnerKey() : $foo->getQualifiedParentKeyName();
257
            $localSegments = explode('.', $localRaw);
258
            $localName = $localSegments[count($localSegments) - 1];
259
            $first = $isBelong ? $localName : $keyName;
260
            $last = $isBelong ? $keyName : $localName;
261
            if (!isset($hooks[$first])) {
262
                $hooks[$first] = [];
263
            }
264
            $hooks[$first][$targ] = [
265
                'property' => $property,
266
                'local' => $last,
267
                'multiplicity' => $mult
268
            ];
269
        }
270
        foreach ($rels['HasMany'] as $property => $foo) {
271
            if ($foo instanceof MorphMany || $foo instanceof MorphToMany) {
272
                continue;
273
            }
274
            $isBelong = $foo instanceof BelongsToMany;
275
            $mult = '*';
276
            $targ = get_class($foo->getRelated());
277
            $keyRaw = $isBelong ? $foo->getQualifiedForeignKeyName() : $foo->getForeignKeyName();
0 ignored issues
show
Bug introduced by
The method getQualifiedForeignKeyName() does not exist on Illuminate\Database\Eloq...Relations\BelongsToMany. Did you maybe mean getQualifiedForeignPivotKeyName()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
278
            $keySegments = explode('.', $keyRaw);
279
            $keyName = $keySegments[count($keySegments) - 1];
280
            $localRaw = $isBelong ? $foo->getQualifiedRelatedKeyName() : $foo->getQualifiedParentKeyName();
0 ignored issues
show
Bug introduced by
The method getQualifiedRelatedKeyName() does not exist on Illuminate\Database\Eloq...Relations\BelongsToMany. Did you maybe mean getQualifiedRelatedPivotKeyName()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
281
            $localSegments = explode('.', $localRaw);
282
            $localName = $localSegments[count($localSegments) - 1];
283
            if (!isset($hooks[$keyName])) {
284
                $hooks[$keyName] = [];
285
            }
286
            $hooks[$keyName][$targ] = [
287
                'property' => $property,
288
                'local' => $localName,
289
                'multiplicity' => $mult
290
            ];
291
        }
292
293
        return $hooks;
294
    }
295
296
    protected function getAllAttributes()
297
    {
298
        // Adapted from http://stackoverflow.com/a/33514981
299
        // $columns = $this->getFillable();
300
        // Another option is to get all columns for the table like so:
301
        $builder = $this->getConnection()->getSchemaBuilder();
302
        $columns = $builder->getColumnListing($this->getTable());
303
        // but it's safer to just get the fillable fields
304
305
        $attributes = $this->getAttributes();
306
307
        foreach ($columns as $column) {
308
            if (!array_key_exists($column, $attributes)) {
309
                $attributes[$column] = null;
310
            }
311
        }
312
313
        $methods = $this->collectGetters();
314
315
        foreach ($methods as $method) {
316
            $attributes[$method] = null;
317
        }
318
319
        return $attributes;
320
    }
321
322
    protected function getRelationshipsFromMethods($biDir = false)
323
    {
324
        $model = $this;
325
        $relationships = array(
326
            "HasOne" => array(),
327
            "UnknownPolyMorphSide"=>array(),
328
            "HasMany"=>array(),
329
            "KnownPolyMorphSide"=>array()
330
        );
331
        $methods = get_class_methods($model);
332
        if (!empty($methods)) {
333
            foreach ($methods as $method) {
334
                if (!method_exists('Illuminate\Database\Eloquent\Model', $method)
335
                ) {
336
                    //Use reflection to inspect the code, based on Illuminate/Support/SerializableClosure.php
337
                    $reflection = new \ReflectionMethod($model, $method);
338
339
                    $file = new \SplFileObject($reflection->getFileName());
340
                    $file->seek($reflection->getStartLine()-1);
341
                    $code = '';
342
                    while ($file->key() < $reflection->getEndLine()) {
343
                        $code .= $file->current();
344
                        $file->next();
345
                    }
346
347
                    $code = trim(preg_replace('/\s\s+/', '', $code));
348
                    assert(false !== stripos($code, 'function'), 'Function definition must have keyword \'function\'');
349
                    $begin = strpos($code, 'function(');
350
                    $code = substr($code, $begin, strrpos($code, '}')-$begin+1);
351
                    $lastCode = $code[strlen($code)-1];
352
                    assert("}" == $lastCode, "Final character of function definition must be closing brace");
353
                    foreach (array(
354
                                'hasMany',
355
                                'hasManyThrough',
356
                                'belongsToMany',
357
                                'hasOne',
358
                                'belongsTo',
359
                                'morphOne',
360
                                'morphTo',
361
                                'morphMany',
362
                                'morphToMany',
363
                                'morphedByMany'
364
                                ) as $relation) {
365
                        $search = '$this->'.$relation.'(';
366
                        if ($pos = stripos($code, $search)) {
367
                            //Resolve the relation's model to a Relation object.
368
                            $relationObj = $model->$method();
369
                            if ($relationObj instanceof Relation) {
370
                                $relatedModel = '\\'.get_class($relationObj->getRelated());
371
                                $relations = [
372
                                    'hasManyThrough',
373
                                    'belongsToMany',
374
                                    'hasMany',
375
                                    'morphMany',
376
                                    'morphToMany',
377
                                    'morphedByMany'
378
                                ];
379
                                if (in_array($relation, $relations)) {
380
                                    //Collection or array of models (because Collection is Arrayable)
381
                                    $relationships["HasMany"][$method] = $biDir ? $relationObj : $relatedModel;
382
                                } elseif ($relation === "morphTo") {
383
                                    // Model isn't specified because relation is polymorphic
384
                                    $relationships["UnknownPolyMorphSide"][$method] =
385
                                        $biDir ? $relationObj : '\Illuminate\Database\Eloquent\Model|\Eloquent';
386
                                } else {
387
                                    //Single model is returned
388
                                    $relationships["HasOne"][$method] = $biDir ? $relationObj : $relatedModel;
389
                                }
390
                                if (in_array($relation, ["morphMany", "morphOne", "morphedByMany"])) {
391
                                    $relationships["KnownPolyMorphSide"][$method] =
392
                                        $biDir ? $relationObj : $relatedModel;
393
                                }
394
                                if (in_array($relation, ["morphToMany"])) {
395
                                    $relationships["UnknownPolyMorphSide"][$method] =
396
                                        $biDir ? $relationObj : $relatedModel;
397
                                }
398
                            }
399
                        }
400
                    }
401
                }
402
            }
403
        }
404
        return $relationships;
405
    }
406
407
    /**
408
     * Get the visible attributes for the model.
409
     *
410
     * @return array
411
     */
412
    public abstract function getVisible();
413
414
    /**
415
     * Get the hidden attributes for the model.
416
     *
417
     * @return array
418
     */
419
    public abstract function getHidden();
420
421
    /**
422
     * Get the primary key for the model.
423
     *
424
     * @return string
425
     */
426
    public abstract function getKeyName();
427
428
    /**
429
     * Get the current connection name for the model.
430
     *
431
     * @return string
432
     */
433
    public abstract function getConnectionName();
434
435
    /**
436
     * Get the database connection for the model.
437
     *
438
     * @return \Illuminate\Database\Connection
439
     */
440
    public abstract function getConnection();
441
442
    /**
443
     * Get all of the current attributes on the model.
444
     *
445
     * @return array
446
     */
447
    public abstract function getAttributes();
448
449
    /**
450
     * Get the table associated with the model.
451
     *
452
     * @return string
453
     */
454
    public abstract function getTable();
455
456
    /**
457
     * Get the fillable attributes for the model.
458
     *
459
     * @return array
460
     */
461
    public abstract function getFillable();
462
463
    /**
464
     * Dig up all defined getters on the model
465
     *
466
     * @return array
467
     */
468
    protected function collectGetters()
469
    {
470
        $getterz = [];
471
        $methods = get_class_methods($this);
472
        foreach ($methods as $method) {
473
            if (starts_with($method, 'get') && ends_with($method, 'Attribute') && 'getAttribute' != $method) {
474
                $getterz[] = $method;
475
            }
476
        }
477
        $methods = [];
478
479
        foreach ($getterz as $getter) {
480
            $residual = substr($getter, 3);
481
            $residual = substr($residual, 0, -9);
482
            $methods[] = $residual;
483
        }
484
        return $methods;
485
    }
486
}
487