Test Failed
Pull Request — master (#106)
by Alex
08:05
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 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 $rawRelationCache;
26
    const POLYMORPHIC = 'polyMorphicPlaceholder';
27
    const POLYMORPHIC_PLURAL = 'polyMorphicPlaceholders';
28 1
    protected $relationHolder;
29 1
30
    /**
31
     * Create a new service provider instance.
32
     *
33
     * @param  \Illuminate\Contracts\Foundation\Application  $app
34
     * @return void
0 ignored issues
show
Comprehensibility Best Practice introduced by
Adding a @return annotation to constructors is generally not recommended as a constructor does not have a meaningful return value.

Adding a @return annotation to a constructor is not recommended, since a constructor does not have a meaningful return value.

Please refer to the PHP core documentation on constructors.

Loading history...
35
     */
36
    public function __construct($app)
37
    {
38
        parent::__construct($app);
39
        $this->relationHolder = new MetadataRelationHolder();
40
    }
41
42
    /**
43
     * Bootstrap the application services.  Post-boot.
44
     *
45
     * @return void
46
     */
47
    public function boot()
48
    {
49
        self::$metaNAMESPACE = env('ODataMetaNamespace', 'Data');
50
        // If we aren't migrated, there's no DB tables to pull metadata _from_, so bail out early
51
        try {
52
            if (!Schema::hasTable(config('database.migrations'))) {
53
                return;
54
            }
55
        } catch (\Exception $e) {
56
            return;
57
        }
58
59
        $isCaching = true === $this->getIsCaching();
60
        $meta = Cache::get('metadata');
61
        $hasCache = null != $meta;
62
63
        if ($isCaching && $hasCache) {
64
            App::instance('metadata', $meta);
65
            return;
66
        }
67
        $meta = App::make('metadata');
68
        $this->reset();
69
70
        $stdRef = new \ReflectionClass(Model::class);
71
        $abstract = $meta->addEntityType($stdRef, static::POLYMORPHIC, true, null);
72
        $meta->addKeyProperty($abstract, 'PrimaryKey', TypeCode::STRING);
73
74
        $meta->addResourceSet(static::POLYMORPHIC, $abstract);
75
76
        $modelNames = $this->getCandidateModels();
77
78
        list($entityTypes) = $this->getEntityTypesAndResourceSets($meta, $modelNames);
79
        $entityTypes[static::POLYMORPHIC] = $abstract;
80
81
        // need to lift EntityTypes, adjust for polymorphic-affected relations, etc
82
        $biDirect = $this->getRepairedRoundTripRelations();
83
84
        // now that endpoints are hooked up, tackle the relationships
85
        // if we'd tried earlier, we'd be guaranteed to try to hook a relation up to null, which would be bad
86
        foreach ($biDirect as $line) {
87
            $this->processRelationLine($line, $entityTypes, $meta);
88
        }
89
90
        $key = 'metadata';
91
        $this->handlePostBoot($isCaching, $hasCache, $key, $meta);
92
    }
93
94
    /**
95
     * Register the application services.  Boot-time only.
96
     *
97
     * @return void
98
     */
99
    public function register()
100
    {
101
        $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...
102
            return new SimpleMetadataProvider('Data', self::$metaNAMESPACE);
103
        });
104
    }
105
106
    /**
107
     * @return array
108
     */
109
    protected function getCandidateModels()
110
    {
111
        $classes = $this->getClassMap();
112
        $ends = [];
113
        $startName = defined('PODATA_LARAVEL_APP_ROOT_NAMESPACE') ? PODATA_LARAVEL_APP_ROOT_NAMESPACE : 'App';
114
        foreach ($classes as $name) {
115
            if (\Illuminate\Support\Str::startsWith($name, $startName)) {
116
                if (in_array('AlgoWeb\\PODataLaravel\\Models\\MetadataTrait', class_uses($name))) {
117
                    $ends[] = $name;
118
                }
119
            }
120
        }
121
        return $ends;
122
    }
123
124
    /**
125
     * @param $meta
126
     * @param $ends
127
     * @return array[]
128
     */
129
    protected function getEntityTypesAndResourceSets($meta, $ends)
