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