Test Failed
Pull Request — master (#100)
by Alex
04:07 queued 33s
created

MetadataProvider::reset()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 4
ccs 0
cts 0
cp 0
rs 10
cc 1
eloc 2
nc 1
nop 0
crap 2
1
<?php
2
3
namespace AlgoWeb\PODataLaravel\Providers;
4
5
use Illuminate\Database\Eloquent\Model;
6
use Illuminate\Support\Facades\App;
7
use Illuminate\Support\ServiceProvider;
8
use Illuminate\Support\Facades\Cache;
9
use Illuminate\Support\Str;
10
use POData\Providers\Metadata\IMetadataProvider;
11
use POData\Providers\Metadata\ResourceEntityType;
12
use POData\Providers\Metadata\ResourceSet;
13
use POData\Providers\Metadata\ResourceType;
14
use POData\Providers\Metadata\SimpleMetadataProvider;
15
use Illuminate\Support\Facades\Route;
16
use Illuminate\Support\Facades\Schema as Schema;
17
use POData\Providers\Metadata\Type\TypeCode;
18
19
class MetadataProvider extends MetadataBaseProvider
20 1
{
21
    protected $multConstraints = [ '0..1' => ['1'], '1' => ['0..1', '*'], '*' => ['1', '*']];
22 1
    protected static $metaNAMESPACE = 'Data';
23
    protected static $relationCache;
24
    const POLYMORPHIC = 'polyMorphicPlaceholder';
25 1
    const POLYMORPHIC_PLURAL = 'polyMorphicPlaceholders';
26
27
    /**
28 1
     * Bootstrap the application services.  Post-boot.
29 1
     *
30
     * @return void
31
     */
32
    public function boot()
33
    {
34
        self::$metaNAMESPACE = env('ODataMetaNamespace', 'Data');
35
        // If we aren't migrated, there's no DB tables to pull metadata _from_, so bail out early
36
        try {
37
            if (!Schema::hasTable(config('database.migrations'))) {
38
                return;
39
            }
40
        } catch (\Exception $e) {
41
            return;
42
        }
43
44
        $isCaching = true === $this->getIsCaching();
45
        $meta = Cache::get('metadata');
46
        $hasCache = null != $meta;
47
48
        if ($isCaching && $hasCache) {
49
            App::instance('metadata', $meta);
50
            return;
51
        }
52
        $meta = App::make('metadata');
53
        $this->reset();
54
55
        $stdRef = new \ReflectionClass(Model::class);
56
        $abstract = $meta->addEntityType($stdRef, static::POLYMORPHIC, true, null);
57
        $meta->addKeyProperty($abstract, 'PrimaryKey', TypeCode::STRING);
58
59
        $meta->addResourceSet(static::POLYMORPHIC, $abstract);
60
61
        $modelNames = $this->getCandidateModels();
62
63
        list($entityTypes) = $this->getEntityTypesAndResourceSets($meta, $modelNames);
64
        $entityTypes[static::POLYMORPHIC] = $abstract;
65
66
        // need to lift EntityTypes, adjust for polymorphic-affected relations, etc
67
        $biDirect = $this->getRepairedRoundTripRelations();
68
69
        // now that endpoints are hooked up, tackle the relationships
70
        // if we'd tried earlier, we'd be guaranteed to try to hook a relation up to null, which would be bad
71
        foreach ($biDirect as $line) {
72
            $this->processRelationLine($line, $entityTypes, $meta);
73
        }
74
75
        $key = 'metadata';
76
        $this->handlePostBoot($isCaching, $hasCache, $key, $meta);
77
    }
78
79
    /**
80
     * Register the application services.  Boot-time only.
81
     *
82
     * @return void
83
     */
84
    public function register()
85
    {
86
        $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...
87
            return new SimpleMetadataProvider('Data', self::$metaNAMESPACE);
88
        });
89
    }
90
91
    /**
92
     * @return array
93
     */
94
    protected function getCandidateModels()
