Test Failed
Pull Request — master (#108)
by Alex
03:15
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 $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