Completed
Push — master ( 3901c9...1218b0 )
by Alex
11:19 queued 06:22
created

MetadataProvider   C

Complexity

Total Complexity 63

Size/Duplication

Total Lines 478
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 5

Test Coverage

Coverage 5.8%

Importance

Changes 9
Bugs 0 Features 0
Metric Value
wmc 63
c 9
b 0
f 0
lcom 1
cbo 5
dl 0
loc 478
ccs 4
cts 69
cp 0.058
rs 5.8893

14 Methods

Rating   Name   Duplication   Size   Complexity  
A register() 0 6 1
B getCandidateModels() 0 14 5
B getEntityTypesAndResourceSets() 0 28 3
A __construct() 0 6 1
B boot() 0 48 6
A getRelationHolder() 0 4 1
A calculateRoundTripRelations() 0 13 3
C getPolymorphicRelationGroups() 0 60 12
B getRepairedRoundTripRelations() 0 30 6
B processRelationLine() 0 45 5
B attachReferenceNonPolymorphic() 0 50 6
C attachReferencePolymorphic() 0 70 11
A reset() 0 5 1
A resolveReverseProperty() 0 22 2

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\MetadataRelationHolder;
6
use Illuminate\Database\Eloquent\Model;
7
use Illuminate\Support\Facades\App;
8
use Illuminate\Support\ServiceProvider;
9
use Illuminate\Support\Facades\Cache;
10
use Illuminate\Support\Str;
11
use POData\Providers\Metadata\IMetadataProvider;
12
use POData\Providers\Metadata\ResourceEntityType;
13
use POData\Providers\Metadata\ResourceSet;
14
use POData\Providers\Metadata\ResourceType;
15
use POData\Providers\Metadata\SimpleMetadataProvider;
16
use Illuminate\Support\Facades\Route;
17
use Illuminate\Support\Facades\Schema as Schema;
18
use POData\Providers\Metadata\Type\TypeCode;
19
use POData\Providers\ProvidersWrapper;
20 1
21
class MetadataProvider extends MetadataBaseProvider
22 1
{
23
    protected $multConstraints = [ '0..1' => ['1'], '1' => ['0..1', '*'], '*' => ['1', '*']];
24
    protected static $metaNAMESPACE = 'Data';
25 1
    protected static $relationCache;
26
    protected static $isBooted = false;
27
    const POLYMORPHIC = 'polyMorphicPlaceholder';
28 1
    const POLYMORPHIC_PLURAL = 'polyMorphicPlaceholders';
29 1
30
    protected $relationHolder;
31
32
    public function __construct($app)
33
    {
34
        parent::__construct($app);
35
        $this->relationHolder = new MetadataRelationHolder();
36
        self::$isBooted = false;
37
    }
38
39
    /**
40
     * Bootstrap the application services.  Post-boot.
41
     *
42
     * @return void
43
     */
44
    public function boot()
45
    {
46
        self::$metaNAMESPACE = env('ODataMetaNamespace', 'Data');
47
        // If we aren't migrated, there's no DB tables to pull metadata _from_, so bail out early
48
        try {
49
            if (!Schema::hasTable(config('database.migrations'))) {
50
                return;
51
            }
52
        } catch (\Exception $e) {
53
            return;
54
        }
55
56
        assert(false === self::$isBooted, 'Provider booted twice');
57
        $isCaching = true === $this->getIsCaching();
58
        $meta = Cache::get('metadata');
59
        $hasCache = null != $meta;
60
61
        if ($isCaching && $hasCache) {
62
            App::instance('metadata', $meta);
63
            return;
64
        }
65
        $meta = App::make('metadata');
66
        $this->reset();
67
68
        $stdRef = new \ReflectionClass(Model::class);
69
        $abstract = $meta->addEntityType($stdRef, static::POLYMORPHIC, true, null);
70
        $meta->addKeyProperty($abstract, 'PrimaryKey', TypeCode::STRING);
71
72
        $meta->addResourceSet(static::POLYMORPHIC, $abstract);
73
74
        $modelNames = $this->getCandidateModels();
75
76
        list($entityTypes) = $this->getEntityTypesAndResourceSets($meta, $modelNames);
77
        $entityTypes[static::POLYMORPHIC] = $abstract;
78
79
        // need to lift EntityTypes, adjust for polymorphic-affected relations, etc
80
        $biDirect = $this->getRepairedRoundTripRelations();
81
82
        // now that endpoints are hooked up, tackle the relationships
83
        // if we'd tried earlier, we'd be guaranteed to try to hook a relation up to null, which would be bad
84
        foreach ($biDirect as $line) {
85
            $this->processRelationLine($line, $entityTypes, $meta);
86
        }
87
88
        $key = 'metadata';
89
        $this->handlePostBoot($isCaching, $hasCache, $key, $meta);
90
        self::$isBooted = true;
91
    }
92
93
    /**
94
     * Register the application services.  Boot-time only.
95
     *
96
     * @return void
97
     */
98
    public function register()
99
    {
100
        $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...
101
            return new SimpleMetadataProvider('Data', self::$metaNAMESPACE);
102
        });
