Completed
Pull Request — master (#137)
by Christopher
02:30
created

MetadataProvider::implement()   C

Complexity

Conditions 8
Paths 34

Size

Total Lines 38
Code Lines 30

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 72

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 38
ccs 0
cts 0
cp 0
rs 5.3846
cc 8
eloc 30
nc 34
nop 1
crap 72
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 1
use POData\Providers\Metadata\ResourceStreamInfo;
21
use POData\Providers\Metadata\SimpleMetadataProvider;
22 1
use POData\Providers\Metadata\Type\TypeCode;
23
24
class MetadataProvider extends MetadataBaseProvider
25 1
{
26
    protected $multConstraints = ['0..1' => ['1'], '1' => ['0..1', '*'], '*' => ['1', '*']];
27
    protected static $metaNAMESPACE = 'Data';
28 1
    protected static $isBooted = false;
29 1
    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
            $gubbins = $modelInstance->extractGubbins();
86
            $isEmpty = 0 === count($gubbins->getFields());
87
            $inArtisan = $this->isRunningInArtisan();
88
            if (!($isEmpty && $inArtisan)) {
89
                $objectMap->addEntity($gubbins);
90
            }
91
        }
92
        if (null != self::$afterExtract) {
93
            $func = self::$afterExtract;
94
            $func($objectMap);
95
        }
96
        return $objectMap;
97
    }
98
99
    private function unify(Map $objectMap)
100
    {
101
        $mgh = $this->getRelationHolder();
102
        foreach ($objectMap->getEntities() as $entity) {
103
            $mgh->addEntity($entity);
104
        }
105
        $objectMap->setAssociations($mgh->getRelations());
106
        if (null != self::$afterUnify) {
107
            $func = self::$afterUnify;
108
            $func($objectMap);
109
        }
110
        return $objectMap;
111
    }
112
113
    private function verify(Map $objectModel)
114
    {
115
        $objectModel->isOK();
116
        if (null != self::$afterVerify) {
117
            $func = self::$afterVerify;
118
            $func($objectModel);
119
        }
120
    }
121
122
    private function implement(Map $objectModel)
123
    {
124
        $meta = App::make('metadata');
125
        $entities = $objectModel->getEntities();
126
        foreach ($entities as $entity) {
127
            $baseType = null;
128
            $className = $entity->getClassName();
129
            $entityName = $entity->getName();
130
            $entityType = $meta->addEntityType(new \ReflectionClass($className), $entityName, false, $baseType);
131
            assert($entityType->hasBaseType() === isset($baseType));
132
            $entity->setOdataResourceType($entityType);
133
            $this->implementProperties($entity);
134
            $meta->addResourceSet($entity->getClassName(), $entityType);
135
            $meta->oDataEntityMap[$className] = $meta->oDataEntityMap[$entityName];
136
        }
137
        $metaCount = count($meta->oDataEntityMap);
138
        $entityCount = count($entities);
139
        $expected = 2 * $entityCount;
140
        assert($metaCount == $expected, 'Expected ' . $expected . ' items, actually got '.$metaCount);
141
142
        if (null === $objectModel->getAssociations()) {
143
            return;
144
        }
145
        $assoc = $objectModel->getAssociations();
146
        $assoc = null === $assoc ? [] : $assoc;
147
        foreach ($assoc as $association) {
148
            assert($association->isOk());
149
            if ($association instanceof AssociationMonomorphic) {
150
                $this->implementAssociationsMonomorphic($objectModel, $association);
151
            } elseif ($association instanceof AssociationPolymorphic) {
152
                $this->implementAssociationsPolymorphic($objectModel, $association);
153
            }
154
        }
155
        if (null != self::$afterImplement) {
156
            $func = self::$afterImplement;
157
            $func($objectModel);
158
        }
159
    }
160
161
    private function implementAssociationsMonomorphic(Map $objectModel, AssociationMonomorphic $associationUnderHammer)
