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