Completed
Pull Request — MetadataImplementationStep (#116)
by Alex
01:56
created

MetadataProvider::verify()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
dl 0
loc 8
ccs 0
cts 8
cp 0
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 5
nc 2
nop 1
crap 6
1
<?php
2
3
namespace AlgoWeb\PODataLaravel\Providers;
4
5
use AlgoWeb\PODataLaravel\Models\MetadataGubbinsHolder;
6
use AlgoWeb\PODataLaravel\Models\ObjectMap\Entities\Associations\Association;
7
use AlgoWeb\PODataLaravel\Models\ObjectMap\Entities\Associations\AssociationMonomorphic;
8
use AlgoWeb\PODataLaravel\Models\ObjectMap\Entities\Associations\AssociationPolymorphic;
9
use AlgoWeb\PODataLaravel\Models\ObjectMap\Entities\Associations\AssociationStubRelationType;
10
use AlgoWeb\PODataLaravel\Models\ObjectMap\Entities\Associations\AssociationType;
11
use AlgoWeb\PODataLaravel\Models\ObjectMap\Entities\EntityFieldType;
12
use AlgoWeb\PODataLaravel\Models\ObjectMap\Entities\EntityGubbins;
13
use AlgoWeb\PODataLaravel\Models\ObjectMap\Map;
14
use Illuminate\Database\Eloquent\Model;
15
use Illuminate\Support\Facades\App;
16
use Illuminate\Support\Facades\Cache;
17
use Illuminate\Support\Facades\Schema as Schema;
18
use POData\Providers\Metadata\ResourceEntityType;
19
use POData\Providers\Metadata\ResourceSet;
20
use POData\Providers\Metadata\ResourceStreamInfo;
21
use POData\Providers\Metadata\SimpleMetadataProvider;
22
use POData\Providers\Metadata\Type\TypeCode;
23
24
class MetadataProvider extends MetadataBaseProvider
25
{
26
    protected $multConstraints = ['0..1' => ['1'], '1' => ['0..1', '*'], '*' => ['1', '*']];
27
    protected static $metaNAMESPACE = 'Data';
28
    protected static $isBooted = false;
29
    const POLYMORPHIC = 'polyMorphicPlaceholder';
30
    const POLYMORPHIC_PLURAL = 'polyMorphicPlaceholders';
31
32
    /**
33
     * @var Map The completed object map set at post Implement;
34
     */
35
    private $completedObjectMap;
36
37
    /**
38
     * @return \AlgoWeb\PODataLaravel\Models\ObjectMap\Map
39
     */
40
    public function getObjectMap()
41
    {
42
        return $this->completedObjectMap;
43
    }
44
45
    protected static $afterExtract;
46
    protected static $afterUnify;
47
    protected static $afterVerify;
48
    protected static $afterImplement;
49
50
    public static function setAfterExtract(callable $method)
51
    {
52
        self::$afterExtract = $method;
53
    }
54
55
    public static function setAfterUnify(callable $method)
56
    {
57
        self::$afterUnify = $method;
58
    }
59
60
    public static function setAfterVerify(callable $method)
61
    {
62
        self::$afterVerify = $method;
63
    }
64
65
    public static function setAfterImplement(callable $method)
66
    {
67
        self::$afterImplement = $method;
68
    }
69
70
71
    protected $relationHolder;
72
73
    public function __construct($app)
74
    {
75
        parent::__construct($app);
76
        $this->relationHolder = new MetadataGubbinsHolder();
77
        self::$isBooted = false;
78
    }
79
80
    private function extract(array $modelNames)
81
    {
82
        $objectMap = new Map();
83
        foreach ($modelNames as $modelName) {
84
            $modelInstance = App::make($modelName);
85
            $objectMap->addEntity($modelInstance->extractGubbins());
86
        }
87
        if (null != self::$afterExtract) {
88
            $func = self::$afterExtract;
89
            $func($objectMap);
90
        }
91
        return $objectMap;
92
    }
93
94
    private function unify(Map $objectMap)
95
    {
96
        $mgh = $this->getRelationHolder();
97
        foreach ($objectMap->getEntities() as $entity) {
98
            $mgh->addEntity($entity);
99
        }
100
        $objectMap->setAssociations($mgh->getRelations());
101
        if (null != self::$afterUnify) {
102
            $func = self::$afterUnify;
103
            $func($objectMap);
104
        }
105
        return $objectMap;
106
    }
107
108
    private function verify(Map $objectModel)
109
    {
110
        $objectModel->isOK();
111
        if (null != self::$afterVerify) {
112
            $func = self::$afterVerify;
113
            $func($objectModel);
114
        }
115
    }
116
117
    private function implement(Map $objectModel)
118
    {
119
        $meta = App::make('metadata');
120
        $entities = $objectModel->getEntities();
121
        foreach ($entities as $entity) {
122
            $baseType = $entity->isPolymorphicAffected() ? $meta->resolveResourceType('polyMorphicPlaceholder') : null;
123
            $className = $entity->getClassName();
124
            $entityName = $entity->getName();
125
            $entityType = $meta->addEntityType(new \ReflectionClass($className), $entityName, false, $baseType);
126
            assert($entityType->hasBaseType() === isset($baseType));
127
            $entity->setOdataResourceType($entityType);
128
            $this->implementProperties($entity);
129
            $meta->addResourceSet($entity->getClassName(), $entityType);
130
            $meta->oDataEntityMap[$className] = $meta->oDataEntityMap[$entityName];
131
        }
132
        $metaCount = count($meta->oDataEntityMap);
133
        $entityCount = count($entities);
134
        assert($metaCount == 2 * $entityCount + 1);
135
136
        if (null === $objectModel->getAssociations()) {
137
            return;
138
        }
139
        $assoc = $objectModel->getAssociations();
140
        $assoc = null === $assoc ? [] : $assoc;
141
        foreach ($assoc as $association) {
142
            assert($association->isOk());
143
            if ($association instanceof AssociationMonomorphic) {
144
                $this->implementAssociationsMonomorphic($objectModel, $association);
145
            } elseif ($association instanceof AssociationPolymorphic) {
146
                $this->implementAssociationsPolymorphic($objectModel, $association);
147
            }
148
        }
149
        if (null != self::$afterImplement) {
150
            $func = self::$afterImplement;
151
            $func($objectModel);
152
        }
153
    }
154
155
    private function implementAssociationsMonomorphic(Map $objectModel, AssociationMonomorphic $associationUnderHammer)
156
    {
157
        $meta = App::make('metadata');
158
        $first = $associationUnderHammer->getFirst();
159
        $last = $associationUnderHammer->getLast();
160
        switch ($associationUnderHammer->getAssociationType()) {
161
            case AssociationType::NULL_ONE_TO_NULL_ONE():
162
            case AssociationType::NULL_ONE_TO_ONE():
163 View Code Duplication
            case AssociationType::ONE_TO_ONE():
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
164
                $meta->addResourceReferenceSinglePropertyBidirectional(
165
                    $objectModel->getEntities()[$first->getBaseType()]->getOdataResourceType(),
166
                    $objectModel->getEntities()[$last->getBaseType()]->getOdataResourceType(),
167
                    $first->getRelationName(),
168
                    $last->getRelationName()
169
                );
170
                break;
171
            case AssociationType::NULL_ONE_TO_MANY():
172
            case AssociationType::ONE_TO_MANY():
173
                if ($first->getMultiplicity()->getValue() == AssociationStubRelationType::MANY) {
174
                    $oneSide = $last;
175
                    $manySide = $first;
176
                } else {
177
                    $oneSide = $first;
178
                    $manySide = $last;
179
                }
180
                $meta->addResourceReferencePropertyBidirectional(
181
                    $objectModel->getEntities()[$oneSide->getBaseType()]->getOdataResourceType(),
182
                    $objectModel->getEntities()[$manySide->getBaseType()]->getOdataResourceType(),
183
                    $oneSide->getRelationName(),
184
                    $manySide->getRelationName()
185
                );
186
                break;
187 View Code Duplication
            case AssociationType::MANY_TO_MANY():
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
188
                $meta->addResourceSetReferencePropertyBidirectional(
189
                    $objectModel->getEntities()[$first->getBaseType()]->getOdataResourceType(),
190
                    $objectModel->getEntities()[$last->getBaseType()]->getOdataResourceType(),
191
                    $first->getRelationName(),
192
                    $last->getRelationName()
193
                );
194
        }
195
    }
196
197
    /**
198
     * @param Map                    $objectModel
199
     * @param AssociationPolymorphic $association
200
     */
201
    private function implementAssociationsPolymorphic(Map $objectModel, AssociationPolymorphic $association)
202
    {
203
        $meta = App::make('metadata');
204
        $first = $association->getFirst();
205
206
        $polySet = $meta->resolveResourceSet(static::POLYMORPHIC_PLURAL);
207
        assert($polySet instanceof ResourceSet);
208
209
        $principalType = $objectModel->getEntities()[$first->getBaseType()]->getOdataResourceType();
210
        assert($principalType instanceof ResourceEntityType);
211
        $principalSet = $principalType->getCustomState();
212
        assert($principalSet instanceof ResourceSet);
213
        $principalProp = $first->getRelationName();
214
        $isPrincipalAdded = null !== $principalType->resolveProperty($principalProp);
215
216
        if (!$isPrincipalAdded) {
217
            if ($first->getMultiplicity()->getValue() !== AssociationStubRelationType::MANY) {
218
                $meta->addResourceReferenceProperty($principalType, $principalProp, $polySet);
219
            } else {
220
                $meta->addResourceSetReferenceProperty($principalType, $principalProp, $polySet);
221
            }
222
        }
223
224
        $types = $association->getAssociationType();
225
        $final = $association->getLast();
226
        $numRows = count($types);
227
        assert($numRows == count($final));
228
229
        for ($i = 0; $i < $numRows; $i++) {
230
            $type = $types[$i];
231
            $last = $final[$i];
232
233
            $dependentType = $objectModel->getEntities()[$last->getBaseType()]->getOdataResourceType();
234
            assert($dependentType instanceof ResourceEntityType);
235
            $dependentSet = $dependentType->getCustomState();
236
            assert($dependentSet instanceof ResourceSet);
237
            $dependentProp = $last->getRelationName();
238
            $isDependentAdded = null !== $dependentType->resolveProperty($dependentProp);
239
240
            switch ($type) {
241
                case AssociationType::NULL_ONE_TO_NULL_ONE():
242
                case AssociationType::NULL_ONE_TO_ONE():
243
                case AssociationType::ONE_TO_ONE():
244
                    if (!$isDependentAdded) {
245
                        $meta->addResourceReferenceProperty($dependentType, $dependentProp, $principalSet);
246
                    }
247
                    break;
248
                case AssociationType::NULL_ONE_TO_MANY():
249
                case AssociationType::ONE_TO_MANY():
250
                    if (!$isDependentAdded) {
251
                        $meta->addResourceSetReferenceProperty($dependentType, $dependentProp, $principalSet);
252
                    }
253
                    break;
254
                case AssociationType::MANY_TO_MANY():
255
                    if (!$isDependentAdded) {
256
                        $meta->addResourceSetReferenceProperty($dependentType, $dependentProp, $principalSet);
257
                    }
258
            }
259
        }
260
    }
261
262
    private function implementProperties(EntityGubbins $unifiedEntity)
263
    {
264
        $meta = App::make('metadata');
265
        $odataEntity = $unifiedEntity->getOdataResourceType();
266
        if (!$unifiedEntity->isPolymorphicAffected()) {
267
            foreach ($unifiedEntity->getKeyFields() as $keyField) {
268
                $meta->addKeyProperty($odataEntity, $keyField->getName(), $keyField->getEdmFieldType());
269
            }
270
        }
271
        foreach ($unifiedEntity->getFields() as $field) {
272
            if (in_array($field, $unifiedEntity->getKeyFields())) {
273
                continue;
274
            }
275
            if ($field->getPrimitiveType() == 'blob') {
276
                $odataEntity->setMediaLinkEntry(true);
277
                $streamInfo = new ResourceStreamInfo($field->getName());
278
                assert($odataEntity->isMediaLinkEntry());
279
                $odataEntity->addNamedStream($streamInfo);
280
                continue;
281
            }
282
            $meta->addPrimitiveProperty(
283
                $odataEntity,
284
                $field->getName(),
285
                $field->getEdmFieldType(),
286
                $field->getFieldType() == EntityFieldType::PRIMITIVE_BAG(),
287
                $field->getDefaultValue(),
288
                $field->getIsNullable()
289
            );
290
        }
291
    }
292
293
    /**
294
     * Bootstrap the application services.  Post-boot.
295
     *
296
     * @param  mixed $reset
297
     *
298
     * @return void
299
     */
300
    public function boot($reset = true)
301
    {
302
        self::$metaNAMESPACE = env('ODataMetaNamespace', 'Data');
303
        // If we aren't migrated, there's no DB tables to pull metadata _from_, so bail out early
304
        try {
305
            if (!Schema::hasTable(config('database.migrations'))) {
306
                return;
307
            }
308
        } catch (\Exception $e) {
309
            return;
310
        }
311
312
        assert(false === self::$isBooted, 'Provider booted twice');
313
        $isCaching = true === $this->getIsCaching();
314
        $meta = Cache::get('metadata');
315
        $hasCache = null != $meta;
316
317
        if ($isCaching && $hasCache) {
318
            App::instance('metadata', $meta);
319
            return;
320
        }
321
        $meta = App::make('metadata');
322
        if (false !== $reset) {
323
            $this->reset();
324
        }
325
326
        $stdRef = new \ReflectionClass(Model::class);
327
        $abstract = $meta->addEntityType($stdRef, static::POLYMORPHIC, true, null);
328
        $meta->addKeyProperty($abstract, 'PrimaryKey', TypeCode::STRING);
329
330
        $meta->addResourceSet(static::POLYMORPHIC, $abstract);
331
332
        $modelNames = $this->getCandidateModels();
333
        $objectModel = $this->extract($modelNames);
334
        $objectModel = $this->unify($objectModel);
335
        $this->verify($objectModel);
336
        $this->implement($objectModel);
337
        $this->completedObjectMap = $objectModel;
338
        $key = 'metadata';
339
        $this->handlePostBoot($isCaching, $hasCache, $key, $meta);
340
        self::$isBooted = true;
341
    }
342
343
    /**
344
     * Register the application services.  Boot-time only.
345
     *
346
     * @return void
347
     */
348
    public function register()
349
    {
350
        $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...
351
            return new SimpleMetadataProvider('Data', self::$metaNAMESPACE);
352
        });
353
    }
