Test Failed
Pull Request — master (#107)
by Alex
03:44
created

calculateRoundTripRelationsFirstPass()   C

Complexity

Conditions 12
Paths 40

Size

Total Lines 58
Code Lines 40

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 58
rs 6.5331
cc 12
eloc 40
nc 40
nop 3

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
3
namespace AlgoWeb\PODataLaravel\Providers;
4
5
use Illuminate\Database\Eloquent\Model;
6
use Illuminate\Support\Facades\App;
7
use Illuminate\Support\ServiceProvider;
8
use Illuminate\Support\Facades\Cache;
9
use Illuminate\Support\Str;
10
use POData\Providers\Metadata\IMetadataProvider;
11
use POData\Providers\Metadata\ResourceEntityType;
12
use POData\Providers\Metadata\ResourceSet;
13
use POData\Providers\Metadata\ResourceType;
14
use POData\Providers\Metadata\SimpleMetadataProvider;
15
use Illuminate\Support\Facades\Route;
16
use Illuminate\Support\Facades\Schema as Schema;
17
use POData\Providers\Metadata\Type\TypeCode;
18
19
class MetadataProviderOld extends MetadataBaseProvider
20
{
21
    protected $multConstraints = [ '0..1' => ['1'], '1' => ['0..1', '*'], '*' => ['1', '*']];
22
    protected static $metaNAMESPACE = 'Data';
23
    protected static $relationCache;
24
    protected static $isBooted = false;
25
    const POLYMORPHIC = 'polyMorphicPlaceholder';
26
    const POLYMORPHIC_PLURAL = 'polyMorphicPlaceholders';
27
28
    /**
29
     * Bootstrap the application services.  Post-boot.
30
     *
31
     * @return void
32
     */
33
    public function boot()
34
    {
35
        self::$metaNAMESPACE = env('ODataMetaNamespace', 'Data');
36
        // If we aren't migrated, there's no DB tables to pull metadata _from_, so bail out early
37
        try {
38
            if (!Schema::hasTable(config('database.migrations'))) {
39
                return;
40
            }
41
        } catch (\Exception $e) {
42
            return;
43
        }
44
45
        assert(false === self::$isBooted, 'Provider booted twice');
46
        $isCaching = true === $this->getIsCaching();
47
        $meta = Cache::get('metadata');
48
        $hasCache = null != $meta;
49
50
        if ($isCaching && $hasCache) {
51
            App::instance('metadata', $meta);
52
            return;
53
        }
54
        $meta = App::make('metadata');
55
        $this->reset();
56
57
        $stdRef = new \ReflectionClass(Model::class);
58
        $abstract = $meta->addEntityType($stdRef, static::POLYMORPHIC, true, null);
59
        $meta->addKeyProperty($abstract, 'PrimaryKey', TypeCode::STRING);
60
61
        $meta->addResourceSet(static::POLYMORPHIC, $abstract);
62
63
        $modelNames = $this->getCandidateModels();
64
65
        list($entityTypes) = $this->getEntityTypesAndResourceSets($meta, $modelNames);
66
        $entityTypes[static::POLYMORPHIC] = $abstract;
67
68
        // need to lift EntityTypes, adjust for polymorphic-affected relations, etc
69
        $biDirect = $this->getRepairedRoundTripRelations();
70
71
        // now that endpoints are hooked up, tackle the relationships
72
        // if we'd tried earlier, we'd be guaranteed to try to hook a relation up to null, which would be bad
73
        foreach ($biDirect as $line) {
74
            $this->processRelationLine($line, $entityTypes, $meta);
75
        }
76
77
        $key = 'metadata';
78
        $this->handlePostBoot($isCaching, $hasCache, $key, $meta);
79
        self::$isBooted = true;
80
    }
81
82
    /**
83
     * Register the application services.  Boot-time only.
84
     *
85
     * @return void
86
     */
87
    public function register()
88
    {
89
        $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...
90
            return new SimpleMetadataProvider('Data', self::$metaNAMESPACE);
91
        });
92
    }
93
94
    /**
95
     * @return array
96
     */