130
    {
131
        assert($meta instanceof IMetadataProvider, get_class($meta));
132
        $entityTypes = [];
133
        $resourceSets = [];
134
        $begins = [];
135
        $numEnds = count($ends);
136
137
        for ($i = 0; $i < $numEnds; $i++) {
138
            $bitter = $ends[$i];
139
            $fqModelName = $bitter;
140
141
            $instance = App::make($fqModelName);
142
            $name = strtolower($instance->getEndpointName());
143
            $metaSchema = $instance->getXmlSchema();
144
145
            // if for whatever reason we don't get an XML schema, move on to next entry and drop current one from
146
            // further processing
147
            if (null == $metaSchema) {
148
                continue;
149
            }
150
            $entityTypes[$fqModelName] = $metaSchema;
151
            $resourceSets[$fqModelName] = $meta->addResourceSet($name, $metaSchema);
152
            $begins[] = $bitter;
153
        }
154
155
        return [$entityTypes, $resourceSets, $begins];
156
    }
157
158
    public function calculateRoundTripRelations()
0 ignored issues
show
Documentation introduced by
The return type could not be reliably inferred; please add a @return annotation.

Our type inference engine in quite powerful, but sometimes the code does not provide enough clues to go by. In these cases we request you to add a @return annotation as described here.

Loading history...
159
    {
160
        if (!isset(static::$rawRelationCache)) {
161
            $modelNames = $this->getCandidateModels();
162
163
            foreach ($modelNames as $name) {
164
                $model = new $name();
165
                $this->getRelationHolder()->addModel($model);
166
            }
167
168
            // model relation gubbins are assembled, now fish out relation results from holder
169
            $lines = [];
170
            foreach ($modelNames as $name) {
171
                $workLines = $this->getRelationHolder()->getRelationsByClass($name);
172
                foreach ($workLines as $work) {
173
                    // if this relation line is already in $rawLines, don't add it again
174
                    $work['principalRSet'] = $work['principalType'];
175
                    $work['dependentRSet'] = $work['dependentType'];
176
                    if (!in_array($work, $lines)) {
177
                        $lines[] = $work;
178
                    }
179
                }
180
            }
181
            static::$rawRelationCache = $lines;
182
        }
183
184
        return static::$rawRelationCache;
185
    }
186
187
    public function getPolymorphicRelationGroups()
188
    {
189
        $modelNames = $this->getCandidateModels();
190
191
        $knownSide = [];
192
        $unknownSide = [];
193
194
        $hooks = [];
195
        // fish out list of polymorphic-affected models for further processing
196
        foreach ($modelNames as $name) {
197
            $model = new $name();
198
            $isPoly = false;
199
            if ($model->isKnownPolymorphSide()) {
200
                $knownSide[$name] = [];
201
                $isPoly = true;
202
            }
203
            if ($model->isUnknownPolymorphSide()) {
204
                $unknownSide[$name] = [];
205
                $isPoly = true;
206
            }
207
            if (false === $isPoly) {
208
                continue;
209
            }
210
211
            $rels = $model->getRelationships();
212
            // it doesn't matter if a model has no relationships here, that lack will simply be skipped over
213
            // during hookup processing
214
            $hooks[$name] = $rels;
215
        }
216
        // ensure we've only loaded up polymorphic-affected models
217
        $knownKeys = array_keys($knownSide);
218
        $unknownKeys = array_keys($unknownSide);
219
        $dualKeys = array_intersect($knownKeys, $unknownKeys);
220
        assert(count($hooks) == (count($unknownKeys) + count($knownKeys) - count($dualKeys)));
221
        // if either list is empty, bail out - there's nothing to do
222
        if (0 === count($knownSide) || 0 === count($unknownSide)) {
223
            return [];
224
        }
225
226
        // commence primary ignition
227
228
        foreach ($unknownKeys as $key) {
229
            assert(isset($hooks[$key]));
230
            $hook = $hooks[$key];
231
            foreach ($hook as $barb) {
232
                foreach ($barb as $knownType => $propData) {
233
                    $propName = array_keys($propData)[0];
234
                    if (in_array($knownType, $knownKeys)) {
235
                        if (!isset($knownSide[$knownType][$key])) {
236
                            $knownSide[$knownType][$key] = [];
237
                        }
238
                        assert(isset($knownSide[$knownType][$key]));
239
                        $knownSide[$knownType][$key][] = $propData[$propName]['property'];
240
                    }
241
                }
242
            }
243
        }
244
245
        return $knownSide;
246
    }
247
248
    /**
249
     * Get round-trip relations after inserting polymorphic-powered placeholders
250
     *
251
     * @return array
252
     */
253
    public function getRepairedRoundTripRelations()
