Test Failed
Push — master ( 066f6d...3e02e1 )
by Alex
03:01
created

MetadataProvider   D

Complexity

Total Complexity 80

Size/Duplication

Total Lines 574
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 4

Test Coverage

Coverage 5.8%

Importance

Changes 8
Bugs 0 Features 0
Metric Value
wmc 80
c 8
b 0
f 0
lcom 1
cbo 4
dl 0
loc 574
ccs 4
cts 69
cp 0.058
rs 4.8717

13 Methods

Rating   Name   Duplication   Size   Complexity  
B boot() 0 45 6
A register() 0 6 1
B getCandidateModels() 0 14 5
B getEntityTypesAndResourceSets() 0 28 3
B calculateRoundTripRelations() 0 39 5
C getPolymorphicRelationGroups() 0 59 12
D getRepairedRoundTripRelations() 0 42 9
B calculateRoundTripRelationsSecondPass() 0 45 6
C calculateRoundTripRelationsFirstPass() 0 56 12
B calculateRoundTripRelationsGenForwardReverse() 0 26 1
B processRelationLine() 0 43 5
B attachReferenceNonPolymorphic() 0 50 6
D attachReferencePolymorphic() 0 41 9

How to fix   Complexity   

Complex Class

Complex classes like MetadataProvider often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use MetadataProvider, and based on these observations, apply Extract Interface, too.

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