97
    protected function getCandidateModels()
98
    {
99
        $classes = $this->getClassMap();
100
        $ends = [];
101
        $startName = defined('PODATA_LARAVEL_APP_ROOT_NAMESPACE') ? PODATA_LARAVEL_APP_ROOT_NAMESPACE : 'App';
102
        foreach ($classes as $name) {
103
            if (\Illuminate\Support\Str::startsWith($name, $startName)) {
104
                if (in_array('AlgoWeb\\PODataLaravel\\Models\\MetadataTrait', class_uses($name))) {
105
                    $ends[] = $name;
106
                }
107
            }
108
        }
109
        return $ends;
110
    }
111
112
    /**
113
     * @param $meta
114
     * @param $ends
115
     * @return array[]
116
     */
117
    protected function getEntityTypesAndResourceSets($meta, $ends)
118
    {
119
        assert($meta instanceof IMetadataProvider, get_class($meta));
120
        $entityTypes = [];
121
        $resourceSets = [];
122
        $begins = [];
123
        $numEnds = count($ends);
124
125
        for ($i = 0; $i < $numEnds; $i++) {
126
            $bitter = $ends[$i];
127
            $fqModelName = $bitter;
128
129
            $instance = App::make($fqModelName);
130
            $name = strtolower($instance->getEndpointName());
131
            $metaSchema = $instance->getXmlSchema();
132
133
            // if for whatever reason we don't get an XML schema, move on to next entry and drop current one from
134
            // further processing
135
            if (null == $metaSchema) {
136
                continue;
137
            }
138
            $entityTypes[$fqModelName] = $metaSchema;
139
            $resourceSets[$fqModelName] = $meta->addResourceSet($name, $metaSchema);
140
            $begins[] = $bitter;
141
        }
142
143
        return [$entityTypes, $resourceSets, $begins];
144
    }
145
146
    public function calculateRoundTripRelations()
147
    {
148
        $modelNames = $this->getCandidateModels();
149
150
        $hooks = [];
151
        foreach ($modelNames as $name) {
152
            $model = new $name();
153
            $rels = $model->getRelationships();
154
            // it doesn't matter if a model has no relationships here, that lack will simply be skipped over
155
            // during hookup processing
156
            $hooks[$name] = $rels;
157
        }
158
159
        // model relation gubbins are assembled, now the hard bit starts
160
        // storing assembled bidirectional relationship schema
161
        $rawLines = [];
162
        // storing unprocessed relation gubbins for second-pass processing
163
        $remix = [];
164
        $this->calculateRoundTripRelationsFirstPass($hooks, $rawLines, $remix);
165
166
        // now for second processing pass, to pick up stuff that first didn't handle
167
        $rawLines = $this->calculateRoundTripRelationsSecondPass($remix, $rawLines);
168
169
        $numLines = count($rawLines);
170
        for ($i = 0; $i < $numLines; $i++) {
171
            $rawLines[$i]['principalRSet'] = $rawLines[$i]['principalType'];
172
            $rawLines[$i]['dependentRSet'] = $rawLines[$i]['dependentType'];
173
        }
174
175
        // deduplicate rawLines - can't use array_unique as array value elements are themselves arrays
176
        $lines = [];
177
        foreach ($rawLines as $line) {
178
            if (!in_array($line, $lines)) {
179
                $lines[] = $line;
180
            }
181
        }
182
183
        return $lines;
184
    }
185
186
    public function getPolymorphicRelationGroups()
