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