103
    }
104
105
    /**
106
     * @return array
107
     */
108
    protected function getCandidateModels()
109
    {
110
        $classes = $this->getClassMap();
111
        $ends = [];
112
        $startName = defined('PODATA_LARAVEL_APP_ROOT_NAMESPACE') ? PODATA_LARAVEL_APP_ROOT_NAMESPACE : 'App';
113
        foreach ($classes as $name) {
114
            if (\Illuminate\Support\Str::startsWith($name, $startName)) {
115
                if (in_array('AlgoWeb\\PODataLaravel\\Models\\MetadataTrait', class_uses($name))) {
116
                    $ends[] = $name;
117
                }
118
            }
119
        }
120
        return $ends;
121
    }
122
123
    /**
124
     * @param $meta
125
     * @param $ends
126
     * @return array[]
127
     */
128
    protected function getEntityTypesAndResourceSets($meta, $ends)
129
    {
130
        assert($meta instanceof IMetadataProvider, get_class($meta));
131
        $entityTypes = [];
132
        $resourceSets = [];
133
        $begins = [];
134
        $numEnds = count($ends);
135
136
        for ($i = 0; $i < $numEnds; $i++) {
137
            $bitter = $ends[$i];
138
            $fqModelName = $bitter;
139
140
            $instance = App::make($fqModelName);
141
            $name = strtolower($instance->getEndpointName());
142
            $metaSchema = $instance->getXmlSchema();
143
144
            // if for whatever reason we don't get an XML schema, move on to next entry and drop current one from
145
            // further processing
146
            if (null == $metaSchema) {
147
                continue;
148
            }
149
            $entityTypes[$fqModelName] = $metaSchema;
150
            $resourceSets[$fqModelName] = $meta->addResourceSet($name, $metaSchema);
151
            $begins[] = $bitter;
152
        }
153
154
        return [$entityTypes, $resourceSets, $begins];
155
    }
156
157
    /**
158
     * @return MetadataRelationHolder
159
     */
160
    public function getRelationHolder()
161
    {
162
        return $this->relationHolder;
163
    }
164
165
    public function calculateRoundTripRelations()
166
    {
167
        $modelNames = $this->getCandidateModels();
168
169
        foreach ($modelNames as $name) {
170
            if (!$this->getRelationHolder()->hasClass($name)) {
171
                $model = new $name();
172
                $this->getRelationHolder()->addModel($model);
173
            }
174
        }
175
176
        return $this->getRelationHolder()->getRelations();
177
    }
178
179
    public function getPolymorphicRelationGroups()
180
    {
181
        $modelNames = $this->getCandidateModels();
182
183
        $knownSide = [];
184
        $unknownSide = [];
185
186
        $hooks = [];
187
        // fish out list of polymorphic-affected models for further processing
188
        foreach ($modelNames as $name) {
189
            $model = new $name();
190
            $isPoly = false;
191
            if ($model->isKnownPolymorphSide()) {
192
                $knownSide[$name] = [];
193
                $isPoly = true;
194
            }
195
            if ($model->isUnknownPolymorphSide()) {
196
                $unknownSide[$name] = [];
197
                $isPoly = true;
198
            }
199
            if (false === $isPoly) {
200
                continue;
201
            }
202
203
            $rels = $model->getRelationships();
204
            // it doesn't matter if a model has no relationships here, that lack will simply be skipped over
205
            // during hookup processing
206
            $hooks[$name] = $rels;
207
        }
208
        // ensure we've only loaded up polymorphic-affected models
209
        $knownKeys = array_keys($knownSide);
210
        $unknownKeys = array_keys($unknownSide);
211
        $dualKeys = array_intersect($knownKeys, $unknownKeys);
212
        assert(count($hooks) == (count($unknownKeys) + count($knownKeys) - count($dualKeys)));
213
        // if either list is empty, bail out - there's nothing to do
214
        if (0 === count($knownSide) || 0 === count($unknownSide)) {
215
            return [];
216
        }
217
218
        // commence primary ignition
219
220
        foreach ($unknownKeys as $key) {
221
            assert(isset($hooks[$key]));
222
            $hook = $hooks[$key];
223
            foreach ($hook as $barb) {
224
                foreach ($barb as $knownType => $propData) {
225
                    $propName = array_keys($propData)[0];
226
                    if (in_array($knownType, $knownKeys)) {
227
                        if (!isset($knownSide[$knownType][$key])) {
228
                            $knownSide[$knownType][$key] = [];
229
                        }
230
                        assert(isset($knownSide[$knownType][$key]));
231
                        $knownSide[$knownType][$key][] = $propData[$propName]['property'];
232
                    }
233
                }
234
            }
235
        }
236
237
        return $knownSide;
238
    }
