Passed
Pull Request — master (#154)
by Alex
06:27
created

implementAssociationsMonomorphic()   C

Complexity

Conditions 8
Paths 9

Size

Total Lines 38
Code Lines 33

Duplication

Lines 14
Ratio 36.84 %

Code Coverage

Tests 0
CRAP Score 72

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 14
loc 38
ccs 0
cts 0
cp 0
rs 5.3846
cc 8
eloc 33
nc 9
nop 2
crap 72
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
            $fieldType = $field->getFieldType();
230
231
            if (EntityFieldType::COMPLEX() == $fieldType) {
0 ignored issues
show
Unused Code introduced by
This if statement is empty and can be removed.

This check looks for the bodies of if statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These if bodies can be removed. If you have an empty if but statements in the else branch, consider inverting the condition.

if (rand(1, 6) > 3) {
//print "Check failed";
} else {
    print "Check succeeded";
}

could be turned into

if (rand(1, 6) <= 3) {
    print "Check succeeded";
}

This is much more concise to read.

Loading history...
232
                // todo: Complex property hookup still WIP
0 ignored issues
show
Coding Style Best Practice introduced by
Comments for TODO tasks are often forgotten in the code; it might be better to use a dedicated issue tracker.
Loading history...
233
            } else {
234
                $meta->addPrimitiveProperty(
235
                    $odataEntity,
236
                    $field->getName(),
237
                    $field->getEdmFieldType(),
238
                    $fieldType == EntityFieldType::PRIMITIVE_BAG(),
239
                    $default,
240
                    $field->getIsNullable()
241
                );
242
            }
243
        }
244
    }
245
246
    /**
247
     * Bootstrap the application services.  Post-boot.
248
     *
249
     * @param mixed $reset
250
     *
251
     * @return void
252
     */
253
    public function boot($reset = true)
254
    {
255
        self::$metaNAMESPACE = env('ODataMetaNamespace', 'Data');
256
        // If we aren't migrated, there's no DB tables to pull metadata _from_, so bail out early
257
        try {
258
            if (!Schema::hasTable(config('database.migrations'))) {
259
                return;
260
            }
261
        } catch (\Exception $e) {
262
            return;
263
        }
264
265
        assert(false === self::$isBooted, 'Provider booted twice');
266
        $isCaching = true === $this->getIsCaching();
267
        $meta = Cache::get('metadata');
268
        $objectMap = Cache::get('objectmap');
269
        $hasCache = null != $meta && null != $objectMap;
270
271
        if ($isCaching && $hasCache) {
272
            App::instance('metadata', $meta);
273
            App::instance('objectmap', $objectMap);
274
            return;
275
        }
276
        $meta = App::make('metadata');
277
        if (false !== $reset) {
278
            $this->reset();
279
        }
280
281
        $modelNames = $this->getCandidateModels();
282
        $objectModel = $this->extract($modelNames);
283
        $objectModel = $this->unify($objectModel);
284
        $this->verify($objectModel);
285
        $this->implement($objectModel);
286
        $this->completedObjectMap = $objectModel;
287
        $key = 'metadata';
288
        $objKey = 'objectmap';
289
        $this->handlePostBoot($isCaching, $hasCache, $key, $meta);
290
        $this->handlePostBoot($isCaching, $hasCache, $objKey, $objectModel);
291
        self::$isBooted = true;
292
    }
293
294
    /**
295
     * Register the application services.  Boot-time only.
296
     *
297
     * @return void
298
     */
299
    public function register()
300
    {
301
        $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

301
        $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...
302
            return new SimpleMetadataProvider('Data', self::$metaNAMESPACE);
303
        });
304
        $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

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

370
        /** @scrutinizer ignore-call */ 
371
        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...
371
        return $association->getLast()->getRelationName();
372
    }
373
374
    public function isRunningInArtisan()
375
    {
376
        return App::runningInConsole() && !App::runningUnitTests();
377
    }
378
}
379