Passed
Pull Request — master (#154)
by Alex
07:05
created

MetadataProvider   D

Complexity

Total Complexity 60

Size/Duplication

Total Lines 348
Duplicated Lines 4.31 %

Test Coverage

Coverage 4.41%

Importance

Changes 0
Metric Value
wmc 60
dl 15
loc 348
ccs 3
cts 68
cp 0.0441
rs 4.2857
c 0
b 0
f 0

19 Methods

Rating   Name   Duplication   Size   Complexity  
A setAfterImplement() 0 3 1
A setAfterVerify() 0 3 1
A unify() 0 12 3
C implementAssociationsMonomorphic() 14 38 8
A getRelationHolder() 0 3 1
B getCandidateModels() 0 14 6
A setAfterExtract() 0 3 1
A verify() 0 6 2
A __construct() 0 5 1
A register() 0 7 1
B implement() 0 32 6
C implementProperties() 0 33 7
B extract() 0 22 6
A isRunningInArtisan() 0 3 2
A getObjectMap() 0 3 1
A setAfterUnify() 0 3 1
A resolveReverseProperty() 0 19 4
A reset() 0 7 1
C boot() 0 39 7

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.

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\AssociationStubRelationType;
9
use AlgoWeb\PODataLaravel\Models\ObjectMap\Entities\Associations\AssociationType;
10
use AlgoWeb\PODataLaravel\Models\ObjectMap\Entities\EntityFieldType;
11
use AlgoWeb\PODataLaravel\Models\ObjectMap\Entities\EntityGubbins;
12
use AlgoWeb\PODataLaravel\Models\ObjectMap\Map;
13
use Illuminate\Contracts\Container\BindingResolutionException;
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 = App::make('objectmap');
83
        foreach ($modelNames as $modelName) {
84
            try {
85
                $modelInstance = App::make($modelName);
86
            } catch (BindingResolutionException $e) {
87
                // if we can't instantiate modelName for whatever reason, move on
88
                continue;
89
            }
90
            $gubbins = $modelInstance->extractGubbins();
91
            $isEmpty = 0 === count($gubbins->getFields());
92
            $inArtisan = $this->isRunningInArtisan();
93
            if (!($isEmpty && $inArtisan)) {
94
                $objectMap->addEntity($gubbins);
95
            }
96
        }
97
        if (null != self::$afterExtract) {
98
            $func = self::$afterExtract;
99
            $func($objectMap);
100
        }
101
        return $objectMap;
102
    }
103
104
    private function unify(Map $objectMap)
105
    {
106
        $mgh = $this->getRelationHolder();
107
        foreach ($objectMap->getEntities() as $entity) {
108
            $mgh->addEntity($entity);
109
        }
110
        $objectMap->setAssociations($mgh->getRelations());
111
        if (null != self::$afterUnify) {
112
            $func = self::$afterUnify;
113
            $func($objectMap);
114
        }
115
        return $objectMap;
116
    }
117
118
    private function verify(Map $objectModel)
119
    {
120
        $objectModel->isOK();
121
        if (null != self::$afterVerify) {
122
            $func = self::$afterVerify;
123
            $func($objectModel);
124
        }
125
    }
126
127
    private function implement(Map $objectModel)
128
    {
129
        $meta = App::make('metadata');
130
        $entities = $objectModel->getEntities();
131
        foreach ($entities as $entity) {
132
            $baseType = null;
133
            $className = $entity->getClassName();
134
            $entityName = $entity->getName();
135
            $entityType = $meta->addEntityType(new \ReflectionClass($className), $entityName, false, $baseType);
136
            assert($entityType->hasBaseType() === isset($baseType));
0 ignored issues
show
Bug introduced by
The call to assert() has too few arguments starting with description. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

136
            /** @scrutinizer ignore-call */ 
137
            assert($entityType->hasBaseType() === isset($baseType));

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
137
            $entity->setOdataResourceType($entityType);
138
            $this->implementProperties($entity);
139
            $meta->addResourceSet($entity->getClassName(), $entityType);
140
            $meta->oDataEntityMap[$className] = $meta->oDataEntityMap[$entityName];
141
        }
142
        $metaCount = count($meta->oDataEntityMap);
143
        $entityCount = count($entities);
144
        $expected = 2 * $entityCount;
145
        assert($metaCount == $expected, 'Expected ' . $expected . ' items, actually got '.$metaCount);
146
147
        if (0 === count($objectModel->getAssociations())) {
148
            return;
149
        }
150
        $assoc = $objectModel->getAssociations();
151
        $assoc = null === $assoc ? [] : $assoc;
152
        foreach ($assoc as $association) {
153
            assert($association->isOk());
154
            $this->implementAssociationsMonomorphic($objectModel, $association);
155
        }
156
        if (null != self::$afterImplement) {
157
            $func = self::$afterImplement;
158
            $func($objectModel);
159
        }
160
    }