95
    {
96
        $classes = $this->getClassMap();
97
        $ends = [];
98
        $startName = defined('PODATA_LARAVEL_APP_ROOT_NAMESPACE') ? PODATA_LARAVEL_APP_ROOT_NAMESPACE : 'App';
99
        foreach ($classes as $name) {
100
            if (\Illuminate\Support\Str::startsWith($name, $startName)) {
101
                if (in_array('AlgoWeb\\PODataLaravel\\Models\\MetadataTrait', class_uses($name))) {
102
                    $ends[] = $name;
103
                }
104
            }
105
        }
106
        return $ends;
107
    }
108
109
    /**
110
     * @param $meta
111
     * @param $ends
112
     * @return array[]
113
     */
114
    protected function getEntityTypesAndResourceSets($meta, $ends)
115
    {
116
        assert($meta instanceof IMetadataProvider, get_class($meta));
117
        $entityTypes = [];
118
        $resourceSets = [];
119
        $begins = [];
120
        $numEnds = count($ends);
121
122
        for ($i = 0; $i < $numEnds; $i++) {
123
            $bitter = $ends[$i];
124
            $fqModelName = $bitter;
125
126
            $instance = App::make($fqModelName);
127
            $name = strtolower($instance->getEndpointName());
128
            $metaSchema = $instance->getXmlSchema();
129
130
            // if for whatever reason we don't get an XML schema, move on to next entry and drop current one from
131
            // further processing
132
            if (null == $metaSchema) {
133
                continue;
134
            }
135
            $entityTypes[$fqModelName] = $metaSchema;
136
            $resourceSets[$fqModelName] = $meta->addResourceSet($name, $metaSchema);
137
            $begins[] = $bitter;
138
        }
139
140
        return [$entityTypes, $resourceSets, $begins];
141
    }
142
143
    public function calculateRoundTripRelations()
144
    {
145
        $modelNames = $this->getCandidateModels();
146
147
        $hooks = [];
148
        foreach ($modelNames as $name) {
149
            $model = new $name();
150
            $rels = $model->getRelationships();
151
            // it doesn't matter if a model has no relationships here, that lack will simply be skipped over
152
            // during hookup processing
153
            $hooks[$name] = $rels;
154
        }
155
156
        // model relation gubbins are assembled, now the hard bit starts
157
        // storing assembled bidirectional relationship schema
158
        $rawLines = [];
159
        // storing unprocessed relation gubbins for second-pass processing
160
        $remix = [];
161
        $this->calculateRoundTripRelationsFirstPass($hooks, $rawLines, $remix);
162
163
        // now for second processing pass, to pick up stuff that first didn't handle
164
        $rawLines = $this->calculateRoundTripRelationsSecondPass($remix, $rawLines);
165
166
        $numLines = count($rawLines);
167
        for ($i = 0; $i < $numLines; $i++) {
168
            $rawLines[$i]['principalRSet'] = $rawLines[$i]['principalType'];
169
            $rawLines[$i]['dependentRSet'] = $rawLines[$i]['dependentType'];
170
        }
171
172
        // deduplicate rawLines - can't use array_unique as array value elements are themselves arrays
173
        $lines = [];
174
        foreach ($rawLines as $line) {
175
            if (!in_array($line, $lines)) {
176
                $lines[] = $line;
177
            }
178
        }
179
180
        return $lines;
181
    }
182
183
    public function getPolymorphicRelationGroups()
