Test Failed
Pull Request — master (#106)
by Alex
03:00
created

calculateRoundTripRelationsSecondPass()   B

Complexity

Conditions 6
Paths 6

Size

Total Lines 45
Code Lines 31

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 42

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 45
ccs 0
cts 0
cp 0
rs 8.439
cc 6
eloc 31
nc 6
nop 2
crap 42
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