Completed
Pull Request — master (#85)
by Alex
26:21 queued 17:04
created

MetadataProvider::getRepairedRoundTripRelations()   D

Complexity

Conditions 10
Paths 35

Size

Total Lines 44
Code Lines 29

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 110

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 44
ccs 0
cts 0
cp 0
rs 4.8196
cc 10
eloc 29
nc 35
nop 0
crap 110

How to fix   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
3
namespace AlgoWeb\PODataLaravel\Providers;
4
5
use AlgoWeb\PODataLaravel\Models\TestMorphOneParent;
6
use Illuminate\Support\Facades\App;
7
use Illuminate\Support\ServiceProvider;
8
use Illuminate\Support\Facades\Cache;
9
use POData\Providers\Metadata\IMetadataProvider;
10
use POData\Providers\Metadata\ResourceEntityType;
11
use POData\Providers\Metadata\ResourceType;
12
use POData\Providers\Metadata\SimpleMetadataProvider;
13
use Illuminate\Support\Facades\Route;
14
use Illuminate\Support\Facades\Schema as Schema;
15
16
class MetadataProvider extends MetadataBaseProvider
17
{
18
    protected $multConstraints = [ '0..1' => ['1'], '1' => ['0..1', '*'], '*' => ['1', '*']];
19
    protected static $metaNAMESPACE = 'Data';
20 1
21
    /**
22 1
     * Bootstrap the application services.  Post-boot.
23
     *
24
     * @return void
25 1
     */
26
    public function boot()
27
    {
28 1
        self::$metaNAMESPACE = env('ODataMetaNamespace', 'Data');
29 1
        // If we aren't migrated, there's no DB tables to pull metadata _from_, so bail out early
30
        try {
31
            if (!Schema::hasTable(config('database.migrations'))) {
32
                return;
33
            }
34
        } catch (\Exception $e) {
35
            return;
36
        }
37
38
        $isCaching = true === $this->getIsCaching();
39
        $meta = Cache::get('metadata');
40
        $hasCache = null != $meta;
41
42
        if ($isCaching && $hasCache) {
43
            App::instance('metadata', $meta);
44
            return;
45
        }
46
        $meta = App::make('metadata');
47
48
        $stdRef = new \ReflectionClass(new \stdClass());
49
        $abstract = $meta->addEntityType($stdRef, 'polyMorphicPlaceholder', true, null);
50
51
        $modelNames = $this->getCandidateModels();
52
53
        list($entityTypes) = $this->getEntityTypesAndResourceSets($meta, $modelNames);
54
        $entityTypes['polyMorphicPlaceholder'] = $abstract;
55
56
        // need to lift EntityTypes
57
        $biDirect = $this->calculateRoundTripRelations();
58
59
        $groups = $this->getPolymorphicRelationGroups();
0 ignored issues
show
Unused Code introduced by
$groups is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
60
61
        // now that endpoints are hooked up, tackle the relationships
62
        // if we'd tried earlier, we'd be guaranteed to try to hook a relation up to null, which would be bad
63
        foreach ($biDirect as $line) {
64
            $this->processRelationLine($line, $entityTypes, $meta);
65
        }
66
67
        $key = 'metadata';
68
        $this->handlePostBoot($isCaching, $hasCache, $key, $meta);
69
    }
70
71
    /**
72
     * Register the application services.  Boot-time only.
73
     *
74
     * @return void
75
     */
76
    public function register()
77
    {
78
        $this->app->singleton('metadata', function ($app) {
0 ignored issues
show
Unused Code introduced by
The parameter $app is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
79
            return new SimpleMetadataProvider('Data', self::$metaNAMESPACE);
80
        });
81
    }
82
83
    /**
84
     * @return array
85
     */
86
    protected function getCandidateModels()
87
    {
88
        $classes = $this->getClassMap();
89
        $ends = [];
90
        $startName = defined('PODATA_LARAVEL_APP_ROOT_NAMESPACE') ? PODATA_LARAVEL_APP_ROOT_NAMESPACE : 'App';
91
        foreach ($classes as $name) {
92
            if (\Illuminate\Support\Str::startsWith($name, $startName)) {
93
                if (in_array('AlgoWeb\\PODataLaravel\\Models\\MetadataTrait', class_uses($name))) {
94
                    $ends[] = $name;
95
                }
96
            }
97
        }
98
        return $ends;
99
    }
100
101
    /**
102
     * @param $meta
103
     * @param $ends
104
     * @return array[]
105
     */
106
    protected function getEntityTypesAndResourceSets($meta, $ends)
107
    {
108
        assert($meta instanceof IMetadataProvider, get_class($meta));
109
        $entityTypes = [];
110
        $resourceSets = [];
111
        $begins = [];
112
        $numEnds = count($ends);
113
114
        for ($i = 0; $i < $numEnds; $i++) {
115
            $bitter = $ends[$i];
116
            $fqModelName = $bitter;
117
118
            $instance = App::make($fqModelName);
119
            $name = strtolower($instance->getEndpointName());
120
            $metaSchema = $instance->getXmlSchema();
121
122
            // if for whatever reason we don't get an XML schema, move on to next entry and drop current one from
123
            // further processing
124
            if (null == $metaSchema) {
125
                continue;
126
            }
127
            $entityTypes[$fqModelName] = $metaSchema;
128
            $resourceSets[$fqModelName] = $meta->addResourceSet($name, $metaSchema);
129
            $begins[] = $bitter;
130
        }
131
132
        return [$entityTypes, $resourceSets, $begins];
133
    }
134
135
    public function calculateRoundTripRelations()
136
    {
137
        $modelNames = $this->getCandidateModels();
138
139
        $hooks = [];
140
        foreach ($modelNames as $name) {
141
            $model = new $name();
142
            $rels = $model->getRelationships();
143
            // it doesn't matter if a model has no relationships here, that lack will simply be skipped over
144
            // during hookup processing
145
            $hooks[$name] = $rels;
146
        }
147
148
        // model relation gubbins are assembled, now the hard bit starts
149
        // storing assembled bidirectional relationship schema
150
        $rawLines = [];
151
        // storing unprocessed relation gubbins for second-pass processing
152
        $remix = [];
153
        $this->calculateRoundTripRelationsFirstPass($hooks, $rawLines, $remix);
154
155
        // now for second processing pass, to pick up stuff that first didn't handle
156
        $rawLines = $this->calculateRoundTripRelationsSecondPass($remix, $rawLines);
157
158
        // deduplicate rawLines - can't use array_unique as array value elements are themselves arrays
159
        $lines = [];
160
        foreach ($rawLines as $line) {
161
            if (!in_array($line, $lines)) {
162
                $lines[] = $line;
163
            }
164
        }
165
166
        return $lines;
167
    }
168
169
    public function getPolymorphicRelationGroups()
170
    {
171
        $modelNames = $this->getCandidateModels();
172
173
        $knownSide = [];
174
        $unknownSide = [];
175
176
        $hooks = [];
177
        // fish out list of polymorphic-affected models for further processing
178
        foreach ($modelNames as $name) {
179
            $model = new $name();
180
            $isPoly = false;
181
            if ($model->isKnownPolymorphSide()) {
182
                $knownSide[$name] = [];
183
                $isPoly = true;
184
            }
185
            if ($model->isUnknownPolymorphSide()) {
186
                $unknownSide[$name] = [];
187
                $isPoly = true;
188
            }
189
            if (false === $isPoly) {
190
                continue;
191
            }
192
193
            $rels = $model->getRelationships();
194
            // it doesn't matter if a model has no relationships here, that lack will simply be skipped over
195
            // during hookup processing
196
            $hooks[$name] = $rels;
197
        }
198
        // ensure we've only loaded up polymorphic-affected models
199
        $knownKeys = array_keys($knownSide);
200
        $unknownKeys = array_keys($unknownSide);
201
        $dualKeys = array_intersect($knownKeys, $unknownKeys);
202
        assert(count($hooks) == (count($unknownKeys) + count($knownKeys) - count($dualKeys)));
203
        // if either list is empty, bail out - there's nothing to do
204
        if (0 === count($knownSide) || 0 === count($unknownSide)) {
205
            return [];
206
        }
207
208
        // commence primary ignition
209
210
        foreach ($unknownKeys as $key) {
211
            assert(isset($hooks[$key]));
212
            $hook = $hooks[$key];
213
            foreach ($hook as $barb) {
214
                foreach ($barb as $knownType => $propData) {
215
                    if (in_array($knownType, $knownKeys)) {
216
                        if (!isset($knownSide[$knownType][$key])) {
217
                            $knownSide[$knownType][$key] = [];
218
                        }
219
                        assert(isset($knownSide[$knownType][$key]));
220
                        $knownSide[$knownType][$key][] = $propData['property'];
221
                    }
222
                }
223
            }
224
        }
225
226
        return $knownSide;
227
    }
228
229
    /**
230
     * Get round-trip relations after inserting polymorphic-powered placeholders
231
     *
232
     * @return array
233
     */
234
    public function getRepairedRoundTripRelations()
235
    {
236
        $rels = $this->calculateRoundTripRelations();
237
        $groups = $this->getPolymorphicRelationGroups();
238
239
        if (0 === count($groups)) {
240
            return $rels;
241
        }
242
243
        $placeholder = 'polyMorphicPlaceholder';
244
245
        $groupKeys = array_keys($groups);
246
247
        // we have at least one polymorphic relation, need to dig it out
248
        $numRels = count($rels);
249
        for ($i = 0; $i < $numRels; $i++) {
250
            $relation = $rels[$i];
251
            $principalType = $relation['principalType'];
252
            $dependentType = $relation['dependentType'];
253
            $principalPoly = in_array($principalType, $groupKeys);
254
            $dependentPoly = in_array($dependentType, $groupKeys);
255
            // if relation is not polymorphic, then move on
256
            if (!($principalPoly || $dependentPoly)) {
257
                continue;
258
            } else {
259
                // if only one end is a known end of a polymorphic relation
260
                // for moment we're punting on both
261
                $oneEnd = $principalPoly !== $dependentPoly;
262
                assert($oneEnd, 'Multi-generational polymorphic relation chains not implemented');
263
                $targRels = $principalPoly ? $groups[$principalType] : $groups[$dependentType];
264
                $targUnknown = $targRels[$principalPoly ? $dependentType : $principalType];
265
                $targProperty = $principalPoly ? $relation['dependentProp'] : $relation['principalProp'];
266
                $msg = 'Specified unknown-side property ' . $targProperty . ' not found in polymorphic relation map';
267
                assert(in_array($targProperty, $targUnknown), $msg);
268
269
                $targType = $principalPoly ? 'dependentType' : 'principalType';
270
                $otherType = $principalPoly ? 'principalType' : 'dependentType';
0 ignored issues
show
Unused Code introduced by
$otherType is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
271
                $rels[$i][$targType] = $placeholder;
272
                continue;
273
            }
274
        }
275
276
        return $rels;
277
    }
278
279
    /**
280
     * @param $remix
281
     * @param $lines
282
     * @return array
283
     */
284
    private function calculateRoundTripRelationsSecondPass($remix, $lines)
285
    {
286
        foreach ($remix as $principalType => $value) {
287
            foreach ($value as $fk => $localRels) {
288
                foreach ($localRels as $dependentType => $deets) {
289
                    $principalMult = $deets['multiplicity'];
290
                    $principalProperty = $deets['property'];
291
                    $principalKey = $deets['local'];
292
293
                    if (!isset($remix[$dependentType])) {
294
                        continue;
295
                    }
296
                    $foreign = $remix[$dependentType];
297
                    if (!isset($foreign[$principalKey])) {
298
                        continue;
299
                    }
300
                    $foreign = $foreign[$principalKey];
301
                    $dependentMult = $foreign[$dependentType]['multiplicity'];
302
                    $dependentProperty = $foreign[$dependentType]['property'];
303
                    assert(
304
                        in_array($dependentMult, $this->multConstraints[$principalMult]),
305
                        'Cannot pair multiplicities ' . $dependentMult . ' and ' . $principalMult
306
                    );
307
                    assert(
308
                        in_array($principalMult, $this->multConstraints[$dependentMult]),
309
                        'Cannot pair multiplicities ' . $principalMult . ' and ' . $dependentMult
310
                    );
311
                    // generate forward and reverse relations
312
                    list($forward, $reverse) = $this->calculateRoundTripRelationsGenForwardReverse(
313
                        $principalType,
314
                        $principalMult,
315
                        $principalProperty,
316
                        $dependentType,
317
                        $dependentMult,
318
                        $dependentProperty
319
                    );
320
                    // add forward relation
321
                    $lines[] = $forward;
322
                    // add reverse relation
323
                    $lines[] = $reverse;
324
                }
325
            }
326
        }
327
        return $lines;
328
    }
329
330
    /**
331
     * @param $hooks
332
     * @param $lines
333
     * @param $remix
334
     */
335
    private function calculateRoundTripRelationsFirstPass($hooks, &$lines, &$remix)
336
    {
337
        foreach ($hooks as $principalType => $value) {
338
            foreach ($value as $fk => $localRels) {
339
                foreach ($localRels as $dependentType => $deets) {
340
                    if (!isset($hooks[$dependentType])) {
341
                        continue;
342
                    }
343
                    $principalMult = $deets['multiplicity'];
344
                    $principalProperty = $deets['property'];
345
                    $principalKey = $deets['local'];
346
347
                    $foreign = $hooks[$dependentType];
348
                    $foreign = null != $foreign && isset($foreign[$principalKey]) ? $foreign[$principalKey] : null;
349
350
                    if (null != $foreign && isset($foreign[$principalType])) {
351
                        $foreign = $foreign[$principalType];
352
                        $dependentMult = $foreign['multiplicity'];
353
                        $dependentProperty = $foreign['property'];
354
                        assert(
355
                            in_array($dependentMult, $this->multConstraints[$principalMult]),
356
                            'Cannot pair multiplicities ' . $dependentMult . ' and ' . $principalMult
357
                        );
358
                        assert(
359
                            in_array($principalMult, $this->multConstraints[$dependentMult]),
360
                            'Cannot pair multiplicities ' . $principalMult . ' and ' . $dependentMult
361
                        );
362
                        // generate forward and reverse relations
363
                        list($forward, $reverse) = $this->calculateRoundTripRelationsGenForwardReverse(
364
                            $principalType,
365
                            $principalMult,
366
                            $principalProperty,
367
                            $dependentType,
368
                            $dependentMult,
369
                            $dependentProperty
370
                        );
371
                        // add forward relation
372
                        $lines[] = $forward;
373
                        // add reverse relation
374
                        $lines[] = $reverse;
375
                    } else {
376
                        if (!isset($remix[$principalType])) {
377
                            $remix[$principalType] = [];
378
                        }
379
                        if (!isset($remix[$principalType][$fk])) {
380
                            $remix[$principalType][$fk] = [];
381
                        }
382
                        if (!isset($remix[$principalType][$fk][$dependentType])) {
383
                            $remix[$principalType][$fk][$dependentType] = $deets;
384
                        }
385
                        assert(isset($remix[$principalType][$fk][$dependentType]));
386
                    }
387
                }
388
            }
389
        }
390
    }
391
392
    /**
393
     * @param $principalType
394
     * @param $principalMult
395
     * @param $principalProperty
396
     * @param $dependentType
397
     * @param $dependentMult
398
     * @param $dependentProperty
399
     * @return array[]
400
     */
401
    private function calculateRoundTripRelationsGenForwardReverse(
402
        $principalType,
403
        $principalMult,
404
        $principalProperty,
405
        $dependentType,
406
        $dependentMult,
407
        $dependentProperty
408
    ) {
409
        $forward = [
410
            'principalType' => $principalType,
411
            'principalMult' => $dependentMult,
412
            'principalProp' => $principalProperty,
413
            'dependentType' => $dependentType,
414
            'dependentMult' => $principalMult,
415
            'dependentProp' => $dependentProperty
416
        ];
417
        $reverse = [
418
            'principalType' => $dependentType,
419
            'principalMult' => $principalMult,
420
            'principalProp' => $dependentProperty,
421
            'dependentType' => $principalType,
422
            'dependentMult' => $dependentMult,
423
            'dependentProp' => $principalProperty
424
        ];
425
        return [$forward, $reverse];
426
    }
427
428
    private function processRelationLine($line, $entityTypes, &$meta)
429
    {
430
        $principalType = $line['principalType'];
431
        $principalMult = $line['principalMult'];
432
        $principalProp = $line['principalProp'];
433
        $dependentType = $line['dependentType'];
434
        $dependentMult = $line['dependentMult'];
435
        $dependentProp = $line['dependentProp'];
436
        if (!isset($entityTypes[$principalType]) || !isset($entityTypes[$dependentType])) {
437
            return;
438
        }
439
        $principal = $entityTypes[$principalType];
440
        $dependent = $entityTypes[$dependentType];
441
        //many-to-many
442
        if ('*' == $principalMult && '*' == $dependentMult) {
443
            $meta->addResourceSetReferencePropertyBidirectional(
444
                $principal,
445
                $dependent,
446
                $principalProp,
447
                $dependentProp
448
            );
449
            return;
450
        }
451
        //one-to-one
452
        if ('0..1' == $principalMult || '0..1' == $dependentMult) {
453
            assert($principalMult != $dependentMult, 'Cannot have both ends with 0..1 multiplicity');
454
            $meta->addResourceReferenceSinglePropertyBidirectional(
455
                $principal,
456
                $dependent,
457
                $principalProp,
458
                $dependentProp
459
            );
460
            return;
461
        }
462
        assert($principalMult != $dependentMult, 'Cannot have both ends same multiplicity for 1:N relation');
463
        //principal-one-to-dependent-many
464
        if ('*' == $principalMult) {
465
            $meta->addResourceReferencePropertyBidirectional(
466
                $principal,
467
                $dependent,
468
                $principalProp,
469
                $dependentProp
470
            );
471
            return;
472
        }
473
        //dependent-one-to-principal-many
474
        $meta->addResourceReferencePropertyBidirectional(
475
            $dependent,
476
            $principal,
477
            $dependentProp,
478
            $principalProp
479
        );
480
        return;
481
    }
482
}
483