161
162
    private function implementAssociationsMonomorphic(Map $objectModel, AssociationMonomorphic $associationUnderHammer)
163
    {
164
        $meta = App::make('metadata');
165
        $first = $associationUnderHammer->getFirst();
166
        $last = $associationUnderHammer->getLast();
167
        switch ($associationUnderHammer->getAssociationType()) {
168
            case AssociationType::NULL_ONE_TO_NULL_ONE():
169
            case AssociationType::NULL_ONE_TO_ONE():
170 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...
171
                $meta->addResourceReferenceSinglePropertyBidirectional(
172
                    $objectModel->getEntities()[$first->getBaseType()]->getOdataResourceType(),
173
                    $objectModel->getEntities()[$last->getBaseType()]->getOdataResourceType(),
174
                    $first->getRelationName(),
175
                    $last->getRelationName()
176
                );
177
                break;
178
            case AssociationType::NULL_ONE_TO_MANY():
179
            case AssociationType::ONE_TO_MANY():
180
                if ($first->getMultiplicity()->getValue() == AssociationStubRelationType::MANY) {
181
                    $oneSide = $last;
182
                    $manySide = $first;
183
                } else {
184
                    $oneSide = $first;
185
                    $manySide = $last;
186
                }
187
                $meta->addResourceReferencePropertyBidirectional(
188
                    $objectModel->getEntities()[$oneSide->getBaseType()]->getOdataResourceType(),
189
                    $objectModel->getEntities()[$manySide->getBaseType()]->getOdataResourceType(),
190
                    $oneSide->getRelationName(),
191
                    $manySide->getRelationName()
192
                );
193
                break;
194 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...
195
                $meta->addResourceSetReferencePropertyBidirectional(
196
                    $objectModel->getEntities()[$first->getBaseType()]->getOdataResourceType(),
197
                    $objectModel->getEntities()[$last->getBaseType()]->getOdataResourceType(),
198
                    $first->getRelationName(),
199
                    $last->getRelationName()
200
                );
201
        }
202
    }
203
204
    private function implementProperties(EntityGubbins $unifiedEntity)
205
    {
206
        $meta = App::make('metadata');
207
        $odataEntity = $unifiedEntity->getOdataResourceType();
208
        $keyFields = $unifiedEntity->getKeyFields();
209
        $fields = $unifiedEntity->getFields();
210
        foreach ($keyFields as $keyField) {
211
            $meta->addKeyProperty($odataEntity, $keyField->getName(), $keyField->getEdmFieldType());
212
        }
213
214
        foreach ($fields as $field) {
215
            if (in_array($field, $keyFields)) {
216
                continue;
217
            }
218
            if ($field->getPrimitiveType() == 'blob') {
219
                $odataEntity->setMediaLinkEntry(true);
220
                $streamInfo = new ResourceStreamInfo($field->getName());
221
                assert($odataEntity->isMediaLinkEntry());
0 ignored issues
show
Bug introduced by
The call to assert() has too few arguments starting with description. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

221
                /** @scrutinizer ignore-call */ 
222
                assert($odataEntity->isMediaLinkEntry());

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
222
                $odataEntity->addNamedStream($streamInfo);
223
                continue;
224
            }
225
226
            $default = $field->getDefaultValue();
227
            $isFieldBool = TypeCode::BOOLEAN == $field->getEdmFieldType();
228
            $default = $isFieldBool ? ($default ? 'true' : 'false') : strval($default);
229
230
            $meta->addPrimitiveProperty(
231
                $odataEntity,
232
                $field->getName(),
233
                $field->getEdmFieldType(),
234
                $field->getFieldType() == EntityFieldType::PRIMITIVE_BAG(),
235
                $default,
236
                $field->getIsNullable()
237
            );
238
        }
239
    }
240
241
    /**
242
     * Bootstrap the application services.  Post-boot.
243
     *
244
     * @param mixed $reset
245
     *
246
     * @return void
247
     */
248
    public function boot($reset = true)