254
    {
255
        if (!isset(self::$relationCache)) {
256
            $rels = $this->calculateRoundTripRelations();
257
            $groups = $this->getPolymorphicRelationGroups();
258
259
            if (0 === count($groups)) {
260
                self::$relationCache = $rels;
261
                return $rels;
262
            }
263
264
            $placeholder = static::POLYMORPHIC;
265
266
            $groupKeys = array_keys($groups);
267
268
            // we have at least one polymorphic relation, need to dig it out
269
            $numRels = count($rels);
270
            for ($i = 0; $i < $numRels; $i++) {
271
                $relation = $rels[$i];
272
                $principalType = $relation['principalType'];
273
                $dependentType = $relation['dependentType'];
274
                $principalPoly = in_array($principalType, $groupKeys);
275
                $dependentPoly = in_array($dependentType, $groupKeys);
276
                // if relation is not polymorphic, then move on
277
                if (!($principalPoly || $dependentPoly)) {
278
                    continue;
279
                } else {
280
                    // if only one end is a known end of a polymorphic relation
281
                    // for moment we're punting on both
282
                    $oneEnd = $principalPoly !== $dependentPoly;
283
                    assert($oneEnd, 'Multi-generational polymorphic relation chains not implemented');
284
                    $targRels = $principalPoly ? $groups[$principalType] : $groups[$dependentType];
285
                    $targUnknown = $targRels[$principalPoly ? $dependentType : $principalType];
286
                    $targProperty = $principalPoly ? $relation['dependentProp'] : $relation['principalProp'];
287
                    $msg = 'Specified unknown-side property ' . $targProperty
288
                           . ' not found in polymorphic relation map';
289
                    assert(in_array($targProperty, $targUnknown), $msg);
290
291
                    $targType = $principalPoly ? 'dependentRSet' : 'principalRSet';
292
                    $rels[$i][$targType] = $placeholder;
293
                    continue;
294
                }
295
            }
296
            self::$relationCache = $rels;
297
        }
298
        return self::$relationCache;
299
    }
300
301
    private function processRelationLine($line, $entityTypes, &$meta)
302
    {
303
        $principalType = $line['principalType'];
304
        $principalMult = $line['principalMult'];
305
        $principalProp = $line['principalProp'];
306
        $principalRSet = $line['principalRSet'];
307
        $dependentType = $line['dependentType'];
308
        $dependentMult = $line['dependentMult'];
309
        $dependentProp = $line['dependentProp'];
310
        $dependentRSet = $line['dependentRSet'];
311
312
        if (!isset($entityTypes[$principalType]) || !isset($entityTypes[$dependentType])) {
313
            return;
314
        }
315
        $principal = $entityTypes[$principalType];
316
        $dependent = $entityTypes[$dependentType];
317
        $isPoly = static::POLYMORPHIC == $principalRSet || static::POLYMORPHIC == $dependentRSet;
318
319
        if ($isPoly) {
320
            $this->attachReferencePolymorphic(
321
                $meta,
322
                $principalMult,
323
                $dependentMult,
324
                $principal,
325
                $dependent,
326
                $principalProp,
327
                $dependentProp,
328
                $principalRSet,
329
                $dependentRSet
330
            );
331
            return null;
332
        }
333
        $this->attachReferenceNonPolymorphic(
334
            $meta,
335
            $principalMult,
336
            $dependentMult,
337
            $principal,
338
            $dependent,
339
            $principalProp,
340
            $dependentProp
341
        );
342
        return null;
343
    }
344
345
    /**
346
     * @param $meta
347
     * @param $principalMult
348
     * @param $dependentMult
349
     * @param $principal
350
     * @param $dependent
351
     * @param $principalProp
352
     * @param $dependentProp
353
     */
354
    private function attachReferenceNonPolymorphic(
355
        &$meta,
356
        $principalMult,
357
        $dependentMult,
358
        $principal,
359
        $dependent,
360
        $principalProp,
361
        $dependentProp
362
    ) {
363
        //many-to-many
364
        if ('*' == $principalMult && '*' == $dependentMult) {
365
            $meta->addResourceSetReferencePropertyBidirectional(
366
                $principal,
367
                $dependent,
368
                $principalProp,
369
                $dependentProp
370
            );
371
            return;
372
        }
373
        //one-to-one
374
        if ('0..1' == $principalMult || '0..1' == $dependentMult) {
375
            assert($principalMult != $dependentMult, 'Cannot have both ends with 0..1 multiplicity');
376
            $meta->addResourceReferenceSinglePropertyBidirectional(
377
                $principal,
378
                $dependent,
379
                $principalProp,
380
                $dependentProp
381
            );
382
            return;
383
        }
384
        assert($principalMult != $dependentMult, 'Cannot have both ends same multiplicity for 1:N relation');
385
        //principal-one-to-dependent-many
386
        if ('*' == $principalMult) {
387
            $meta->addResourceReferencePropertyBidirectional(
388
                $principal,
389
                $dependent,
390
                $principalProp,
391
                $dependentProp
392
            );
393
            return;
394
        }
395
        //dependent-one-to-principal-many
396
        $meta->addResourceReferencePropertyBidirectional(
397
            $dependent,
398
            $principal,
399
            $dependentProp,
400
            $principalProp
401
        );
402
        return;
403
    }