184
    {
185
        $modelNames = $this->getCandidateModels();
186
187
        $knownSide = [];
188
        $unknownSide = [];
189
190
        $hooks = [];
191
        // fish out list of polymorphic-affected models for further processing
192
        foreach ($modelNames as $name) {
193
            $model = new $name();
194
            $isPoly = false;
195
            if ($model->isKnownPolymorphSide()) {
196
                $knownSide[$name] = [];
197
                $isPoly = true;
198
            }
199
            if ($model->isUnknownPolymorphSide()) {
200
                $unknownSide[$name] = [];
201
                $isPoly = true;
202
            }
203
            if (false === $isPoly) {
204
                continue;
205
            }
206
207
            $rels = $model->getRelationships();
208
            // it doesn't matter if a model has no relationships here, that lack will simply be skipped over
209
            // during hookup processing
210
            $hooks[$name] = $rels;
211
        }
212
        // ensure we've only loaded up polymorphic-affected models
213
        $knownKeys = array_keys($knownSide);
214
        $unknownKeys = array_keys($unknownSide);
215
        $dualKeys = array_intersect($knownKeys, $unknownKeys);
216
        assert(count($hooks) == (count($unknownKeys) + count($knownKeys) - count($dualKeys)));
217
        // if either list is empty, bail out - there's nothing to do
218
        if (0 === count($knownSide) || 0 === count($unknownSide)) {
219
            return [];
220
        }
221
222
        // commence primary ignition
223
224
        foreach ($unknownKeys as $key) {
225
            assert(isset($hooks[$key]));
226
            $hook = $hooks[$key];
227
            foreach ($hook as $barb) {
228
                foreach ($barb as $knownType => $propData) {
229
                    if (in_array($knownType, $knownKeys)) {
230
                        if (!isset($knownSide[$knownType][$key])) {
231
                            $knownSide[$knownType][$key] = [];
232
                        }
233
                        assert(isset($knownSide[$knownType][$key]));
234
                        $knownSide[$knownType][$key][] = $propData['property'];
235
                    }
236
                }
237
            }
238
        }
239
240
        return $knownSide;
241
    }
242
243
    /**
244
     * Get round-trip relations after inserting polymorphic-powered placeholders
245
     *
246
     * @return array
247
     */
248
    public function getRepairedRoundTripRelations()
249
    {
250
        if (!isset(self::$relationCache)) {
251
            $rels = $this->calculateRoundTripRelations();
252
            $groups = $this->getPolymorphicRelationGroups();
253
254
            if (0 === count($groups)) {
255
                self::$relationCache = $rels;
256
                return $rels;
257
            }
258
259
            $placeholder = static::POLYMORPHIC;
260
261
            $groupKeys = array_keys($groups);
262
263
            // we have at least one polymorphic relation, need to dig it out
264
            $numRels = count($rels);
265
            for ($i = 0; $i < $numRels; $i++) {
266
                $relation = $rels[$i];
267
                $principalType = $relation['principalType'];
268
                $dependentType = $relation['dependentType'];
269
                $principalPoly = in_array($principalType, $groupKeys);
270
                $dependentPoly = in_array($dependentType, $groupKeys);
271
                // if relation is not polymorphic, then move on
272
                if (!($principalPoly || $dependentPoly)) {
273
                    continue;
274
                } else {
275
                    // if only one end is a known end of a polymorphic relation
276
                    // for moment we're punting on both
277
                    $oneEnd = $principalPoly !== $dependentPoly;
278
                    assert($oneEnd, 'Multi-generational polymorphic relation chains not implemented');
279
                    $targRels = $principalPoly ? $groups[$principalType] : $groups[$dependentType];
280
                    $targUnknown = $targRels[$principalPoly ? $dependentType : $principalType];
281
                    $targProperty = $principalPoly ? $relation['dependentProp'] : $relation['principalProp'];
282
                    $msg = 'Specified unknown-side property ' . $targProperty
283
                           . ' not found in polymorphic relation map';
284
                    assert(in_array($targProperty, $targUnknown), $msg);
285
286
                    $targType = $principalPoly ? 'dependentRSet' : 'principalRSet';
287
                    $rels[$i][$targType] = $placeholder;
288
                    continue;
289
                }
290
            }
291
            self::$relationCache = $rels;
292
        }
293
        return self::$relationCache;
294
    }
295
296
    /**
297
     * @param $remix
298
     * @param $lines
299
     * @return array
300
     */
301
    private function calculateRoundTripRelationsSecondPass($remix, $lines)
