Test Failed
Push — master ( 122adf...2912c0 )
by Alex
02:37
created

MetadataProvider::getRepairedRoundTripRelations()   B

Complexity

Conditions 6
Paths 3

Size

Total Lines 30
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 42

Importance

Changes 2
Bugs 0 Features 0
Metric Value
c 2
b 0
f 0
dl 0
loc 30
ccs 0
cts 0
cp 0
rs 8.439
cc 6
eloc 20
nc 3
nop 0
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 $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