187
    {
188
        $modelNames = $this->getCandidateModels();
189
190
        $knownSide = [];
191
        $unknownSide = [];
192
193
        $hooks = [];
194
        // fish out list of polymorphic-affected models for further processing
195
        foreach ($modelNames as $name) {
196
            $model = new $name();
197
            $isPoly = false;
198
            if ($model->isKnownPolymorphSide()) {
199
                $knownSide[$name] = [];
200
                $isPoly = true;
201
            }
202
            if ($model->isUnknownPolymorphSide()) {
203
                $unknownSide[$name] = [];
204
                $isPoly = true;
205
            }
206
            if (false === $isPoly) {
207
                continue;
208
            }
209
210
            $rels = $model->getRelationships();
211
            // it doesn't matter if a model has no relationships here, that lack will simply be skipped over
212
            // during hookup processing
213
            $hooks[$name] = $rels;
214
        }
215
        // ensure we've only loaded up polymorphic-affected models
216
        $knownKeys = array_keys($knownSide);
217
        $unknownKeys = array_keys($unknownSide);
218
        $dualKeys = array_intersect($knownKeys, $unknownKeys);
219
        assert(count($hooks) == (count($unknownKeys) + count($knownKeys) - count($dualKeys)));
220
        // if either list is empty, bail out - there's nothing to do
221
        if (0 === count($knownSide) || 0 === count($unknownSide)) {
222
            return [];
223
        }
224
225
        // commence primary ignition
226
227
        foreach ($unknownKeys as $key) {
228
            assert(isset($hooks[$key]));
229
            $hook = $hooks[$key];
230
            foreach ($hook as $barb) {
231
                foreach ($barb as $knownType => $propData) {
232
                    $propName = array_keys($propData)[0];
233
                    if (in_array($knownType, $knownKeys)) {
234
                        if (!isset($knownSide[$knownType][$key])) {
235
                            $knownSide[$knownType][$key] = [];
236
                        }
237
                        assert(isset($knownSide[$knownType][$key]));
238
                        $knownSide[$knownType][$key][] = $propData[$propName]['property'];
239
                    }
240
                }
241
            }
242
        }
243
244
        return $knownSide;
245
    }
246
247
    /**
248
     * Get round-trip relations after inserting polymorphic-powered placeholders
249
     *
250
     * @return array
251
     */
252
    public function getRepairedRoundTripRelations()
253
    {
254
        if (!isset(self::$relationCache)) {
255
            $rels = $this->calculateRoundTripRelations();
256
            $groups = $this->getPolymorphicRelationGroups();
257
258
            if (0 === count($groups)) {
259
                self::$relationCache = $rels;
260
                return $rels;
261
            }
262
263
            $placeholder = static::POLYMORPHIC;
264
265
            $groupKeys = array_keys($groups);
266
267
            // we have at least one polymorphic relation, need to dig it out
268
            $numRels = count($rels);
269
            for ($i = 0; $i < $numRels; $i++) {
270
                $relation = $rels[$i];
271
                $principalType = $relation['principalType'];
272
                $dependentType = $relation['dependentType'];
273
                $principalPoly = in_array($principalType, $groupKeys);
274
                $dependentPoly = in_array($dependentType, $groupKeys);
275
                // if relation is not polymorphic, then move on
276
                if (!($principalPoly || $dependentPoly)) {
277
                    continue;
278
                } else {
279
                    // if only one end is a known end of a polymorphic relation
280
                    // for moment we're punting on both
281
                    $oneEnd = $principalPoly !== $dependentPoly;
282
                    assert($oneEnd, 'Multi-generational polymorphic relation chains not implemented');
283
                    $targRels = $principalPoly ? $groups[$principalType] : $groups[$dependentType];
284
                    $targUnknown = $targRels[$principalPoly ? $dependentType : $principalType];
285
                    $targProperty = $principalPoly ? $relation['dependentProp'] : $relation['principalProp'];
286
                    $msg = 'Specified unknown-side property ' . $targProperty
287
                           . ' not found in polymorphic relation map';
288
                    assert(in_array($targProperty, $targUnknown), $msg);
289
290
                    $targType = $principalPoly ? 'dependentRSet' : 'principalRSet';
291
                    $rels[$i][$targType] = $placeholder;
292
                    continue;
293
                }
294
            }
295
            self::$relationCache = $rels;
296
        }
297
        return self::$relationCache;
298
    }
299
300
    /**
301
     * @param $remix
302
     * @param $lines
303
     * @return array
304
     */
305
    private function calculateRoundTripRelationsSecondPass($remix, $lines)
