Test Failed
Pull Request — master (#108)
by Alex
03:40
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
                $rels[$i]['principalRSet'] = $principalPoly ? $placeholder : $principalType;
268
                $rels[$i]['dependentRSet'] = $dependentPoly ? $placeholder : $dependentType;
269
            }
270
            self::$relationCache = $rels;
271
        }
272
        return self::$relationCache;
273
    }
274
275
    private function processRelationLine($line, $entityTypes, &$meta)
276
    {
277
        $principalType = $line['principalType'];
278
        $principalMult = $line['principalMult'];
279
        $principalProp = $line['principalProp'];
280
        $principalRSet = $line['principalRSet'];
281
        $dependentType = $line['dependentType'];
282
        $dependentMult = $line['dependentMult'];
283
        $dependentProp = $line['dependentProp'];
284
        $dependentRSet = $line['dependentRSet'];
285
286
        if (!isset($entityTypes[$principalType]) || !isset($entityTypes[$dependentType])) {
287
            return;
288
        }
289
        $principal = $entityTypes[$principalType];
290
        $dependent = $entityTypes[$dependentType];
291
        $isPoly = static::POLYMORPHIC == $principalRSet || static::POLYMORPHIC == $dependentRSet;
292
293
        if ($isPoly) {
294
            $this->attachReferencePolymorphic(
295
                $meta,
296
                $principalMult,
297
                $dependentMult,
298
                $principal,
299
                $dependent,
300
                $principalProp,
301
                $dependentProp,
302
                $principalRSet,
303
                $dependentRSet
304
            );
305
            return null;
306
        }
307
        $this->attachReferenceNonPolymorphic(
308
            $meta,
309
            $principalMult,
310
            $dependentMult,
311
            $principal,
312
            $dependent,
313
            $principalProp,
314
            $dependentProp
315
        );
316
        return null;
317
    }
318
319
    /**
320
     * @param $meta
321
     * @param $principalMult
322
     * @param $dependentMult
323
     * @param $principal
324
     * @param $dependent
325
     * @param $principalProp
326
     * @param $dependentProp
327
     */
328
    private function attachReferenceNonPolymorphic(
329
        &$meta,
330
        $principalMult,
331
        $dependentMult,
332
        $principal,
333
        $dependent,
334
        $principalProp,
335
        $dependentProp
336
    ) {
337
        //many-to-many
338
        if ('*' == $principalMult && '*' == $dependentMult) {
339
            $meta->addResourceSetReferencePropertyBidirectional(
340
                $principal,
341
                $dependent,
342
                $principalProp,
343
                $dependentProp
344
            );
345
            return;
346
        }
347
        //one-to-one
348
        if ('0..1' == $principalMult || '0..1' == $dependentMult) {
349
            assert($principalMult != $dependentMult, 'Cannot have both ends with 0..1 multiplicity');
350
            $meta->addResourceReferenceSinglePropertyBidirectional(
351
                $principal,
352
                $dependent,
353
                $principalProp,
354
                $dependentProp
355
            );
356
            return;
357
        }
358
        assert($principalMult != $dependentMult, 'Cannot have both ends same multiplicity for 1:N relation');
359
        //principal-one-to-dependent-many
360
        if ('*' == $principalMult) {
361
            $meta->addResourceReferencePropertyBidirectional(
362
                $principal,
363
                $dependent,
364
                $principalProp,
365
                $dependentProp
366
            );
367
            return;
368
        }
369
        //dependent-one-to-principal-many
370
        $meta->addResourceReferencePropertyBidirectional(
371
            $dependent,
372
            $principal,
373
            $dependentProp,
374
            $principalProp
375
        );
376
        return;
377
    }
378
379
    /**
380
     * @param $meta
381
     * @param $principalMult
382
     * @param $dependentMult
383
     * @param $principal
384
     * @param $dependent
385
     * @param $principalProp
386
     * @param $dependentProp
387
     */
388
    private function attachReferencePolymorphic(
389
        &$meta,
390
        $principalMult,
391
        $dependentMult,
392
        $principal,
393
        $dependent,
394
        $principalProp,
395
        $dependentProp,
396
        $principalRSet,
397
        $dependentRSet
398
    ) {
399
        $prinPoly = static::POLYMORPHIC == $principalRSet;
400
        $depPoly = static::POLYMORPHIC == $dependentRSet;
401
        $principalSet = (!$prinPoly) ? $principal->getCustomState()
402
            : $meta->resolveResourceSet(static::POLYMORPHIC_PLURAL);
403
        $dependentSet = (!$depPoly) ? $dependent->getCustomState()
404
            : $meta->resolveResourceSet(static::POLYMORPHIC_PLURAL);
405
        assert($principalSet instanceof ResourceSet, $principalRSet);
406
        assert($dependentSet instanceof ResourceSet, $dependentRSet);
407
408
        $isPrincipalAdded = null !== $principal->resolveProperty($principalProp);
409
        $isDependentAdded = null !== $dependent->resolveProperty($dependentProp);
410
        $prinMany = '*' == $principalMult;
411
        $depMany = '*' == $dependentMult;
412
413
        if (!$isPrincipalAdded) {
414
            if ('*' == $principalMult || $depMany) {
415
                $meta->addResourceSetReferenceProperty($principal, $principalProp, $dependentSet);
416
            } else {
417
                $meta->addResourceReferenceProperty($principal, $principalProp, $dependentSet, $prinPoly, $depMany);
418
            }
419
        }
420
        if (!$isDependentAdded) {
421
            if ('*' == $dependentMult || $prinMany) {
422
                $meta->addResourceSetReferenceProperty($dependent, $dependentProp, $principalSet);
423
            } else {
424
                $meta->addResourceReferenceProperty($dependent, $dependentProp, $principalSet, $depPoly, $prinMany);
425
            }
426
        }
427
        return;
428
    }
429
430
    public function reset()
431
    {
432
        self::$relationCache = null;
433
        self::$isBooted = false;
434
    }
435
436
    /**
437
     * Resolve possible reverse relation property names
438
     *
439
     * @param Model $source
440
     * @param Model $target
441
     * @param $propName
442
     * @return string|null
443
     */
444
    public function resolveReverseProperty(Model $source, Model $target, $propName)
445
    {
446
        assert(is_string($propName), 'Property name must be string');
447
        $relations = $this->getRepairedRoundTripRelations();
448
449
        $sourceName = get_class($source);
450
        $targName = get_class($target);
451
452
        $filter = function ($segment) use ($sourceName, $targName, $propName) {
453
            $match = $sourceName == $segment['principalType'];
454
            $match &= $targName == $segment['dependentType'];
455
            $match &= $propName == $segment['principalProp'];
456
457
            return $match;
458
        };
459
460
        // array_filter does not reset keys - we have to do it ourselves
461
        $trim = array_values(array_filter($relations, $filter));
462
        $result = 0 === count($trim) ? null : $trim[0]['dependentProp'];
463
464
        return $result;
465
    }
466
}
467