249
    {
250
        self::$metaNAMESPACE = env('ODataMetaNamespace', 'Data');
251
        // If we aren't migrated, there's no DB tables to pull metadata _from_, so bail out early
252
        try {
253
            if (!Schema::hasTable(config('database.migrations'))) {
254
                return;
255
            }
256
        } catch (\Exception $e) {
257
            return;
258
        }
259
260
        assert(false === self::$isBooted, 'Provider booted twice');
261
        $isCaching = true === $this->getIsCaching();
262
        $meta = Cache::get('metadata');
263
        $objectMap = Cache::get('objectmap');
264
        $hasCache = null != $meta && null != $objectMap;
265
266
        if ($isCaching && $hasCache) {
267
            App::instance('metadata', $meta);
268
            App::instance('objectmap', $objectMap);
269
            return;
270
        }
271
        $meta = App::make('metadata');
272
        if (false !== $reset) {
273
            $this->reset();
274
        }
275
276
        $modelNames = $this->getCandidateModels();
277
        $objectModel = $this->extract($modelNames);
278
        $objectModel = $this->unify($objectModel);
279
        $this->verify($objectModel);
280
        $this->implement($objectModel);
281
        $this->completedObjectMap = $objectModel;
282
        $key = 'metadata';
283
        $objKey = 'objectmap';
284
        $this->handlePostBoot($isCaching, $hasCache, $key, $meta);
285
        $this->handlePostBoot($isCaching, $hasCache, $objKey, $objectModel);
286
        self::$isBooted = true;
287
    }
288
289
    /**
290
     * Register the application services.  Boot-time only.
291
     *
292
     * @return void
293
     */
294
    public function register()
295
    {
296
        $this->app->singleton('metadata', function ($app) {
0 ignored issues
show
Unused Code introduced by
The parameter $app is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

296
        $this->app->singleton('metadata', function (/** @scrutinizer ignore-unused */ $app) {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
297
            return new SimpleMetadataProvider('Data', self::$metaNAMESPACE);
298
        });
299
        $this->app->singleton('objectmap', function ($app) {
0 ignored issues
show
Unused Code introduced by
The parameter $app is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

299
        $this->app->singleton('objectmap', function (/** @scrutinizer ignore-unused */ $app) {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
300
            return new Map();
301
        });
302
    }
303
304
    /**
305
     * @return array
306
     */
307
    protected function getCandidateModels()
308
    {
309
        $classes = $this->getClassMap();
310
        $ends = [];
311
        $startName = defined('PODATA_LARAVEL_APP_ROOT_NAMESPACE') ? PODATA_LARAVEL_APP_ROOT_NAMESPACE : 'App';
312
        foreach ($classes as $name) {
313
            if (\Illuminate\Support\Str::startsWith($name, $startName)) {
314
                if (in_array('AlgoWeb\\PODataLaravel\\Models\\MetadataTrait', class_uses($name)) &&
315
                is_subclass_of($name, '\\Illuminate\\Database\\Eloquent\\Model')) {
316
                    $ends[] = $name;
317
                }
318
            }
319
        }
320
        return $ends;
321
    }
322
323
    /**
324
     * @return MetadataGubbinsHolder
325
     */
326
    public function getRelationHolder()
327
    {
328
        return $this->relationHolder;
329
    }
330
331
    public function reset()
332
    {
333
        self::$isBooted = false;
334
        self::$afterExtract = null;
335
        self::$afterUnify = null;
336
        self::$afterVerify = null;
337
        self::$afterImplement = null;
338
    }
339
340
    /**
341
     * Resolve possible reverse relation property names.
342
     *
343
     * @param Model $source
344
     * @param $propName
345
     * @return null|string
346
     * @internal param Model $target
347
     */
348
    public function resolveReverseProperty(Model $source, $propName)
349
    {
350
        assert(is_string($propName), 'Property name must be string');
351
        $entity = $this->getObjectMap()->resolveEntity(get_class($source));
352
        if (null === $entity) {
353
            $msg = 'Source model not defined';
354
            throw new \InvalidArgumentException($msg);
355
        }
356
        $association = $entity->resolveAssociation($propName);
357
        if (null === $association) {
358
            return null;
359
        }
360
        $isFirst = $propName === $association->getFirst()->getRelationName();
361
        if (!$isFirst) {
362
            return $association->getFirst()->getRelationName();
363
        }
364
365
        assert($association instanceof AssociationMonomorphic);
0 ignored issues
show
Bug introduced by
The call to assert() has too few arguments starting with description. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

365
        /** @scrutinizer ignore-call */ 
366
        assert($association instanceof AssociationMonomorphic);

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
366
        return $association->getLast()->getRelationName();
367
    }
368
369
    public function isRunningInArtisan()
370
    {
371
        return App::runningInConsole() && !App::runningUnitTests();
372
    }
373
}
374