404
405
    /**
406
     * @param $meta
407
     * @param $principalMult
408
     * @param $dependentMult
409
     * @param $principal
410
     * @param $dependent
411
     * @param $principalProp
412
     * @param $dependentProp
413
     */
414
    private function attachReferencePolymorphic(
415
        &$meta,
416
        $principalMult,
417
        $dependentMult,
418
        $principal,
419
        $dependent,
420
        $principalProp,
421
        $dependentProp,
422
        $principalRSet,
423
        $dependentRSet
424
    ) {
425
        $prinPoly = static::POLYMORPHIC == $principalRSet;
426
        $depPoly = static::POLYMORPHIC == $dependentRSet;
427
        $principalSet = (!$prinPoly) ? $principal->getCustomState()
428
            : $meta->resolveResourceSet(static::POLYMORPHIC_PLURAL);
429
        $dependentSet = (!$depPoly) ? $dependent->getCustomState()
430
            : $meta->resolveResourceSet(static::POLYMORPHIC_PLURAL);
431
        assert($principalSet instanceof ResourceSet, $principalRSet);
432
        assert($dependentSet instanceof ResourceSet, $dependentRSet);
433
434
        $isPrincipalAdded = null !== $principal->resolveProperty($principalProp);
435
        $isDependentAdded = null !== $dependent->resolveProperty($dependentProp);
436
        $prinMany = '*' == $principalMult;
437
        $depMany = '*' == $dependentMult;
438
439
        if (!$isPrincipalAdded) {
440
            if ('*' == $principalMult || $depMany) {
441
                $meta->addResourceSetReferenceProperty($principal, $principalProp, $dependentSet);
442
            } else {
443
                $meta->addResourceReferenceProperty($principal, $principalProp, $dependentSet, $prinPoly, $depMany);
444
            }
445
        }
446
        if (!$isDependentAdded) {
447
            if ('*' == $dependentMult || $prinMany) {
448
                $meta->addResourceSetReferenceProperty($dependent, $dependentProp, $principalSet);
449
            } else {
450
                $meta->addResourceReferenceProperty($dependent, $dependentProp, $principalSet, $depPoly, $prinMany);
451
            }
452
        }
453
        return;
454
    }
455
456
    public function reset()
457
    {
458
        self::$relationCache = null;
459
        self::$rawRelationCache = null;
460
        $this->getRelationHolder()->reset();
461
    }
462
463
    /**
464
     * Resolve possible reverse relation property names
465
     *
466
     * @param Model $source
467
     * @param Model $target
468
     * @param $propName
469
     * @return string|null
470
     */
471
    public function resolveReverseProperty(Model $source, Model $target, $propName)
472
    {
473
        assert(is_string($propName), 'Property name must be string');
474
        $relations = $this->getRepairedRoundTripRelations();
475
476
        $sourceName = get_class($source);
477
        $targName = get_class($target);
478
479
        $filter = function ($segment) use ($sourceName, $targName, $propName) {
480
            $match = $sourceName == $segment['principalType'];
481
            $match &= $targName == $segment['dependentType'];
482
            $match &= $propName == $segment['principalProp'];
483
484
            return $match;
485
        };
486
487
        // array_filter does not reset keys - we have to do it ourselves
488
        $trim = array_values(array_filter($relations, $filter));
489
        $result = 0 === count($trim) ? null : $trim[0]['dependentProp'];
490
491
        return $result;
492
    }
493
494
    /**
495
     * @return MetadataRelationHolder
496
     */
497
    public function getRelationHolder()
498
    {
499
        assert(null !== $this->relationHolder, 'Relation holder must not be null');
500
        return $this->relationHolder;
501
    }
502
}
503