306
    {
307
        foreach ($remix as $principalType => $value) {
308
            foreach ($value as $fk => $localRels) {
309
                foreach ($localRels as $dependentType => $deets) {
310
                    $propName = array_keys($deets)[0];
311
                    $principalMult = $deets[$propName]['multiplicity'];
312
                    $principalProperty = $deets[$propName]['property'];
313
                    $principalKey = $deets[$propName]['local'];
314
315
                    if (!isset($remix[$dependentType])) {
316
                        continue;
317
                    }
318
                    $foreign = $remix[$dependentType];
319
                    if (!isset($foreign[$principalKey])) {
320
                        continue;
321
                    }
322
                    $foreign = $foreign[$principalKey];
323
                    $propName = array_keys($foreign[$dependentType])[0];
324
                    $dependentMult = $foreign[$dependentType][$propName]['multiplicity'];
325
                    $dependentProperty = $foreign[$dependentType][$propName]['property'];
326
                    assert(
327
                        in_array($dependentMult, $this->multConstraints[$principalMult]),
328
                        'Cannot pair multiplicities ' . $dependentMult . ' and ' . $principalMult
329
                    );
330
                    assert(
331
                        in_array($principalMult, $this->multConstraints[$dependentMult]),
332
                        'Cannot pair multiplicities ' . $principalMult . ' and ' . $dependentMult
333
                    );
334
                    // generate forward and reverse relations
335
                    list($forward, $reverse) = $this->calculateRoundTripRelationsGenForwardReverse(
336
                        $principalType,
337
                        $principalMult,
338
                        $principalProperty,
339
                        $dependentType,
340
                        $dependentMult,
341
                        $dependentProperty
342
                    );
343
                    // add forward relation
344
                    $lines[] = $forward;
345
                    // add reverse relation
346
                    $lines[] = $reverse;
347
                }
348
            }
349
        }
350
        return $lines;
351
    }
352
353
    /**
354
     * @param $hooks
355
     * @param $lines
356
     * @param $remix
357
     */
358
    private function calculateRoundTripRelationsFirstPass($hooks, &$lines, &$remix)
359
    {
360
        foreach ($hooks as $principalType => $value) {
361
            foreach ($value as $fk => $localRels) {
362
                foreach ($localRels as $dependentType => $deets) {
363
                    if (!isset($hooks[$dependentType])) {
364
                        continue;
365
                    }
366
                    $propName = array_keys($deets)[0];
367
                    $principalMult = $deets[$propName]['multiplicity'];
368
                    $principalProperty = $deets[$propName]['property'];
369
                    $principalKey = $deets[$propName]['local'];
370
371
                    $foreign = $hooks[$dependentType];
372
                    $foreign = null != $foreign && isset($foreign[$principalKey]) ? $foreign[$principalKey] : null;
373
374
                    if (null != $foreign && isset($foreign[$principalType])) {
375
                        $foreign = $foreign[$principalType];
376
                        $propName = array_keys($foreign)[0];
377
                        $dependentMult = $foreign[$propName]['multiplicity'];
378
                        $dependentProperty = $foreign[$propName]['property'];
379
                        assert(
380
                            in_array($dependentMult, $this->multConstraints[$principalMult]),
381
                            'Cannot pair multiplicities ' . $dependentMult . ' and ' . $principalMult
382
                        );
383
                        assert(
384
                            in_array($principalMult, $this->multConstraints[$dependentMult]),
385
                            'Cannot pair multiplicities ' . $principalMult . ' and ' . $dependentMult
386
                        );
387
                        // generate forward and reverse relations
388
                        list($forward, $reverse) = $this->calculateRoundTripRelationsGenForwardReverse(
389
                            $principalType,
390
                            $principalMult,
391
                            $principalProperty,
392
                            $dependentType,
393
                            $dependentMult,
394
                            $dependentProperty
395
                        );
396
                        // add forward relation
397
                        $lines[] = $forward;
398
                        // add reverse relation
399
                        $lines[] = $reverse;
400
                    } else {
401
                        if (!isset($remix[$principalType])) {
402
                            $remix[$principalType] = [];
403
                        }
404
                        if (!isset($remix[$principalType][$fk])) {
405
                            $remix[$principalType][$fk] = [];
406
                        }
407
                        if (!isset($remix[$principalType][$fk][$dependentType])) {
408
                            $remix[$principalType][$fk][$dependentType] = $deets;
409
                        }
410
                        assert(isset($remix[$principalType][$fk][$dependentType]));
411
                    }
412
                }
413
            }
414
        }
415
    }