302
    {
303
        foreach ($remix as $principalType => $value) {
304
            foreach ($value as $fk => $localRels) {
305
                foreach ($localRels as $dependentType => $deets) {
306
                    $principalMult = $deets['multiplicity'];
307
                    $principalProperty = $deets['property'];
308
                    $principalKey = $deets['local'];
309
310
                    if (!isset($remix[$dependentType])) {
311
                        continue;
312
                    }
313
                    $foreign = $remix[$dependentType];
314
                    if (!isset($foreign[$principalKey])) {
315
                        continue;
316
                    }
317
                    $foreign = $foreign[$principalKey];
318
                    $dependentMult = $foreign[$dependentType]['multiplicity'];
319
                    $dependentProperty = $foreign[$dependentType]['property'];
320
                    assert(
321
                        in_array($dependentMult, $this->multConstraints[$principalMult]),
322
                        'Cannot pair multiplicities ' . $dependentMult . ' and ' . $principalMult
323
                    );
324
                    assert(
325
                        in_array($principalMult, $this->multConstraints[$dependentMult]),
326
                        'Cannot pair multiplicities ' . $principalMult . ' and ' . $dependentMult
327
                    );
328
                    // generate forward and reverse relations
329
                    list($forward, $reverse) = $this->calculateRoundTripRelationsGenForwardReverse(
330
                        $principalType,
331
                        $principalMult,
332
                        $principalProperty,
333
                        $dependentType,
334
                        $dependentMult,
335
                        $dependentProperty
336
                    );
337
                    // add forward relation
338
                    $lines[] = $forward;
339
                    // add reverse relation
340
                    $lines[] = $reverse;
341
                }
342
            }
343
        }
344
        return $lines;
345
    }
346
347
    /**
348
     * @param $hooks
349
     * @param $lines
350
     * @param $remix
351
     */
352
    private function calculateRoundTripRelationsFirstPass($hooks, &$lines, &$remix)
353
    {
354
        foreach ($hooks as $principalType => $value) {
355
            foreach ($value as $fk => $localRels) {
356
                foreach ($localRels as $dependentType => $deets) {
357
                    if (!isset($hooks[$dependentType])) {
358
                        continue;
359
                    }
360
                    $principalMult = $deets['multiplicity'];
361
                    $principalProperty = $deets['property'];
362
                    $principalKey = $deets['local'];
363
364
                    $foreign = $hooks[$dependentType];
365
                    $foreign = null != $foreign && isset($foreign[$principalKey]) ? $foreign[$principalKey] : null;
366
367
                    if (null != $foreign && isset($foreign[$principalType])) {
368
                        $foreign = $foreign[$principalType];
369
                        $dependentMult = $foreign['multiplicity'];
370
                        $dependentProperty = $foreign['property'];
371
                        assert(
372
                            in_array($dependentMult, $this->multConstraints[$principalMult]),
373
                            'Cannot pair multiplicities ' . $dependentMult . ' and ' . $principalMult
374
                        );
375
                        assert(
376
                            in_array($principalMult, $this->multConstraints[$dependentMult]),
377
                            'Cannot pair multiplicities ' . $principalMult . ' and ' . $dependentMult
378
                        );
379
                        // generate forward and reverse relations
380
                        list($forward, $reverse) = $this->calculateRoundTripRelationsGenForwardReverse(
381
                            $principalType,
382
                            $principalMult,
383
                            $principalProperty,
384
                            $dependentType,
385
                            $dependentMult,
386
                            $dependentProperty
387
                        );
388
                        // add forward relation
389
                        $lines[] = $forward;
390
                        // add reverse relation
391
                        $lines[] = $reverse;
392
                    } else {
393
                        if (!isset($remix[$principalType])) {
394
                            $remix[$principalType] = [];
395
                        }
396
                        if (!isset($remix[$principalType][$fk])) {
397
                            $remix[$principalType][$fk] = [];
398
                        }
399
                        if (!isset($remix[$principalType][$fk][$dependentType])) {
400
                            $remix[$principalType][$fk][$dependentType] = $deets;
401
                        }
402
                        assert(isset($remix[$principalType][$fk][$dependentType]));
403
                    }
404
                }
405
            }
406
        }
407
    }