239
240
    /**
241
     * Get round-trip relations after inserting polymorphic-powered placeholders
242
     *
243
     * @return array
244
     */
245
    public function getRepairedRoundTripRelations()
246
    {
247
        if (!isset(self::$relationCache)) {
248
            $rels = $this->calculateRoundTripRelations();
249
            $groups = $this->getPolymorphicRelationGroups();
250
251
            if (0 === count($groups)) {
252
                self::$relationCache = $rels;
253
                return $rels;
254
            }
255
256
            $placeholder = static::POLYMORPHIC;
257
258
            $groupKeys = array_keys($groups);
259
260
            // we have at least one polymorphic relation, need to dig it out
261
            $numRels = count($rels);
262
            for ($i = 0; $i < $numRels; $i++) {
263
                $relation = $rels[$i];
264
                $principalType = $relation['principalType'];
265
                $dependentType = $relation['dependentType'];
266
                $principalPoly = in_array($principalType, $groupKeys);
267
                $dependentPoly = in_array($dependentType, $groupKeys);
268
                $rels[$i]['principalRSet'] = $principalPoly ? $placeholder : $principalType;
269
                $rels[$i]['dependentRSet'] = $dependentPoly ? $placeholder : $dependentType;
270
            }
271
            self::$relationCache = $rels;
272
        }
273
        return self::$relationCache;
274
    }
275
276
    private function processRelationLine($line, $entityTypes, &$meta)
277
    {
278
        $principalType = $line['principalType'];
279
        $principalMult = $line['principalMult'];
280
        $principalProp = $line['principalProp'];
281
        $principalRSet = $line['principalRSet'];
282
        $dependentType = $line['dependentType'];
283
        $dependentMult = $line['dependentMult'];
284
        $dependentProp = $line['dependentProp'];
285
        $dependentRSet = $line['dependentRSet'];
286
287
        if (!isset($entityTypes[$principalType]) || !isset($entityTypes[$dependentType])) {
288
            return;
289
        }
290
        $principal = $entityTypes[$principalType];
291
        $dependent = $entityTypes[$dependentType];
292
        $isPoly = static::POLYMORPHIC == $principalRSet || static::POLYMORPHIC == $dependentRSet;
293
294
        if ($isPoly) {
295
            $this->attachReferencePolymorphic(
296
                $meta,
297
                $principalMult,
298
                $dependentMult,
299
                $principal,
300
                $dependent,
301
                $principalProp,
302
                $dependentProp,
303
                $principalRSet,
304
                $dependentRSet,
305
                $principalType,
306
                $dependentType
307
            );
308
            return null;
309
        }
310
        $this->attachReferenceNonPolymorphic(
311
            $meta,
312
            $principalMult,
313
            $dependentMult,
314
            $principal,
315
            $dependent,
316
            $principalProp,
317
            $dependentProp
318
        );
319
        return null;
320
    }
321
322
    /**
323
     * @param $meta
324
     * @param $principalMult
325
     * @param $dependentMult
326
     * @param $principal
327
     * @param $dependent
328
     * @param $principalProp
329
     * @param $dependentProp
330
     */
331
    private function attachReferenceNonPolymorphic(
332
        &$meta,
333
        $principalMult,
334
        $dependentMult,
335
        $principal,
336
        $dependent,
337
        $principalProp,
338
        $dependentProp
339
    ) {
340
        //many-to-many
341
        if ('*' == $principalMult && '*' == $dependentMult) {
342
            $meta->addResourceSetReferencePropertyBidirectional(
343
                $principal,
344
                $dependent,
345
                $principalProp,
346
                $dependentProp
347
            );
348
            return;
349
        }
350
        //one-to-one
351
        if ('0..1' == $principalMult || '0..1' == $dependentMult) {
352
            assert($principalMult != $dependentMult, 'Cannot have both ends with 0..1 multiplicity');
353
            $meta->addResourceReferenceSinglePropertyBidirectional(
354
                $principal,
355
                $dependent,
356
                $principalProp,
357
                $dependentProp
358
            );
359
            return;
360
        }
361
        assert($principalMult != $dependentMult, 'Cannot have both ends same multiplicity for 1:N relation');
362
        //principal-one-to-dependent-many
363
        if ('*' == $principalMult) {
364
            $meta->addResourceReferencePropertyBidirectional(
365
                $principal,
366
                $dependent,
367
                $principalProp,
368
                $dependentProp
369
            );
370
            return;
371
        }
372
        //dependent-one-to-principal-many
373
        $meta->addResourceReferencePropertyBidirectional(
374
            $dependent,
375
            $principal,
376
            $dependentProp,
377
            $principalProp
378
        );
379
        return;
380
    }