162
    {
163
        $meta = App::make('metadata');
164
        $first = $associationUnderHammer->getFirst();
165
        $last = $associationUnderHammer->getLast();
166
        switch ($associationUnderHammer->getAssociationType()) {
167
            case AssociationType::NULL_ONE_TO_NULL_ONE():
168
            case AssociationType::NULL_ONE_TO_ONE():
169 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...
170
                $meta->addResourceReferenceSinglePropertyBidirectional(
171
                    $objectModel->getEntities()[$first->getBaseType()]->getOdataResourceType(),
172
                    $objectModel->getEntities()[$last->getBaseType()]->getOdataResourceType(),
173
                    $first->getRelationName(),
174
                    $last->getRelationName()
175
                );
176
                break;
177
            case AssociationType::NULL_ONE_TO_MANY():
178
            case AssociationType::ONE_TO_MANY():
179
                if ($first->getMultiplicity()->getValue() == AssociationStubRelationType::MANY) {
180
                    $oneSide = $last;
181
                    $manySide = $first;
182
                } else {
183
                    $oneSide = $first;
184
                    $manySide = $last;
185
                }
186
                $meta->addResourceReferencePropertyBidirectional(
187
                    $objectModel->getEntities()[$oneSide->getBaseType()]->getOdataResourceType(),
188
                    $objectModel->getEntities()[$manySide->getBaseType()]->getOdataResourceType(),
189
                    $oneSide->getRelationName(),
190
                    $manySide->getRelationName()
191
                );
192
                break;
193 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...
194
                $meta->addResourceSetReferencePropertyBidirectional(
195
                    $objectModel->getEntities()[$first->getBaseType()]->getOdataResourceType(),
196
                    $objectModel->getEntities()[$last->getBaseType()]->getOdataResourceType(),
197
                    $first->getRelationName(),
198
                    $last->getRelationName()
199
                );
200
        }
201
    }
202
203
    /**
204
     * @param Map                    $objectModel
205
     * @param AssociationPolymorphic $association
206
     */
207
    private function implementAssociationsPolymorphic(Map $objectModel, AssociationPolymorphic $association)
208
    {
209
        $meta = App::make('metadata');
210
        $first = $association->getFirst();
211
212
        $polySet = $meta->resolveResourceSet(static::POLYMORPHIC_PLURAL);
213
        assert($polySet instanceof ResourceSet);
214
215
        $principalType = $objectModel->getEntities()[$first->getBaseType()]->getOdataResourceType();
216
        assert($principalType instanceof ResourceEntityType);
217
        $principalSet = $principalType->getCustomState();
218
        assert($principalSet instanceof ResourceSet);
219
        $principalProp = $first->getRelationName();
220
        $isPrincipalAdded = null !== $principalType->resolveProperty($principalProp);
221
222
        if (!$isPrincipalAdded) {
223
            if ($first->getMultiplicity()->getValue() !== AssociationStubRelationType::MANY) {
224
                $meta->addResourceReferenceProperty($principalType, $principalProp, $polySet);
225
            } else {
226
                $meta->addResourceSetReferenceProperty($principalType, $principalProp, $polySet);
227
            }
228
        }
229
230
        $types = $association->getAssociationType();
231
        $final = $association->getLast();
232
        $numRows = count($types);
233
        assert($numRows == count($final));
234
235
        for ($i = 0; $i < $numRows; $i++) {
236
            $type = $types[$i];
237
            $last = $final[$i];
238
239
            $dependentType = $objectModel->getEntities()[$last->getBaseType()]->getOdataResourceType();
240
            assert($dependentType instanceof ResourceEntityType);
241
            $dependentSet = $dependentType->getCustomState();
242
            assert($dependentSet instanceof ResourceSet);
243
            $dependentProp = $last->getRelationName();
244
            $isDependentAdded = null !== $dependentType->resolveProperty($dependentProp);
245
246
            switch ($type) {
247
                case AssociationType::NULL_ONE_TO_NULL_ONE():
248
                case AssociationType::NULL_ONE_TO_ONE():
249
                case AssociationType::ONE_TO_ONE():
250
                    if (!$isDependentAdded) {
251
                        $meta->addResourceReferenceProperty($dependentType, $dependentProp, $principalSet);
252
                    }
253
                    break;
254
                case AssociationType::NULL_ONE_TO_MANY():
255
                case AssociationType::ONE_TO_MANY():
256
                    if (!$isDependentAdded) {
257
                        $meta->addResourceSetReferenceProperty($dependentType, $dependentProp, $principalSet);
258
                    }
259
                    break;
260
                case AssociationType::MANY_TO_MANY():
261
                    if (!$isDependentAdded) {
262
                        $meta->addResourceSetReferenceProperty($dependentType, $dependentProp, $principalSet);
263
                    }
264
            }
265
        }
266
    }
267
268
    private function implementProperties(EntityGubbins $unifiedEntity)
269
    {
270
        $meta = App::make('metadata');
271
        $odataEntity = $unifiedEntity->getOdataResourceType();
272
        $keyFields = $unifiedEntity->getKeyFields();
273
        $fields = $unifiedEntity->getFields();
274
        foreach ($keyFields as $keyField) {
275
            $meta->addKeyProperty($odataEntity, $keyField->getName(), $keyField->getEdmFieldType());
276
        }
277
278
        foreach ($fields as $field) {
279
            if (in_array($field, $keyFields)) {
280
                continue;
281
            }
282
            if ($field->getPrimitiveType() == 'blob') {
283
                $odataEntity->setMediaLinkEntry(true);
284
                $streamInfo = new ResourceStreamInfo($field->getName());
285
                assert($odataEntity->isMediaLinkEntry());
286
                $odataEntity->addNamedStream($streamInfo);
287
                continue;
288
            }
289
            $meta->addPrimitiveProperty(
290
                $odataEntity,
291
                $field->getName(),
292
                $field->getEdmFieldType(),
293
                $field->getFieldType() == EntityFieldType::PRIMITIVE_BAG(),
294
                strval($field->getDefaultValue()),
295
                $field->getIsNullable()
296
            );
297
        }
298
    }
