Test Failed
Pull Request — master (#106)
by Alex
08:05
created

MetadataProvider::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 5
rs 9.4285
c 0
b 0
f 0
ccs 0
cts 4
cp 0
cc 1
eloc 3
nc 1
nop 1
crap 2
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