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

MetadataProvider::implementProperties()   B

Complexity

Conditions 5
Paths 8

Size

Total Lines 31
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 30

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 31
ccs 0
cts 0
cp 0
rs 8.439
cc 5
eloc 23
nc 8
nop 1
crap 30
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
        assert($metaCount == 2 * $entityCount + 1);
140
141
        if (null === $objectModel->getAssociations()) {
142
            return;
143
        }
144
        $assoc = $objectModel->getAssociations();
145
        $assoc = null === $assoc ? [] : $assoc;
146
        foreach ($assoc as $association) {
147
            assert($association->isOk());
148
            if ($association instanceof AssociationMonomorphic) {
149
                $this->implementAssociationsMonomorphic($objectModel, $association);
150
            } elseif ($association instanceof AssociationPolymorphic) {
151
                $this->implementAssociationsPolymorphic($objectModel, $association);
152
            }
153
        }
154
        if (null != self::$afterImplement) {
155
            $func = self::$afterImplement;
156
            $func($objectModel);
157
        }
158
    }
159
160
    private function implementAssociationsMonomorphic(Map $objectModel, AssociationMonomorphic $associationUnderHammer)
161
    {
162
        $meta = App::make('metadata');
163
        $first = $associationUnderHammer->getFirst();
164
        $last = $associationUnderHammer->getLast();
165
        switch ($associationUnderHammer->getAssociationType()) {
166
            case AssociationType::NULL_ONE_TO_NULL_ONE():
167
            case AssociationType::NULL_ONE_TO_ONE():
168 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...
169
                $meta->addResourceReferenceSinglePropertyBidirectional(
170
                    $objectModel->getEntities()[$first->getBaseType()]->getOdataResourceType(),
171
                    $objectModel->getEntities()[$last->getBaseType()]->getOdataResourceType(),
172
                    $first->getRelationName(),
173
                    $last->getRelationName()
174
                );
175
                break;
176
            case AssociationType::NULL_ONE_TO_MANY():
177
            case AssociationType::ONE_TO_MANY():
178
                if ($first->getMultiplicity()->getValue() == AssociationStubRelationType::MANY) {
179
                    $oneSide = $last;
180
                    $manySide = $first;
181
                } else {
182
                    $oneSide = $first;
183
                    $manySide = $last;
184
                }
185
                $meta->addResourceReferencePropertyBidirectional(
186
                    $objectModel->getEntities()[$oneSide->getBaseType()]->getOdataResourceType(),
187
                    $objectModel->getEntities()[$manySide->getBaseType()]->getOdataResourceType(),
188
                    $oneSide->getRelationName(),
189
                    $manySide->getRelationName()
190
                );
191
                break;
192 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...
193
                $meta->addResourceSetReferencePropertyBidirectional(
194
                    $objectModel->getEntities()[$first->getBaseType()]->getOdataResourceType(),
195
                    $objectModel->getEntities()[$last->getBaseType()]->getOdataResourceType(),
196
                    $first->getRelationName(),
197
                    $last->getRelationName()
198
                );
199
        }
200
    }
201
202
    /**
203
     * @param Map                    $objectModel
204
     * @param AssociationPolymorphic $association
205
     */
206
    private function implementAssociationsPolymorphic(Map $objectModel, AssociationPolymorphic $association)