354
355
    /**
356
     * @return array
357
     */
358
    protected function getCandidateModels()
359
    {
360
        $classes = $this->getClassMap();
361
        $ends = [];
362
        $startName = defined('PODATA_LARAVEL_APP_ROOT_NAMESPACE') ? PODATA_LARAVEL_APP_ROOT_NAMESPACE : 'App';
363
        foreach ($classes as $name) {
364
            if (\Illuminate\Support\Str::startsWith($name, $startName)) {
365
                if (in_array('AlgoWeb\\PODataLaravel\\Models\\MetadataTrait', class_uses($name))) {
366
                    $ends[] = $name;
367
                }
368
            }
369
        }
370
        return $ends;
371
    }
372
373
    /**
374
     * @return MetadataGubbinsHolder
375
     */
376
    public function getRelationHolder()
377
    {
378
        return $this->relationHolder;
379
    }
380
381
    public function reset()
382
    {
383
        self::$isBooted = false;
384
        self::$afterExtract = null;
385
        self::$afterUnify = null;
386
        self::$afterVerify = null;
387
        self::$afterImplement = null;
388
    }
389
390
    /**
391
     * Resolve possible reverse relation property names.
392
     *
393
     * @param Model $source
394
     * @param Model $target
395
     * @param       $propName
396
     *
397
     * @return string|null
398
     */
399
    public function resolveReverseProperty(Model $source, Model $target, $propName)
400
    {
401
        assert(is_string($propName), 'Property name must be string');
402
        $entity = $this->getObjectMap()->resolveEntity(get_class($source));
403
        if (null === $entity) {
404
            $msg = 'Source model not defined';
405
            throw new \InvalidArgumentException($msg);
406
        }
407
        $association = $entity->resolveAssociation($propName);
408
        if (null === $association) {
409
            return null;
410
        }
411
        $isFirst = $propName === $association->getFirst()->getRelationName();
412
        if (!$isFirst) {
413
            return $association->getFirst()->getRelationName();
414
        }
415
416
        if ($association instanceof AssociationMonomorphic) {
417
            return $association->getLast()->getRelationName();
418
        }
419
        assert($association instanceof AssociationPolymorphic);
420
421
        $lasts = $association->getLast();
422
        foreach ($lasts as $stub) {
423
            if ($stub->getBaseType() == get_class($target)) {
424
                return $stub->getRelationName();
425
            }
426
        }
427
        return null;
428
    }
429
}
430