416
417
    /**
418
     * @param $principalType
419
     * @param $principalMult
420
     * @param $principalProperty
421
     * @param $dependentType
422
     * @param $dependentMult
423
     * @param $dependentProperty
424
     * @return array[]
425
     */
426
    private function calculateRoundTripRelationsGenForwardReverse(
427
        $principalType,
428
        $principalMult,
429
        $principalProperty,
430
        $dependentType,
431
        $dependentMult,
432
        $dependentProperty
433
    ) {
434
        $forward = [
435
            'principalType' => $principalType,
436
            'principalMult' => $dependentMult,
437
            'principalProp' => $principalProperty,
438
            'dependentType' => $dependentType,
439
            'dependentMult' => $principalMult,
440
            'dependentProp' => $dependentProperty
441
        ];
442
        $reverse = [
443
            'principalType' => $dependentType,
444
            'principalMult' => $principalMult,
445
            'principalProp' => $dependentProperty,
446
            'dependentType' => $principalType,
447
            'dependentMult' => $dependentMult,
448
            'dependentProp' => $principalProperty
449
        ];
450
        return [$forward, $reverse];
451
    }
452
453
    private function processRelationLine($line, $entityTypes, &$meta)
454
    {
455
        $principalType = $line['principalType'];
456
        $principalMult = $line['principalMult'];
457
        $principalProp = $line['principalProp'];
458
        $principalRSet = $line['principalRSet'];
459
        $dependentType = $line['dependentType'];
460
        $dependentMult = $line['dependentMult'];
461
        $dependentProp = $line['dependentProp'];
462
        $dependentRSet = $line['dependentRSet'];
463
464
        if (!isset($entityTypes[$principalType]) || !isset($entityTypes[$dependentType])) {
465
            return;
466
        }
467
        $principal = $entityTypes[$principalType];
468
        $dependent = $entityTypes[$dependentType];
469
        $isPoly = static::POLYMORPHIC == $principalRSet || static::POLYMORPHIC == $dependentRSet;
470
471
        if ($isPoly) {
472
            $this->attachReferencePolymorphic(
473
                $meta,
474
                $principalMult,
475
                $dependentMult,
476
                $principal,
477
                $dependent,
478
                $principalProp,
479
                $dependentProp,
480
                $principalRSet,
481
                $dependentRSet
482
            );
483
            return null;
484
        }
485
        $this->attachReferenceNonPolymorphic(
486
            $meta,
487
            $principalMult,
488
            $dependentMult,
489
            $principal,
490
            $dependent,
491
            $principalProp,
492
            $dependentProp
493
        );
494
        return null;
495
    }
496
497
    /**
498
     * @param $meta
499
     * @param $principalMult
500
     * @param $dependentMult
501
     * @param $principal
502
     * @param $dependent
503
     * @param $principalProp
504
     * @param $dependentProp
505
     */
506
    private function attachReferenceNonPolymorphic(
507
        &$meta,
508
        $principalMult,
509
        $dependentMult,
510
        $principal,
511
        $dependent,
512
        $principalProp,
513
        $dependentProp
514
    ) {
515
        //many-to-many
516
        if ('*' == $principalMult && '*' == $dependentMult) {
517
            $meta->addResourceSetReferencePropertyBidirectional(
518
                $principal,
519
                $dependent,
520
                $principalProp,
521
                $dependentProp
522
            );
523
            return;
524
        }
525
        //one-to-one
526
        if ('0..1' == $principalMult || '0..1' == $dependentMult) {
527
            assert($principalMult != $dependentMult, 'Cannot have both ends with 0..1 multiplicity');
528
            $meta->addResourceReferenceSinglePropertyBidirectional(
529
                $principal,
530
                $dependent,
531
                $principalProp,
532
                $dependentProp
533
            );
534
            return;
535
        }
536
        assert($principalMult != $dependentMult, 'Cannot have both ends same multiplicity for 1:N relation');
537
        //principal-one-to-dependent-many
538
        if ('*' == $principalMult) {
539
            $meta->addResourceReferencePropertyBidirectional(
540
                $principal,
541
                $dependent,
542
                $principalProp,
543
                $dependentProp
544
            );
545
            return;
546
        }
547
        //dependent-one-to-principal-many
548
        $meta->addResourceReferencePropertyBidirectional(
549
            $dependent,
550
            $principal,
551
            $dependentProp,
552
            $principalProp
553
        );
554
        return;
555
    }
