Completed
Pull Request — master (#219)
by Christopher
08:02 queued 01:57
created

MetadataRelationsTrait   C

Complexity

Total Complexity 54

Size/Duplication

Total Lines 299
Duplicated Lines 0 %

Importance

Changes 9
Bugs 0 Features 0
Metric Value
wmc 54
eloc 132
c 9
b 0
f 0
dl 0
loc 299
rs 6.4799

10 Methods

Rating   Name   Duplication   Size   Complexity  
A isUnknownPolymorphSide() 0 6 1
A getRelationships() 0 19 2
A isKnownPolymorphSide() 0 6 1
A getCodeForMethod() 0 25 4
F getRelationshipsFromMethods() 0 58 20
A addRelationsHook() 0 22 3
A getRelationshipsHasOne() 0 19 6
B getRelationshipsKnownPolyMorph() 0 17 7
A getRelationshipsHasMany() 0 21 5
A getRelationshipsUnknownPolyMorph() 0 17 5

How to fix   Complexity   

Complex Class

Complex classes like MetadataRelationsTrait often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use MetadataRelationsTrait, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * Created by PhpStorm.
4
 * User: alex
5
 * Date: 13/02/20
6
 * Time: 1:08 PM.
7
 */
8
namespace AlgoWeb\PODataLaravel\Models;
9
10
use Illuminate\Database\Eloquent\Model;
11
use Illuminate\Database\Eloquent\Relations\BelongsTo;
12
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
13
use Illuminate\Database\Eloquent\Relations\MorphMany;
14
use Illuminate\Database\Eloquent\Relations\MorphOne;
15
use Illuminate\Database\Eloquent\Relations\MorphToMany;
16
use Illuminate\Database\Eloquent\Relations\Relation;
17
use POData\Common\InvalidOperationException;
18
19
trait MetadataRelationsTrait
20
{
21
    use MetadataKeyMethodNamesTrait;
22
23
    protected static $relationHooks = [];
24
    protected static $relationCategories = [];
25
26
27
    /**
28
     * Get model's relationships.
29
     *
30
     * @throws InvalidOperationException
31
     * @throws \ReflectionException
32
     * @return array
33
     */
34
    public function getRelationships()
35
    {
36
        if (empty(static::$relationHooks)) {
37
            $hooks = [];
38
39
            $rels = $this->getRelationshipsFromMethods(true);
40
41
            $this->getRelationshipsUnknownPolyMorph($rels, $hooks);
42
43
            $this->getRelationshipsKnownPolyMorph($rels, $hooks);
44
45
            $this->getRelationshipsHasOne($rels, $hooks);
46
47
            $this->getRelationshipsHasMany($rels, $hooks);
48
49
            static::$relationHooks = $hooks;
50
        }
51
52
        return static::$relationHooks;
53
    }
54
55
    /**
56
     * Is this model the known side of at least one polymorphic relation?
57
     *
58
     * @throws InvalidOperationException
59
     * @throws \ReflectionException
60
     */
61
    public function isKnownPolymorphSide()
62
    {
63
        // isKnownPolymorph needs to be checking KnownPolymorphSide results - if you're checking UnknownPolymorphSide,
64
        // you're turned around
65
        $rels = $this->getRelationshipsFromMethods();
66
        return !empty($rels['KnownPolyMorphSide']);
67
    }
68
69
    /**
70
     * Is this model on the unknown side of at least one polymorphic relation?
71
     *
72
     * @throws InvalidOperationException
73
     * @throws \ReflectionException
74
     */
75
    public function isUnknownPolymorphSide()
76
    {
77
        // isUnknownPolymorph needs to be checking UnknownPolymorphSide results - if you're checking KnownPolymorphSide,
78
        // you're turned around
79
        $rels = $this->getRelationshipsFromMethods();
80
        return !empty($rels['UnknownPolyMorphSide']);
81
    }
82
    /**
83
     * @param \ReflectionMethod $method
84
     * @return string
85
     * @throws InvalidOperationException
86
     */
87
    protected function getCodeForMethod(\ReflectionMethod $method) : string
88
    {
89
        $fileName = $method->getFileName();
90
91
        $file = new \SplFileObject($fileName);
92
        $file->seek($method->getStartLine() - 1);
93
        $code = '';
94
        while ($file->key() < $method->getEndLine()) {
95
            $code .= $file->current();
96
            $file->next();
97
        }
98
99
        $code = trim(preg_replace('/\s\s+/', '', $code));
100
        if (false === stripos($code, 'function')) {
101
            $msg = 'Function definition must have keyword \'function\'';
102
            throw new InvalidOperationException($msg);
103
        }
104
        $begin = strpos($code, 'function(');
105
        $code = substr($code, $begin, strrpos($code, '}') - $begin + 1);
106
        $lastCode = $code[strlen($code) - 1];
107
        if ('}' != $lastCode) {
108
            $msg = 'Final character of function definition must be closing brace';
109
            throw new InvalidOperationException($msg);
110
        }
111
        return $code;
112
    }
113
    /**
114
     * @param bool $biDir
115
     *
116
     * @throws InvalidOperationException
117
     * @throws \ReflectionException
118
     * @return array
119
     */
120
    protected function getRelationshipsFromMethods(bool $biDir = false)
121
    {
122
        $biDirVal = intval($biDir);
123
        $isCached = isset(static::$relationCategories[$biDirVal]) && !empty(static::$relationCategories[$biDirVal]);
124
        if ($isCached) {
125
            return static::$relationCategories[$biDirVal];
126
        }
127
        /** @var Model $model */
128
        $model = $this;
129
        $relationships = [
130
            'HasOne' => [],
131
            'UnknownPolyMorphSide' => [],
132
            'HasMany' => [],
133
            'KnownPolyMorphSide' => []
134
        ];
135
        $methods = $this->getModelClassMethods($model);
136
        foreach ($methods as $method) {
137
            //Use reflection to inspect the code, based on Illuminate/Support/SerializableClosure.php
138
            $reflection = new \ReflectionMethod($model, $method);
139
            $code = $this->getCodeForMethod($reflection);
140
            foreach (static::$relTypes as $relation) {
141
                //Resolve the relation's model to a Relation object.
142
                if (
143
                    !stripos($code, sprintf('$this->%s(', $relation)) ||
144
                    !(($relationObj = $model->$method()) instanceof Relation) ||
145
                    !in_array(MetadataTrait::class, class_uses($relObject = $relationObj->getRelated()))
146
                ) {
147
                    continue;
148
                }
149
                $targObject = $biDir ? $relationObj : '\\' . get_class($relObject);
150
                switch ($relation) {
151
                    case 'morphedByMany':
152
                        $relationships['UnknownPolyMorphSide'][$method] = $targObject;
153
                        $relationships['HasMany'][$method] = $targObject;
154
                        break;
155
                    case 'morphMany':
156
                    case 'morphToMany':
157
                        $relationships['KnownPolyMorphSide'][$method] = $targObject;
158
                    case 'hasMany':
159
                    case 'hasManyThrough':
160
                    case 'belongsToMany':
161
                    //Collection or array of models (because Collection is Arrayable)
162
                $relationships['HasMany'][$method] = $targObject;
163
                    break;
164
                    case 'morphOne':
165
                        $relationships['KnownPolyMorphSide'][$method] = $targObject;
166
                    case 'hasOne':
167
                    case 'belongsTo':
168
                        $relationships['HasOne'][$method] = $targObject;
169
                        break;
170
                    case 'morphTo':
171
                        // Model isn't specified because relation is polymorphic
172
                        $relationships['UnknownPolyMorphSide'][$method] =
173
                            $biDir ? $relationObj : '\Illuminate\Database\Eloquent\Model|\Eloquent';
174
                }
175
            }
176
        }
177
        return static::$relationCategories[$biDirVal] = $relationships;
178
    }
179
180
    /**
181
     * @param  array                     $rels
182
     * @param  array                     $hooks
183
     * @throws InvalidOperationException
184
     */
185
    protected function getRelationshipsHasMany(array $rels, array &$hooks)
186
    {
187
        /**
188
         * @var string   $property
189
         * @var Relation $relation
190
         */
191
        foreach ($rels['HasMany'] as $property => $relation) {
192
            if ($relation instanceof MorphMany || $relation instanceof MorphToMany) {
193
                continue;
194
            }
195
            $mult = '*';
196
            $targ = get_class($relation->getRelated());
197
            $keyName = $this->polyglotFkKey($relation);
198
            $localName = $this->polyglotRkKey($relation);
199
            $thruName = $relation instanceof HasManyThrough ?
200
                $this->polyglotThroughKey($relation) :
201
                null;
202
203
            $first = $keyName;
204
            $last = $localName;
205
            $this->addRelationsHook($hooks, $first, $property, $last, $mult, $targ, null, $thruName);
206
        }
207
    }
208
209
    /**
210
     * @param  array                     $rels
211
     * @param  array                     $hooks
212
     * @throws InvalidOperationException
213
     */
214
    protected function getRelationshipsHasOne(array $rels, array &$hooks)
215
    {
216
        /**
217
         * @var string   $property
218
         * @var Relation $foo
219
         */
220
        foreach ($rels['HasOne'] as $property => $foo) {
221
            if ($foo instanceof MorphOne) {
222
                continue;
223
            }
224
            $isBelong = $foo instanceof BelongsTo;
225
            $mult = $isBelong ? '1' : '0..1';
226
            $targ = get_class($foo->getRelated());
227
228
            $keyName = $this->polyglotFkKey($foo);
229
            $localName = $this->polyglotRkKey($foo);
230
            $first = $isBelong ? $localName : $keyName;
231
            $last = $isBelong ? $keyName : $localName;
232
            $this->addRelationsHook($hooks, $first, $property, $last, $mult, $targ);
233
        }
234
    }
235
236
    /**
237
     * @param  array                     $rels
238
     * @param  array                     $hooks
239
     * @throws InvalidOperationException
240
     */
241
    protected function getRelationshipsKnownPolyMorph(array $rels, array &$hooks)
242
    {
243
        /**
244
         * @var string   $property
245
         * @var Relation $foo
246
         */
247
        foreach ($rels['KnownPolyMorphSide'] as $property => $foo) {
248
            $isMany = $foo instanceof MorphToMany;
249
            $targ = get_class($foo->getRelated());
250
            $mult = $isMany || $foo instanceof MorphMany ? '*' : '1';
251
            $mult = $foo instanceof MorphOne ? '0..1' : $mult;
252
253
            $keyName = $this->polyglotFkKey($foo);
254
            $localName = $this->polyglotRkKey($foo);
255
            $first = $isMany ? $keyName : $localName;
256
            $last = $isMany ? $localName : $keyName;
257
            $this->addRelationsHook($hooks, $first, $property, $last, $mult, $targ, 'unknown');
258
        }
259
    }
260
261
    /**
262
     * @param  array                     $rels
263
     * @param  array                     $hooks
264
     * @throws InvalidOperationException
265
     */
266
    protected function getRelationshipsUnknownPolyMorph(array $rels, array &$hooks)
267
    {
268
        /**
269
         * @var string   $property
270
         * @var Relation $foo
271
         */
272
        foreach ($rels['UnknownPolyMorphSide'] as $property => $foo) {
273
            $isMany = $foo instanceof MorphToMany;
274
            $targ = get_class($foo->getRelated());
275
            $mult = $isMany ? '*' : '1';
276
277
            $keyName = $this->polyglotFkKey($foo);
278
            $localName = $this->polyglotRkKey($foo);
279
280
            $first = $keyName;
281
            $last = (isset($localName) && '' != $localName) ? $localName : $foo->getRelated()->getKeyName();
282
            $this->addRelationsHook($hooks, $first, $property, $last, $mult, $targ, 'known');
283
        }
284
    }
285
286
    /**
287
     * @param             $hooks
288
     * @param             $foreignField
289
     * @param             $RelationName
290
     * @param             $localKey
291
     * @param             $mult
292
     * @param string|null $relatedObject
293
     * @param mixed|null  $type
294
     * @param mixed|null  $through
295
     */
296
    protected function addRelationsHook(
297
        array &$hooks,
298
        $foreignField,
299
        $RelationName,
300
        $localKey,
301
        $mult,
302
        $relatedObject,
303
        $type = null,
304
        $through = null
305
    ) {
306
        if (!isset($hooks[$foreignField])) {
307
            $hooks[$foreignField] = [];
308
        }
309
        if (!isset($hooks[$foreignField][$relatedObject])) {
310
            $hooks[$foreignField][$relatedObject] = [];
311
        }
312
        $hooks[$foreignField][$relatedObject][$RelationName] = [
313
            'property' => $RelationName,
314
            'local' => $localKey,
315
            'through' => $through,
316
            'multiplicity' => $mult,
317
            'type' => $type
318
        ];
319
    }
320
}
321