Completed
Pull Request — master (#123)
by Alex
02:06
created

MetadataProvider::getCandidateModels()   B

Complexity

Conditions 5
Paths 8

Size

Total Lines 14
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 30

Importance

Changes 0
Metric Value
dl 0
loc 14
ccs 0
cts 0
cp 0
rs 8.8571
c 0
b 0
f 0
cc 5
eloc 9
nc 8
nop 0
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
            $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
        $keyFields = $unifiedEntity->getKeyFields();
267
        $fields = $unifiedEntity->getFields();
268
        $affected = $unifiedEntity->isPolymorphicAffected();
269
        if (!$affected) {
270
            foreach ($keyFields as $keyField) {
271
                $meta->addKeyProperty($odataEntity, $keyField->getName(), $keyField->getEdmFieldType());
272
            }
273
        }
274
275
        foreach ($fields as $field) {
276
            if (in_array($field, $keyFields) && !$affected) {
277
                continue;
278
            }
279
            if ($field->getPrimitiveType() == 'blob') {
280
                $odataEntity->setMediaLinkEntry(true);
281
                $streamInfo = new ResourceStreamInfo($field->getName());
282
                assert($odataEntity->isMediaLinkEntry());
283
                $odataEntity->addNamedStream($streamInfo);
284
                continue;
285
            }
286
            $meta->addPrimitiveProperty(
287
                $odataEntity,
288
                $field->getName(),
289
                $field->getEdmFieldType(),
290
                $field->getFieldType() == EntityFieldType::PRIMITIVE_BAG(),
291
                $field->getDefaultValue(),
292
                $field->getIsNullable()
293
            );
294
        }
295
    }
296
297
    /**
298
     * Bootstrap the application services.  Post-boot.
299
     *
300
     * @param mixed $reset
301
     *
302
     * @return void
303
     */
304
    public function boot($reset = true)
305
    {
306
        self::$metaNAMESPACE = env('ODataMetaNamespace', 'Data');
307
        // If we aren't migrated, there's no DB tables to pull metadata _from_, so bail out early
308
        try {
309
            if (!Schema::hasTable(config('database.migrations'))) {
310
                return;
311
            }
312
        } catch (\Exception $e) {
313
            return;
314
        }
315
316
        assert(false === self::$isBooted, 'Provider booted twice');
317
        $isCaching = true === $this->getIsCaching();
318
        $meta = Cache::get('metadata');
319
        $hasCache = null != $meta;
320
321
        if ($isCaching && $hasCache) {
322
            App::instance('metadata', $meta);
323
            return;
324
        }
325
        $meta = App::make('metadata');
326
        if (false !== $reset) {
327
            $this->reset();
328
        }
329
330
        $stdRef = new \ReflectionClass(Model::class);
331
        $abstract = $meta->addEntityType($stdRef, static::POLYMORPHIC, true, null);
332
        $meta->addKeyProperty($abstract, 'PrimaryKey', TypeCode::STRING);
333
334
        $meta->addResourceSet(static::POLYMORPHIC, $abstract);
335
336
        $modelNames = $this->getCandidateModels();
337
        $objectModel = $this->extract($modelNames);
338
        $objectModel = $this->unify($objectModel);
339
        $this->verify($objectModel);
340
        $this->implement($objectModel);
341
        $this->completedObjectMap = $objectModel;
342
        $key = 'metadata';
343
        $this->handlePostBoot($isCaching, $hasCache, $key, $meta);
344
        self::$isBooted = true;
345
    }
346
347
    /**
348
     * Register the application services.  Boot-time only.
349
     *
350
     * @return void
351
     */
352
    public function register()
353
    {
354
        $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...
355
            return new SimpleMetadataProvider('Data', self::$metaNAMESPACE);
356
        });
357
    }
358
359
    /**
360
     * @return array
361
     */
362
    protected function getCandidateModels()
363
    {
364
        $classes = $this->getClassMap();
365
        $ends = [];
366
        $startName = defined('PODATA_LARAVEL_APP_ROOT_NAMESPACE') ? PODATA_LARAVEL_APP_ROOT_NAMESPACE : 'App';
367
        foreach ($classes as $name) {
368
            if (\Illuminate\Support\Str::startsWith($name, $startName)) {
369
                if (in_array('AlgoWeb\\PODataLaravel\\Models\\MetadataTrait', class_uses($name))) {
370
                    $ends[] = $name;
371
                }
372
            }
373
        }
374
        return $ends;
375
    }
376
377
    /**
378
     * @return MetadataGubbinsHolder
379
     */
380
    public function getRelationHolder()
381
    {
382
        return $this->relationHolder;
383
    }
384
385
    public function reset()
386
    {
387
        self::$isBooted = false;
388
        self::$afterExtract = null;
389
        self::$afterUnify = null;
390
        self::$afterVerify = null;
391
        self::$afterImplement = null;
392
    }
393
394
    /**
395
     * Resolve possible reverse relation property names.
396
     *
397
     * @param Model $source
398
     * @param Model $target
399
     * @param       $propName
400
     *
401
     * @return string|null
402
     */
403
    public function resolveReverseProperty(Model $source, Model $target, $propName)
404
    {
405
        assert(is_string($propName), 'Property name must be string');
406
        $entity = $this->getObjectMap()->resolveEntity(get_class($source));
407
        if (null === $entity) {
408
            $msg = 'Source model not defined';
409
            throw new \InvalidArgumentException($msg);
410
        }
411
        $association = $entity->resolveAssociation($propName);
412
        if (null === $association) {
413
            return null;
414
        }
415
        $isFirst = $propName === $association->getFirst()->getRelationName();
416
        if (!$isFirst) {
417
            return $association->getFirst()->getRelationName();
418
        }
419
420
        if ($association instanceof AssociationMonomorphic) {
421
            return $association->getLast()->getRelationName();
422
        }
423
        assert($association instanceof AssociationPolymorphic);
424
425
        $lasts = $association->getLast();
426
        foreach ($lasts as $stub) {
427
            if ($stub->getBaseType() == get_class($target)) {
428
                return $stub->getRelationName();
429
            }
430
        }
431
        return null;
432
    }
433
}
434