299
300
    /**
301
     * Bootstrap the application services.  Post-boot.
302
     *
303
     * @param mixed $reset
304
     *
305
     * @return void
306
     */
307
    public function boot($reset = true)
308
    {
309
        self::$metaNAMESPACE = env('ODataMetaNamespace', 'Data');
310
        // If we aren't migrated, there's no DB tables to pull metadata _from_, so bail out early
311
        try {
312
            if (!Schema::hasTable(config('database.migrations'))) {
313
                return;
314
            }
315
        } catch (\Exception $e) {
316
            return;
317
        }
318
319
        assert(false === self::$isBooted, 'Provider booted twice');
320
        $isCaching = true === $this->getIsCaching();
321
        $meta = Cache::get('metadata');
322
        $hasCache = null != $meta;
323
324
        if ($isCaching && $hasCache) {
325
            App::instance('metadata', $meta);
326
            return;
327
        }
328
        $meta = App::make('metadata');
329
        if (false !== $reset) {
330
            $this->reset();
331
        }
332
333
        $modelNames = $this->getCandidateModels();
334
        $objectModel = $this->extract($modelNames);
335
        $objectModel = $this->unify($objectModel);
336
        $this->verify($objectModel);
337
        $this->implement($objectModel);
338
        $this->completedObjectMap = $objectModel;
339
        $key = 'metadata';
340
        $this->handlePostBoot($isCaching, $hasCache, $key, $meta);
341
        self::$isBooted = true;
342
    }
343
344
    /**
345
     * Register the application services.  Boot-time only.
346
     *
347
     * @return void
348
     */
349
    public function register()
350
    {
351
        $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...
352
            return new SimpleMetadataProvider('Data', self::$metaNAMESPACE);
353
        });
354
    }
355
356
    /**
357
     * @return array
358
     */
359
    protected function getCandidateModels()
360
    {
361
        $classes = $this->getClassMap();
362
        $ends = [];
363
        $startName = defined('PODATA_LARAVEL_APP_ROOT_NAMESPACE') ? PODATA_LARAVEL_APP_ROOT_NAMESPACE : 'App';
364
        foreach ($classes as $name) {
365
            if (\Illuminate\Support\Str::startsWith($name, $startName)) {
366
                if (in_array('AlgoWeb\\PODataLaravel\\Models\\MetadataTrait', class_uses($name))) {
367
                    $ends[] = $name;
368
                }
369
            }
370
        }
371
        return $ends;
372
    }
373
374
    /**
375
     * @return MetadataGubbinsHolder
376
     */
377
    public function getRelationHolder()
378
    {
379
        return $this->relationHolder;
380
    }
381
382
    public function reset()
383
    {
384
        self::$isBooted = false;
385
        self::$afterExtract = null;
386
        self::$afterUnify = null;
387
        self::$afterVerify = null;
388
        self::$afterImplement = null;
389
    }
390
391
    /**
392
     * Resolve possible reverse relation property names.
393
     *
394
     * @param Model $source
395
     * @param Model $target
396
     * @param       $propName
397
     *
398
     * @return string|null
399
     */
400
    public function resolveReverseProperty(Model $source, Model $target, $propName)
401
    {
402
        assert(is_string($propName), 'Property name must be string');
403
        $entity = $this->getObjectMap()->resolveEntity(get_class($source));
404
        if (null === $entity) {
405
            $msg = 'Source model not defined';
406
            throw new \InvalidArgumentException($msg);
407
        }
408
        $association = $entity->resolveAssociation($propName);
409
        if (null === $association) {
410
            return null;
411
        }
412
        $isFirst = $propName === $association->getFirst()->getRelationName();
413
        if (!$isFirst) {
414
            return $association->getFirst()->getRelationName();
415
        }
416
417
        if ($association instanceof AssociationMonomorphic) {
418
            return $association->getLast()->getRelationName();
419
        }
420
        assert($association instanceof AssociationPolymorphic);
421
422
        $lasts = $association->getLast();
423
        foreach ($lasts as $stub) {
424
            if ($stub->getBaseType() == get_class($target)) {
425
                return $stub->getRelationName();
426
            }
427
        }
428
        return null;
429
    }
430
431
    public function isRunningInArtisan()
432
    {
433
        return App::runningInConsole() && !App::runningUnitTests();
434
    }
435
}
436