381
382
    /**
383
     * @param $meta
384
     * @param $principalMult
385
     * @param $dependentMult
386
     * @param $principal
387
     * @param $dependent
388
     * @param $principalProp
389
     * @param $dependentProp
390
     */
391
    private function attachReferencePolymorphic(
392
        &$meta,
393
        $principalMult,
394
        $dependentMult,
395
        $principal,
396
        $dependent,
397
        $principalProp,
398
        $dependentProp,
399
        $principalRSet,
400
        $dependentRSet,
401
        $principalType,
402
        $dependentType
403
    ) {
404
        $prinPoly = static::POLYMORPHIC == $principalRSet;
405
        $depPoly = static::POLYMORPHIC == $dependentRSet;
406
        $principalSet = (!$prinPoly) ? $principal->getCustomState()
407
            : $meta->resolveResourceSet(static::POLYMORPHIC_PLURAL);
408
        $dependentSet = (!$depPoly) ? $dependent->getCustomState()
409
            : $meta->resolveResourceSet(static::POLYMORPHIC_PLURAL);
410
        assert($principalSet instanceof ResourceSet, $principalRSet);
411
        assert($dependentSet instanceof ResourceSet, $dependentRSet);
412
413
        $isPrincipalAdded = null !== $principal->resolveProperty($principalProp);
414
        $isDependentAdded = null !== $dependent->resolveProperty($dependentProp);
415
        $prinMany = '*' == $principalMult;
416
        $depMany = '*' == $dependentMult;
417
418
        $prinConcrete = null;
419
        $depConcrete = null;
420
        if ($prinPoly) {
421
            $prinBitz = explode('\\', $principalType);
422
            $prinConcrete = $meta->resolveResourceType($prinBitz[count($prinBitz)-1]);
423
            assert(static::POLYMORPHIC !== $prinConcrete->getName());
424
        }
425
        if ($depPoly) {
426
            $depBitz = explode('\\', $dependentType);
427
            $depConcrete = $meta->resolveResourceType($depBitz[count($depBitz)-1]);
428
            assert(static::POLYMORPHIC !== $depConcrete->getName());
429
        }
430
431
        if (!$isPrincipalAdded) {
432
            if ('*' == $principalMult || $depMany) {
433
                $meta->addResourceSetReferenceProperty($principal, $principalProp, $dependentSet, $depConcrete);
434
            } else {
435
                $meta->addResourceReferenceProperty(
436
                    $principal,
437
                    $principalProp,
438
                    $dependentSet,
439
                    $prinPoly,
440
                    $depMany,
441
                    $depConcrete
442
                );
443
            }
444
        }
445
        if (!$isDependentAdded) {
446
            if ('*' == $dependentMult || $prinMany) {
447
                $meta->addResourceSetReferenceProperty($dependent, $dependentProp, $principalSet, $prinConcrete);
448
            } else {
449
                $meta->addResourceReferenceProperty(
450
                    $dependent,
451
                    $dependentProp,
452
                    $principalSet,
453
                    $depPoly,
454
                    $prinMany,
455
                    $prinConcrete
456
                );
457
            }
458
        }
459
        return;
460
    }
461
462
    public function reset()
463
    {
464
        self::$relationCache = null;
465
        self::$isBooted = false;
466
    }
467
468
    /**
469
     * Resolve possible reverse relation property names
470
     *
471
     * @param Model $source
472
     * @param Model $target
473
     * @param $propName
474
     * @return string|null
475
     */
476
    public function resolveReverseProperty(Model $source, Model $target, $propName)
477
    {
478
        assert(is_string($propName), 'Property name must be string');
479
        $relations = $this->getRepairedRoundTripRelations();
480
481
        $sourceName = get_class($source);
482
        $targName = get_class($target);
483
484
        $filter = function ($segment) use ($sourceName, $targName, $propName) {
485
            $match = $sourceName == $segment['principalType'];
486
            $match &= $targName == $segment['dependentType'];
487
            $match &= $propName == $segment['principalProp'];
488
489
            return $match;
490
        };
491
492
        // array_filter does not reset keys - we have to do it ourselves
493
        $trim = array_values(array_filter($relations, $filter));
494
        $result = 0 === count($trim) ? null : $trim[0]['dependentProp'];
495
496
        return $result;
497
    }
498
}
499