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

MetadataProviderOld::attachReferencePolymorphic()   D

Complexity

Conditions 9
Paths 36

Size

Total Lines 41
Code Lines 33

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 41
rs 4.909
cc 9
eloc 33
nc 36
nop 9

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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