556
557
    /**
558
     * @param $meta
559
     * @param $principalMult
560
     * @param $dependentMult
561
     * @param $principal
562
     * @param $dependent
563
     * @param $principalProp
564
     * @param $dependentProp
565
     */
566
    private function attachReferencePolymorphic(
567
        &$meta,
568
        $principalMult,
569
        $dependentMult,
570
        $principal,
571
        $dependent,
572
        $principalProp,
573
        $dependentProp,
574
        $principalRSet,
575
        $dependentRSet
576
    ) {
577
        $prinPoly = static::POLYMORPHIC == $principalRSet;
578
        $depPoly = static::POLYMORPHIC == $dependentRSet;
579
        $principalSet = (!$prinPoly) ? $principal->getCustomState()
580
            : $meta->resolveResourceSet(static::POLYMORPHIC_PLURAL);
581
        $dependentSet = (!$depPoly) ? $dependent->getCustomState()
582
            : $meta->resolveResourceSet(static::POLYMORPHIC_PLURAL);
583
        assert($principalSet instanceof ResourceSet, $principalRSet);
584
        assert($dependentSet instanceof ResourceSet, $dependentRSet);
585
586
        $isPrincipalAdded = null !== $principal->resolveProperty($principalProp);
587
        $isDependentAdded = null !== $dependent->resolveProperty($dependentProp);
588
        $prinMany = '*' == $principalMult;
589
        $depMany = '*' == $dependentMult;
590
591
        if (!$isPrincipalAdded) {
592
            if ('*' == $principalMult || $depMany) {
593
                $meta->addResourceSetReferenceProperty($principal, $principalProp, $dependentSet);
594
            } else {
595
                $meta->addResourceReferenceProperty($principal, $principalProp, $dependentSet, $prinPoly, $depMany);
596
            }
597
        }
598
        if (!$isDependentAdded) {
599
            if ('*' == $dependentMult || $prinMany) {
600
                $meta->addResourceSetReferenceProperty($dependent, $dependentProp, $principalSet);
601
            } else {
602
                $meta->addResourceReferenceProperty($dependent, $dependentProp, $principalSet, $depPoly, $prinMany);
603
            }
604
        }
605
        return;
606
    }
607
608
    public function reset()
609
    {
610
        self::$relationCache = null;
611
        self::$isBooted = false;
612
    }
613
614
    /**
615
     * Resolve possible reverse relation property names
616
     *
617
     * @param Model $source
618
     * @param Model $target
619
     * @param $propName
620
     * @return string|null
621
     */
622
    public function resolveReverseProperty(Model $source, Model $target, $propName)
623
    {
624
        assert(is_string($propName), 'Property name must be string');
625
        $relations = $this->getRepairedRoundTripRelations();
626
627
        $sourceName = get_class($source);
628
        $targName = get_class($target);
629
630
        $filter = function ($segment) use ($sourceName, $targName, $propName) {
631
            $match = $sourceName == $segment['principalType'];
632
            $match &= $targName == $segment['dependentType'];
633
            $match &= $propName == $segment['principalProp'];
634
635
            return $match;
636
        };
637
638
        // array_filter does not reset keys - we have to do it ourselves
639
        $trim = array_values(array_filter($relations, $filter));
640
        $result = 0 === count($trim) ? null : $trim[0]['dependentProp'];
641
642
        return $result;
643
    }
644
}
645