Test Failed
Pull Request — master (#108)
by Alex
03:15
created

MetadataProvider   C

Complexity

Total Complexity 67

Size/Duplication

Total Lines 465
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 5

Test Coverage

Coverage 7.14%

Importance

Changes 9
Bugs 0 Features 0
Metric Value
wmc 67
c 9
b 0
f 0
lcom 1
cbo 5
dl 0
loc 465
ccs 5
cts 70
cp 0.0714
rs 5.7097

14 Methods

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