207
    {
208
        $meta = App::make('metadata');
209
        $first = $association->getFirst();
210
211
        $polySet = $meta->resolveResourceSet(static::POLYMORPHIC_PLURAL);
212
        assert($polySet instanceof ResourceSet);
213
214
        $principalType = $objectModel->getEntities()[$first->getBaseType()]->getOdataResourceType();
215
        assert($principalType instanceof ResourceEntityType);
216
        $principalSet = $principalType->getCustomState();
217
        assert($principalSet instanceof ResourceSet);
218
        $principalProp = $first->getRelationName();
219
        $isPrincipalAdded = null !== $principalType->resolveProperty($principalProp);
220
221
        if (!$isPrincipalAdded) {
222
            if ($first->getMultiplicity()->getValue() !== AssociationStubRelationType::MANY) {
223
                $meta->addResourceReferenceProperty($principalType, $principalProp, $polySet);
224
            } else {
225
                $meta->addResourceSetReferenceProperty($principalType, $principalProp, $polySet);
226
            }
227
        }
228
229
        $types = $association->getAssociationType();
230
        $final = $association->getLast();
231
        $numRows = count($types);
232
        assert($numRows == count($final));
233
234
        for ($i = 0; $i < $numRows; $i++) {
235
            $type = $types[$i];
236
            $last = $final[$i];
237
238
            $dependentType = $objectModel->getEntities()[$last->getBaseType()]->getOdataResourceType();
239
            assert($dependentType instanceof ResourceEntityType);
240
            $dependentSet = $dependentType->getCustomState();
241
            assert($dependentSet instanceof ResourceSet);
242
            $dependentProp = $last->getRelationName();
243
            $isDependentAdded = null !== $dependentType->resolveProperty($dependentProp);
244
245
            switch ($type) {
246
                case AssociationType::NULL_ONE_TO_NULL_ONE():
247
                case AssociationType::NULL_ONE_TO_ONE():
248
                case AssociationType::ONE_TO_ONE():
249
                    if (!$isDependentAdded) {
250
                        $meta->addResourceReferenceProperty($dependentType, $dependentProp, $principalSet);
251
                    }
252
                    break;
253
                case AssociationType::NULL_ONE_TO_MANY():
254
                case AssociationType::ONE_TO_MANY():
255
                    if (!$isDependentAdded) {
256
                        $meta->addResourceSetReferenceProperty($dependentType, $dependentProp, $principalSet);
257
                    }
258
                    break;
259
                case AssociationType::MANY_TO_MANY():
260
                    if (!$isDependentAdded) {
261
                        $meta->addResourceSetReferenceProperty($dependentType, $dependentProp, $principalSet);
262
                    }
263
            }
264
        }
265
    }
266
267
    private function implementProperties(EntityGubbins $unifiedEntity)
268
    {
269
        $meta = App::make('metadata');
270
        $odataEntity = $unifiedEntity->getOdataResourceType();
271
        $keyFields = $unifiedEntity->getKeyFields();
272
        $fields = $unifiedEntity->getFields();
273
        foreach ($keyFields as $keyField) {
274
            $meta->addKeyProperty($odataEntity, $keyField->getName(), $keyField->getEdmFieldType());
275
        }
276
277
        foreach ($fields as $field) {
278
            if (in_array($field, $keyFields)) {
279
                continue;
280
            }
281
            if ($field->getPrimitiveType() == 'blob') {
282
                $odataEntity->setMediaLinkEntry(true);
283
                $streamInfo = new ResourceStreamInfo($field->getName());
284
                assert($odataEntity->isMediaLinkEntry());
285
                $odataEntity->addNamedStream($streamInfo);
286
                continue;
287
            }
288
            $meta->addPrimitiveProperty(
289
                $odataEntity,
290
                $field->getName(),
291
                $field->getEdmFieldType(),
292
                $field->getFieldType() == EntityFieldType::PRIMITIVE_BAG(),
293
                strval($field->getDefaultValue()),
294
                $field->getIsNullable()
295
            );
296
        }
297
    }
298
299
    /**
300
     * Bootstrap the application services.  Post-boot.
301
     *
302
     * @param mixed $reset
303
     *
304
     * @return void
305
     */
306
    public function boot($reset = true)
307
    {
308
        self::$metaNAMESPACE = env('ODataMetaNamespace', 'Data');
309
        // If we aren't migrated, there's no DB tables to pull metadata _from_, so bail out early
310
        try {
311
            if (!Schema::hasTable(config('database.migrations'))) {
312
                return;
313
            }
314
        } catch (\Exception $e) {
315
            return;
316
        }
317
318
        assert(false === self::$isBooted, 'Provider booted twice');
319
        $isCaching = true === $this->getIsCaching();
320
        $meta = Cache::get('metadata');
321
        $hasCache = null != $meta;
322
323
        if ($isCaching && $hasCache) {
324
            App::instance('metadata', $meta);
325
            return;
326
        }
327
        $meta = App::make('metadata');
328
        if (false !== $reset) {
329
            $this->reset();
330
        }
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
    public function isRunningInArtisan()
431
    {
432
        return App::runningInConsole() && !App::runningUnitTests();
433
    }
434
}
435