408
409
    /**
410
     * @param $principalType
411
     * @param $principalMult
412
     * @param $principalProperty
413
     * @param $dependentType
414
     * @param $dependentMult
415
     * @param $dependentProperty
416
     * @return array[]
417
     */
418
    private function calculateRoundTripRelationsGenForwardReverse(
419
        $principalType,
420
        $principalMult,
421
        $principalProperty,
422
        $dependentType,
423
        $dependentMult,
424
        $dependentProperty
425
    ) {
426
        $forward = [
427
            'principalType' => $principalType,
428
            'principalMult' => $dependentMult,
429
            'principalProp' => $principalProperty,
430
            'dependentType' => $dependentType,
431
            'dependentMult' => $principalMult,
432
            'dependentProp' => $dependentProperty
433
        ];
434
        $reverse = [
435
            'principalType' => $dependentType,
436
            'principalMult' => $principalMult,
437
            'principalProp' => $dependentProperty,
438
            'dependentType' => $principalType,
439
            'dependentMult' => $dependentMult,
440
            'dependentProp' => $principalProperty
441
        ];
442
        return [$forward, $reverse];
443
    }
444
445
    private function processRelationLine($line, $entityTypes, &$meta)
446
    {
447
        $principalType = $line['principalType'];
448
        $principalMult = $line['principalMult'];
449
        $principalProp = $line['principalProp'];
450
        $principalRSet = $line['principalRSet'];
451
        $dependentType = $line['dependentType'];
452
        $dependentMult = $line['dependentMult'];
453
        $dependentProp = $line['dependentProp'];
454
        $dependentRSet = $line['dependentRSet'];
455
456
        if (!isset($entityTypes[$principalType]) || !isset($entityTypes[$dependentType])) {
457
            return;
458
        }
459
        $principal = $entityTypes[$principalType];
460
        $dependent = $entityTypes[$dependentType];
461
        $isPoly = static::POLYMORPHIC == $principalRSet || static::POLYMORPHIC == $dependentRSet;
462
463
        if ($isPoly) {
464
            $this->attachReferencePolymorphic(
465
                $meta,
466
                $principalMult,
467
                $dependentMult,
468
                $principal,
469
                $dependent,
470
                $principalProp,
471
                $dependentProp,
472
                $principalRSet,
473
                $dependentRSet
474
            );
475
            return null;
476
        }
477
        $this->attachReferenceNonPolymorphic(
478
            $meta,
479
            $principalMult,
480
            $dependentMult,
481
            $principal,
482
            $dependent,
483
            $principalProp,
484
            $dependentProp
485
        );
486
        return null;
487
    }
488
489
    /**
490
     * @param $meta
491
     * @param $principalMult
492
     * @param $dependentMult
493
     * @param $principal
494
     * @param $dependent
495
     * @param $principalProp
496
     * @param $dependentProp
497
     */
498
    private function attachReferenceNonPolymorphic(
499
        &$meta,
500
        $principalMult,
501
        $dependentMult,
502
        $principal,
503
        $dependent,
504
        $principalProp,
505
        $dependentProp
506
    ) {
507
        //many-to-many
508
        if ('*' == $principalMult && '*' == $dependentMult) {
509
            $meta->addResourceSetReferencePropertyBidirectional(
510
                $principal,
511
                $dependent,
512
                $principalProp,
513
                $dependentProp
514
            );
515
            return;
516
        }
517
        //one-to-one
518
        if ('0..1' == $principalMult || '0..1' == $dependentMult) {
519
            assert($principalMult != $dependentMult, 'Cannot have both ends with 0..1 multiplicity');
520
            $meta->addResourceReferenceSinglePropertyBidirectional(
521
                $principal,
522
                $dependent,
523
                $principalProp,
524
                $dependentProp
525
            );
526
            return;
527
        }
528
        assert($principalMult != $dependentMult, 'Cannot have both ends same multiplicity for 1:N relation');
529
        //principal-one-to-dependent-many
530
        if ('*' == $principalMult) {
531
            $meta->addResourceReferencePropertyBidirectional(
532
                $principal,
533
                $dependent,
534
                $principalProp,
535
                $dependentProp
536
            );
537
            return;
538
        }
539
        //dependent-one-to-principal-many
540
        $meta->addResourceReferencePropertyBidirectional(
541
            $dependent,
542
            $principal,
543
            $dependentProp,
544
            $principalProp
545
        );
546
        return;
547
    }
