Test Failed
Pull Request — master (#100)
by Alex
03:53
created

MetadataProvider::attachReferencePolymorphic()   D

Complexity

Conditions 9
Paths 36

Size

Total Lines 41
Code Lines 33

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 90

Importance

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

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