Passed
Pull Request — master (#201)
by Alex
07:32
created

getRelationshipsKnownPolyMorph()   B

Complexity

Conditions 9
Paths 129

Size

Total Lines 24
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 16
c 1
b 0
f 0
dl 0
loc 24
rs 7.8138
cc 9
nc 129
nop 2
1
<?php
2
/**
3
 * Created by PhpStorm.
4
 * User: alex
5
 * Date: 13/02/20
6
 * Time: 1:08 PM
7
 */
8
9
namespace AlgoWeb\PODataLaravel\Models;
10
11
use Illuminate\Database\Eloquent\Model;
12
use Illuminate\Database\Eloquent\Relations\BelongsTo;
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
     * @return array
31
     * @throws InvalidOperationException
32
     * @throws \ReflectionException
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
    /**
84
     * @param bool $biDir
85
     *
86
     * @return array
87
     * @throws InvalidOperationException
88
     * @throws \ReflectionException
89
     */
90
    protected function getRelationshipsFromMethods($biDir = false)
91
    {
92
        $biDirVal = intval($biDir);
93
        $isCached = isset(static::$relationCategories[$biDirVal]) && !empty(static::$relationCategories[$biDirVal]);
94
        if (!$isCached) {
95
            /** @var Model $model */
96
            $model = $this;
97
            $relationships = [
98
                'HasOne' => [],
99
                'UnknownPolyMorphSide' => [],
100
                'HasMany' => [],
101
                'KnownPolyMorphSide' => []
102
            ];
103
            $methods = $this->getModelClassMethods($model);
104
            foreach ($methods as $method) {
105
                //Use reflection to inspect the code, based on Illuminate/Support/SerializableClosure.php
106
                $reflection = new \ReflectionMethod($model, $method);
107
                $fileName = $reflection->getFileName();
108
109
                $file = new \SplFileObject($fileName);
110
                $file->seek($reflection->getStartLine()-1);
111
                $code = '';
112
                while ($file->key() < $reflection->getEndLine()) {
113
                    $code .= $file->current();
114
                    $file->next();
115
                }
116
117
                $code = trim(preg_replace('/\s\s+/', '', $code));
118
                if (false === stripos($code, 'function')) {
119
                    $msg = 'Function definition must have keyword \'function\'';
120
                    throw new InvalidOperationException($msg);
121
                }
122
                $begin = strpos($code, 'function(');
123
                $code = substr($code, /** @scrutinizer ignore-type */$begin, strrpos($code, '}')-$begin+1);
124
                $lastCode = $code[strlen(/** @scrutinizer ignore-type */$code)-1];
125
                if ('}' != $lastCode) {
126
                    $msg = 'Final character of function definition must be closing brace';
127
                    throw new InvalidOperationException($msg);
128
                }
129
                foreach (static::$relTypes as $relation) {
130
                    $search = '$this->' . $relation . '(';
131
                    $found = stripos(/** @scrutinizer ignore-type */$code, $search);
132
                    if (!$found) {
133
                        continue;
134
                    }
135
                    //Resolve the relation's model to a Relation object.
136
                    $relationObj = $model->$method();
137
                    if (!($relationObj instanceof Relation)) {
138
                        continue;
139
                    }
140
                    $relObject = $relationObj->getRelated();
141
                    $relatedModel = '\\' . get_class($relObject);
142
                    if (!in_array(MetadataTrait::class, class_uses($relatedModel))) {
143
                        continue;
144
                    }
145
                    $targObject = $biDir ? $relationObj : $relatedModel;
146
                    if (in_array($relation, static::$manyRelTypes)) {
147
                        //Collection or array of models (because Collection is Arrayable)
148
                        $relationships['HasMany'][$method] = $targObject;
149
                    } elseif ('morphTo' === $relation) {
150
                        // Model isn't specified because relation is polymorphic
151
                        $relationships['UnknownPolyMorphSide'][$method] =
152
                            $biDir ? $relationObj : '\Illuminate\Database\Eloquent\Model|\Eloquent';
153
                    } else {
154
                        //Single model is returned
155
                        $relationships['HasOne'][$method] = $targObject;
156
                    }
157
                    if (in_array($relation, ['morphMany', 'morphOne', 'morphToMany'])) {
158
                        $relationships['KnownPolyMorphSide'][$method] = $targObject;
159
                    }
160
                    if (in_array($relation, ['morphedByMany'])) {
161
                        $relationships['UnknownPolyMorphSide'][$method] = $targObject;
162
                    }
163
                }
164
            }
165
            static::$relationCategories[$biDirVal] = $relationships;
166
        }
167
        return static::$relationCategories[$biDirVal];
168
    }
169
170
    /**
171
     * @param $rels
172
     * @param $hooks
173
     * @throws InvalidOperationException
174
     */
175
    protected function getRelationshipsHasMany($rels, &$hooks)
176
    {
177
        /**
178
         * @var string $property
179
         * @var Relation $foo
180
         */
181
        foreach ($rels['HasMany'] as $property => $foo) {
182
            if ($foo instanceof MorphMany || $foo instanceof MorphToMany) {
183
                continue;
184
            }
185
            $mult = '*';
186
            $targ = get_class($foo->getRelated());
187
            list($thruName, $fkMethodName, $rkMethodName) = $this->getRelationsHasManyKeyNames($foo);
188
189
            $keyRaw = $foo->$fkMethodName();
190
            $keySegments = explode('.', $keyRaw);
191
            $keyName = $keySegments[count($keySegments) - 1];
192
            $localRaw = $foo->$rkMethodName();
193
            $localSegments = explode('.', $localRaw);
194
            $localName = $localSegments[count($localSegments) - 1];
195
            if (null !== $thruName) {
196
                $thruRaw = $foo->$thruName();
197
                $thruSegments = explode('.', $thruRaw);
198
                $thruName = $thruSegments[count($thruSegments) - 1];
199
            }
200
            $first = $keyName;
201
            $last = $localName;
202
            $this->addRelationsHook($hooks, $first, $property, $last, $mult, $targ, null, $thruName);
203
        }
204
    }
205
206
    /**
207
     * @param $rels
208
     * @param $hooks
209
     * @throws InvalidOperationException
210
     */
211
    protected function getRelationshipsHasOne($rels, &$hooks)
212
    {
213
        /**
214
         * @var string $property
215
         * @var Relation $foo
216
         */
217
        foreach ($rels['HasOne'] as $property => $foo) {
218
            if ($foo instanceof MorphOne) {
219
                continue;
220
            }
221
            $isBelong = $foo instanceof BelongsTo;
222
            $mult = $isBelong ? '1' : '0..1';
223
            $targ = get_class($foo->getRelated());
224
225
            list($fkMethodName, $rkMethodName) = $this->polyglotKeyMethodNames($foo, $isBelong);
226
            list($fkMethodAlternate, $rkMethodAlternate) = $this->polyglotKeyMethodBackupNames($foo, !$isBelong);
227
228
            $keyName = $isBelong ? $foo->$fkMethodName() : $foo->$fkMethodAlternate();
229
            $keySegments = explode('.', $keyName);
230
            $keyName = $keySegments[count($keySegments) - 1];
231
            $localRaw = $isBelong ? $foo->$rkMethodName() : $foo->$rkMethodAlternate();
232
            $localSegments = explode('.', $localRaw);
233
            $localName = $localSegments[count($localSegments) - 1];
234
            $first = $isBelong ? $localName : $keyName;
235
            $last = $isBelong ? $keyName : $localName;
236
            $this->addRelationsHook($hooks, $first, $property, $last, $mult, $targ);
237
        }
238
    }
239
240
    /**
241
     * @param $rels
242
     * @param $hooks
243
     * @throws InvalidOperationException
244
     */
245
    protected function getRelationshipsKnownPolyMorph($rels, &$hooks)
246
    {
247
        /**
248
         * @var string $property
249
         * @var Relation $foo
250
         */
251
        foreach ($rels['KnownPolyMorphSide'] as $property => $foo) {
252
            $isMany = $foo instanceof MorphToMany;
253
            $targ = get_class($foo->getRelated());
254
            $mult = $isMany ? '*' : ($foo instanceof MorphMany ? '*' : '1');
255
            $mult = $foo instanceof MorphOne ? '0..1' : $mult;
256
257
            list($fkMethodName, $rkMethodName) = $this->polyglotKeyMethodNames($foo, $isMany);
258
            list($fkMethodAlternate, $rkMethodAlternate) = $this->polyglotKeyMethodBackupNames($foo, !$isMany);
259
260
            $keyRaw = $isMany ? $foo->$fkMethodName() : $foo->$fkMethodAlternate();
261
            $keySegments = explode('.', $keyRaw);
262
            $keyName = $keySegments[count($keySegments) - 1];
263
            $localRaw = $isMany ? $foo->$rkMethodName() : $foo->$rkMethodAlternate();
264
            $localSegments = explode('.', $localRaw);
265
            $localName = $localSegments[count($localSegments) - 1];
266
            $first = $isMany ? $keyName : $localName;
267
            $last = $isMany ? $localName : $keyName;
268
            $this->addRelationsHook($hooks, $first, $property, $last, $mult, $targ, 'unknown');
269
        }
270
    }
271
272
    /**
273
     * @param $rels
274
     * @param $hooks
275
     * @throws InvalidOperationException
276
     */
277
    protected function getRelationshipsUnknownPolyMorph($rels, &$hooks)
278
    {
279
        /**
280
         * @var string $property
281
         * @var Relation $foo
282
         */
283
        foreach ($rels['UnknownPolyMorphSide'] as $property => $foo) {
284
            $isMany = $foo instanceof MorphToMany;
285
            $targ = get_class($foo->getRelated());
286
            $mult = $isMany ? '*' : '1';
287
288
            list($fkMethodName, $rkMethodName) = $this->polyglotKeyMethodNames($foo, $isMany);
289
            list($fkMethodAlternate, $rkMethodAlternate) = $this->polyglotKeyMethodBackupNames($foo, !$isMany);
290
291
            $keyRaw = $isMany ? $foo->$fkMethodName() : $foo->$fkMethodAlternate();
292
            $keySegments = explode('.', $keyRaw);
293
            $keyName = $keySegments[count($keySegments) - 1];
294
            $localRaw = $isMany ? $foo->$rkMethodName() : $foo->$rkMethodAlternate();
295
            $localSegments = explode('.', $localRaw);
296
            $localName = $localSegments[count($localSegments) - 1];
297
298
            $first = $keyName;
299
            $last = (isset($localName) && '' != $localName) ? $localName : $foo->getRelated()->getKeyName();
300
            $this->addRelationsHook($hooks, $first, $property, $last, $mult, $targ, 'known');
301
        }
302
    }
303
304
    /**
305
     * @param             $hooks
306
     * @param             $first
307
     * @param             $property
308
     * @param             $last
309
     * @param             $mult
310
     * @param string|null $targ
311
     * @param mixed|null  $type
312
     * @param mixed|null  $through
313
     */
314
    protected function addRelationsHook(&$hooks, $first, $property, $last, $mult, $targ, $type = null, $through = null)
315
    {
316
        if (!isset($hooks[$first])) {
317
            $hooks[$first] = [];
318
        }
319
        if (!isset($hooks[$first][$targ])) {
320
            $hooks[$first][$targ] = [];
321
        }
322
        $hooks[$first][$targ][$property] = [
323
            'property' => $property,
324
            'local' => $last,
325
            'through' => $through,
326
            'multiplicity' => $mult,
327
            'type' => $type
328
        ];
329
    }
330
}
331