548
549
    /**
550
     * @param $meta
551
     * @param $principalMult
552
     * @param $dependentMult
553
     * @param $principal
554
     * @param $dependent
555
     * @param $principalProp
556
     * @param $dependentProp
557
     */
558
    private function attachReferencePolymorphic(
559
        &$meta,
560
        $principalMult,
561
        $dependentMult,
562
        $principal,
563
        $dependent,
564
        $principalProp,
565
        $dependentProp,
566
        $principalRSet,
567
        $dependentRSet
568
    ) {
569
        $prinPoly = static::POLYMORPHIC == $principalRSet;
570
        $depPoly = static::POLYMORPHIC == $dependentRSet;
571
        $principalSet = (!$prinPoly) ? $principal->getCustomState()
572
            : $meta->resolveResourceSet(static::POLYMORPHIC_PLURAL);
573
        $dependentSet = (!$depPoly) ? $dependent->getCustomState()
574
            : $meta->resolveResourceSet(static::POLYMORPHIC_PLURAL);
575
        assert($principalSet instanceof ResourceSet, $principalRSet);
576
        assert($dependentSet instanceof ResourceSet, $dependentRSet);
577
578
        $isPrincipalAdded = null !== $principal->resolveProperty($principalProp);
579
        $isDependentAdded = null !== $dependent->resolveProperty($dependentProp);
580
        $prinMany = '*' == $principalMult;
581
        $depMany = '*' == $dependentMult;
582
583
        if (!$isPrincipalAdded) {
584
            if ('*' == $principalMult || $depMany) {
585
                $meta->addResourceSetReferenceProperty($principal, $principalProp, $dependentSet);
586
            } else {
587
                $meta->addResourceReferenceProperty($principal, $principalProp, $dependentSet, $prinPoly, $depMany);
588
            }
589
        }
590
        if (!$isDependentAdded) {
591
            if ('*' == $dependentMult || $prinMany) {
592
                $meta->addResourceSetReferenceProperty($dependent, $dependentProp, $principalSet);
593
            } else {
594
                $meta->addResourceReferenceProperty($dependent, $dependentProp, $principalSet, $depPoly, $prinMany);
595
            }
596
        }
597
        return;
598
    }
599
600
    public function reset()
601
    {
602
        self::$relationCache = null;
603
    }
604
605
    /**
606
     * Resolve possible reverse relation property names
607
     *
608
     * @param Model $source
609
     * @param Model $target
610
     * @param $propName
611
     * @return string|null
612
     */
613
    public function resolveReverseProperty(Model $source, Model $target, $propName)
614
    {
615
        assert(is_string($propName), 'Property name must be string');
616
        $relations = $this->getRepairedRoundTripRelations();
617
618
        $sourceName = get_class($source);
619
        $targName = get_class($target);
620
621
        $filter = function ($segment) use ($sourceName, $targName, $propName) {
622
            $match = $sourceName == $segment['principalType'];
623
            $match &= $targName == $segment['dependentType'];
624
            $match &= $propName == $segment['principalProp'];
625
626
            return $match;
627
        };
628
629
        // array_filter does not reset keys - we have to do it ourselves
630
        $trim = array_values(array_filter($relations, $filter));
631
        $result = 0 === count($trim) ? null : $trim[0]['dependentProp'];
632
633
        return $result;
634
    }
635
}
636