Completed
Push — master ( d3cb7c...64c6e5 )
by Christopher
13s
created

MetadataProvider   F

Complexity

Total Complexity 76

Size/Duplication

Total Lines 420
Duplicated Lines 3.57 %

Coupling/Cohesion

Components 1
Dependencies 17

Test Coverage

Coverage 4.41%

Importance

Changes 9
Bugs 0 Features 0
Metric Value
wmc 76
lcom 1
cbo 17
dl 15
loc 420
ccs 3
cts 68
cp 0.0441
rs 2.2388
c 9
b 0
f 0

20 Methods

Rating   Name   Duplication   Size   Complexity  
A getObjectMap() 0 4 1
A setAfterImplement() 0 4 1
A setAfterExtract() 0 4 1
A setAfterUnify() 0 4 1
A setAfterVerify() 0 4 1
A __construct() 0 6 1
B extract() 0 18 5
A unify() 0 13 3
A verify() 0 8 2
D implement() 0 37 9
C implementAssociationsMonomorphic() 15 41 8
C implementAssociationsPolymorphic() 0 60 13
C implementProperties() 0 34 7
B boot() 0 42 6
A register() 0 6 1
B getCandidateModels() 0 14 5
A getRelationHolder() 0 4 1
A reset() 0 8 1
C resolveReverseProperty() 0 30 7
A isRunningInArtisan() 0 4 2

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like MetadataProvider often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use MetadataProvider, and based on these observations, apply Extract Interface, too.

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