MetadataProvider   B
last analyzed

Complexity

Total Complexity 45

Size/Duplication

Total Lines 330
Duplicated Lines 0 %

Test Coverage

Coverage 0%

Importance

Changes 14
Bugs 0 Features 0
Metric Value
wmc 45
eloc 158
c 14
b 0
f 0
dl 0
loc 330
rs 8.8
ccs 0
cts 64
cp 0

13 Methods

Rating   Name   Duplication   Size   Complexity  
A unify() 0 11 2
B implementAssociationsMonomorphic() 0 49 11
A getRelationHolder() 0 3 1
A getCandidateModels() 0 3 1
A verify() 0 4 1
A __construct() 0 4 1
A register() 0 7 1
B implement() 0 44 7
B implementProperties() 0 31 6
A extract() 0 21 5
A isRunningInArtisan() 0 3 2
A getObjectMap() 0 3 1
B boot() 0 40 6

How to fix   Complexity   

Complex Class

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
declare(strict_types=1);
4
5
namespace AlgoWeb\PODataLaravel\Providers;
6
7
use AlgoWeb\PODataLaravel\Models\ClassReflectionHelper;
8
use AlgoWeb\PODataLaravel\Models\IMetadataRelationshipContainer;
9
use AlgoWeb\PODataLaravel\Models\MetadataRelationshipContainer;
10
use AlgoWeb\PODataLaravel\Models\MetadataTrait;
11
use AlgoWeb\PODataLaravel\Models\ObjectMap\Entities\Associations\AssociationMonomorphic;
12
use AlgoWeb\PODataLaravel\Models\ObjectMap\Entities\Associations\AssociationStubRelationType;
13
use AlgoWeb\PODataLaravel\Models\ObjectMap\Entities\Associations\AssociationType;
14
use AlgoWeb\PODataLaravel\Models\ObjectMap\Entities\EntityField;
15
use AlgoWeb\PODataLaravel\Models\ObjectMap\Entities\EntityFieldType;
16
use AlgoWeb\PODataLaravel\Models\ObjectMap\Entities\EntityGubbins;
17
use AlgoWeb\PODataLaravel\Models\ObjectMap\Map;
18
use Cruxinator\ClassFinder\ClassFinder;
19
use Illuminate\Contracts\Container\BindingResolutionException;
20 1
use Illuminate\Database\Eloquent\Model;
21
use Illuminate\Support\Facades\App;
22 1
use Illuminate\Support\Facades\Cache;
23
use Illuminate\Support\Facades\Schema as Schema;
24
use Illuminate\Support\Str;
25 1
use POData\Common\InvalidOperationException;
26
use POData\Providers\Metadata\ResourceEntityType;
27
use POData\Providers\Metadata\ResourceStreamInfo;
28 1
use POData\Providers\Metadata\SimpleMetadataProvider;
29 1
use POData\Providers\Metadata\Type\TypeCode;
30
31
class MetadataProvider extends MetadataBaseProvider
32
{
33
    use MetadataProviderStepTrait;
34
35
    /** @var array<array>  */
36
    protected $multConstraints      = ['0..1' => ['1'], '1' => ['0..1', '*'], '*' => ['1', '*']];
37
    /** @var string  */
38
    protected static $metaNAMESPACE = 'Data';
39
    /** @var bool */
40
    protected static $isBooted      = false;
41
    const POLYMORPHIC               = 'polyMorphicPlaceholder';
42
    const POLYMORPHIC_PLURAL        = 'polyMorphicPlaceholders';
43
44
    /**
45
     * @var Map The completed object map set at post Implement;
46
     */
47
    private $completedObjectMap;
48
49
    /**
50
     * @return \AlgoWeb\PODataLaravel\Models\ObjectMap\Map
51
     */
52
    public function getObjectMap()
53
    {
54
        return $this->completedObjectMap;
55
    }
56
57
    /** @var IMetadataRelationshipContainer|null */
58
    protected $relationHolder;
59
60
    public function __construct($app)
61
    {
62
        parent::__construct($app);
63
        self::$isBooted = false;
64
    }
65
66
    /**
67
     * @param  string[]                     $modelNames
68
     * @throws InvalidOperationException
69
     * @throws \Doctrine\DBAL\DBALException
70
     * @throws \ReflectionException
71
     * @return Map
72
     */
73
    private function extract(array $modelNames): Map
74
    {
75
        /** @var Map $objectMap */
76
        $objectMap = App::make('objectmap');
77
        foreach ($modelNames as $modelName) {
78
            try {
79
                /** @var MetadataTrait $modelInstance */
80
                $modelInstance = App::make($modelName);
81
            } catch (BindingResolutionException $e) {
82
                // if we can't instantiate modelName for whatever reason, move on
83
                continue;
84
            }
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
        $this->handleCustomFunction($objectMap, self::$afterExtract);
93
        return $objectMap;
94
    }
95
96
    /**
97
     * @param  Map                       $objectMap
98
     * @throws InvalidOperationException
99
     * @return Map
100
     */
101
    private function unify(Map $objectMap)
102
    {
103
        /** @var IMetadataRelationshipContainer $mgh */
104
        $mgh = $this->getRelationHolder();
105
        foreach ($objectMap->getEntities() as $entity) {
106
            $mgh->addEntity($entity);
107
        }
108
        $objectMap->setAssociations($mgh->getRelations());
109
110
        $this->handleCustomFunction($objectMap, self::$afterUnify);
111
        return $objectMap;
112
    }
113
114
    /**
115
     * @param Map $objectModel
116
     *
117
     * @return void
118
     */
119
    private function verify(Map $objectModel): void
120
    {
121
        $objectModel->isOK();
122
        $this->handleCustomFunction($objectModel, self::$afterVerify);
123
    }
124
125
    /**
126
     * @param  Map                       $objectModel
127
     * @throws InvalidOperationException
128
     * @throws \ReflectionException
129
     */
130
    private function implement(Map $objectModel): void
131
    {
132
        /** @var SimpleMetadataProvider $meta */
133
        $meta      = App::make('metadata');
134
        $namespace = $meta->getContainerNamespace() . '.';
135
136
        $entities = $objectModel->getEntities();
137
        foreach ($entities as $entity) {
138
            /** @var class-string $className */
139
            $className  = $entity->getClassName();
140
            /** @var string $entityName */
141
            $entityName = $entity->getName();
142
            $pluralName = Str::plural($entityName);
143
            $entityType = $meta->addEntityType(new \ReflectionClass($className), $entityName, null, false, null);
144
            if ($entityType->hasBaseType() !== false) {
145
                throw new InvalidOperationException('');
146
            }
147
            $entity->setOdataResourceType($entityType);
148
            $this->implementProperties($entity);
149
            $meta->addResourceSet($pluralName, $entityType);
150
            $meta->oDataEntityMap[$className] = $meta->oDataEntityMap[$namespace . $entityName];
151
        }
152
        $metaCount   = count($meta->oDataEntityMap);
153
        $entityCount = count($entities);
154
        $expected    = 2 * $entityCount;
155
        if ($metaCount != $expected) {
156
            $msg = 'Expected ' . $expected . ' items, actually got ' . $metaCount;
157
            throw new InvalidOperationException($msg);
158
        }
159
160
        if (0 === count($objectModel->getAssociations())) {
161
            return;
162
        }
163
        $assoc = $objectModel->getAssociations();
164
        $assoc = array_filter($assoc, function ($value) {
165
            return $value instanceof AssociationMonomorphic;
166
        });
167
        foreach ($assoc as $association) {
168
            if (!$association->isOk()) {
169
                throw new InvalidOperationException('');
170
            }
171
            $this->implementAssociationsMonomorphic($objectModel, $association);
172
        }
173
        $this->handleCustomFunction($objectModel, self::$afterImplement);
174
    }
175
176
    /**
177
     * @param  Map                       $objectModel
178
     * @param  AssociationMonomorphic    $associationUnderHammer
179
     * @throws InvalidOperationException
180
     * @throws \ReflectionException
181
     */
182
    private function implementAssociationsMonomorphic(
183
        Map $objectModel,
184
        AssociationMonomorphic $associationUnderHammer
185
    ): void {
186
        /** @var SimpleMetadataProvider $meta */
187
        $meta           = App::make('metadata');
188
        $first          = $associationUnderHammer->getFirst();
189
        $last           = $associationUnderHammer->getLast();
190
        $assocType      = $associationUnderHammer->getAssociationType();
191
        $firstIsMany    = (AssociationType::NULL_ONE_TO_MANY() == $assocType || AssociationType::ONE_TO_MANY() == $assocType) &&
0 ignored issues
show
introduced by
Consider adding parentheses for clarity. Current Interpretation: $firstIsMany = (AlgoWeb\...ubRelationType::MANY()), Probably Intended Meaning: $firstIsMany = AlgoWeb\P...ubRelationType::MANY())
Loading history...
192
                          ($first->getMultiplicity() == AssociationStubRelationType::MANY());
193
194
        $firstSide      = $firstIsMany ? $last : $first;
195
        $lastSide       = $firstIsMany ? $first : $last;
196
197
        /** @var ResourceEntityType $firstType */
198
        $firstType      = $objectModel->getEntities()[$firstSide->getBaseType()]->getOdataResourceType();
199
        /** @var ResourceEntityType $secondType */
200
        $secondType     = $objectModel->getEntities()[$lastSide->getBaseType()]->getOdataResourceType();
201
202
        $firstName      = $firstSide->getRelationName();
203
        $lastName       = $lastSide->getRelationName();
204
205
        switch ($assocType) {
206
            case AssociationType::NULL_ONE_TO_NULL_ONE():
207
            case AssociationType::NULL_ONE_TO_ONE():
208
            case AssociationType::ONE_TO_ONE():
209
                $meta->addResourceReferenceSinglePropertyBidirectional(
210
                    $firstType,
211
                    $secondType,
212
                    $firstName,
213
                    $lastName
214
                );
215
                break;
216
            case AssociationType::NULL_ONE_TO_MANY():
217
            case AssociationType::ONE_TO_MANY():
218
                $meta->addResourceReferencePropertyBidirectional(
219
                    $firstType,
220
                    $secondType,
221
                    $firstName,
222
                    $lastName
223
                );
224
                break;
225
            case AssociationType::MANY_TO_MANY():
226
                $meta->addResourceSetReferencePropertyBidirectional(
227
                    $firstType,
228
                    $secondType,
229
                    $firstName,
230
                    $lastName
231
                );
232
        }
233
    }
234
235
    /**
236
     * @param  EntityGubbins             $unifiedEntity
237
     * @throws InvalidOperationException
238
     * @throws \ReflectionException
239
     */
240
    private function implementProperties(EntityGubbins $unifiedEntity): void
241
    {
242
        /** @var SimpleMetadataProvider $meta */
243
        $meta        = App::make('metadata');
244
        $odataEntity = $unifiedEntity->getOdataResourceType();
245
        $keyFields   = $unifiedEntity->getKeyFields();
246
        /** @var EntityField[] $fields */
247
        $fields = array_diff_key($unifiedEntity->getFields(), $keyFields);
248
        foreach ($keyFields as $keyField) {
249
            $meta->addKeyProperty($odataEntity, $keyField->getName(), $keyField->getEdmFieldType());
250
        }
251
252
        foreach ($fields as $field) {
253
            if ($field->getPrimitiveType() == 'blob') {
254
                $odataEntity->setMediaLinkEntry(true);
255
                $streamInfo = new ResourceStreamInfo($field->getName());
256
                $odataEntity->addNamedStream($streamInfo);
257
                continue;
258
            }
259
260
            $default     = $field->getDefaultValue();
261
            $isFieldBool = TypeCode::BOOLEAN() == $field->getEdmFieldType();
262
            $default     = $isFieldBool ? ($default ? 'true' : 'false') : strval($default);
263
264
            $meta->addPrimitiveProperty(
265
                $odataEntity,
266
                $field->getName(),
267
                $field->getEdmFieldType(),
268
                $field->getFieldType() == EntityFieldType::PRIMITIVE_BAG(),
269
                $default,
270
                $field->getIsNullable()
271
            );
272
        }
273
    }
274
275
    /**
276
     * Bootstrap the application services.  Post-boot.
277
     *
278
     * @throws InvalidOperationException
279
     * @throws \ReflectionException
280
     * @throws \Doctrine\DBAL\DBALException
281
     * @throws \Exception
282
     * @return void
283
     */
284
    public function boot()
285
    {
286
        App::forgetInstance('metadata');
287
        App::forgetInstance('objectmap');
288
        $this->relationHolder = new MetadataRelationshipContainer();
289
290
        self::$metaNAMESPACE = env('ODataMetaNamespace', 'Data');
291
        // If we aren't migrated, there's no DB tables to pull metadata _from_, so bail out early
292
        try {
293
            if (!Schema::hasTable(strval(config('database.migrations')))) {
294
                return;
295
            }
296
        } catch (\Exception $e) {
297
            return;
298
        }
299
300
        $isCaching = true === $this->getIsCaching();
301
        $meta      = Cache::get('metadata');
302
        $objectMap = Cache::get('objectmap');
303
        $hasCache  = null != $meta && null != $objectMap;
304
305
        if ($isCaching && $hasCache) {
306
            App::instance('metadata', $meta);
307
            App::instance('objectmap', $objectMap);
308
            self::$isBooted = true;
309
            return;
310
        }
311
        $meta = App::make('metadata');
312
313
        $modelNames  = $this->getCandidateModels();
314
        $objectModel = $this->extract($modelNames);
315
        $objectModel = $this->unify($objectModel);
316
        $this->verify($objectModel);
317
        $this->implement($objectModel);
318
        $this->completedObjectMap = $objectModel;
319
        $key                      = 'metadata';
320
        $objKey                   = 'objectmap';
321
        $this->handlePostBoot($isCaching, $hasCache, $key, $meta);
322
        $this->handlePostBoot($isCaching, $hasCache, $objKey, $objectModel);
323
        self::$isBooted = true;
324
    }
325
326
    /**
327
     * Register the application services.  Boot-time only.
328
     *
329
     * @return void
330
     */
331
    public function register()
332
    {
333
        $this->app->/* @scrutinizer ignore-call */singleton('metadata', function () {
334
            return new SimpleMetadataProvider('Data', self::$metaNAMESPACE);
335
        });
336
        $this->app->/* @scrutinizer ignore-call */singleton('objectmap', function () {
337
            return new Map();
338
        });
339
    }
340
341
    /**
342
     * @throws \Exception
343
     * @return string[]
344
     */
345
    protected function getCandidateModels(): array
346
    {
347
        return ClassReflectionHelper::getCandidateModels();
348
    }
349
350
    /**
351
     * @return IMetadataRelationshipContainer|null
352
     */
353
    public function getRelationHolder(): ?IMetadataRelationshipContainer
354
    {
355
        return $this->relationHolder;
356
    }
357
358
    public function isRunningInArtisan(): bool
359
    {
360
        return App::runningInConsole() && !App::runningUnitTests();
361
    }
362
}
363