Completed
Push — master ( f26d62...e046b5 )
by Christopher
01:51
created

MetadataProvider::verify()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
dl 0
loc 8
rs 9.4285
c 0
b 0
f 0
ccs 0
cts 3
cp 0
cc 2
eloc 5
nc 2
nop 1
crap 6
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)
0 ignored issues
show
Documentation introduced by
The return type could not be reliably inferred; please add a @return annotation.

Our type inference engine in quite powerful, but sometimes the code does not provide enough clues to go by. In these cases we request you to add a @return annotation as described here.

Loading history...
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));
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());
222
                $odataEntity->addNamedStream($streamInfo);
223
                continue;
224
            }
225
226
            $default = $field->getDefaultValue();
227
            $default = (!is_bool($default)) ? strval($default) : $default ? 'true' : 'false';
228
229
            $meta->addPrimitiveProperty(
230
                $odataEntity,
231
                $field->getName(),
232
                $field->getEdmFieldType(),
233
                $field->getFieldType() == EntityFieldType::PRIMITIVE_BAG(),
234
                $default,
235
                $field->getIsNullable()
236
            );
237
        }
238
    }
239
240
    /**
241
     * Bootstrap the application services.  Post-boot.
242
     *
243
     * @param mixed $reset
244
     *
245
     * @return void
246
     */
247
    public function boot($reset = true)
248
    {
249
        self::$metaNAMESPACE = env('ODataMetaNamespace', 'Data');
250
        // If we aren't migrated, there's no DB tables to pull metadata _from_, so bail out early
251
        try {
252
            if (!Schema::hasTable(config('database.migrations'))) {
253
                return;
254
            }
255
        } catch (\Exception $e) {
256
            return;
257
        }
258
259
        assert(false === self::$isBooted, 'Provider booted twice');
260
        $isCaching = true === $this->getIsCaching();
261
        $meta = Cache::get('metadata');
262
        $objectMap = Cache::get('objectmap');
263
        $hasCache = null != $meta && null != $objectMap;
264
265
        if ($isCaching && $hasCache) {
266
            App::instance('metadata', $meta);
267
            App::instance('objectmap', $objectMap);
268
            return;
269
        }
270
        $meta = App::make('metadata');
271
        if (false !== $reset) {
272
            $this->reset();
273
        }
274
275
        $modelNames = $this->getCandidateModels();
276
        $objectModel = $this->extract($modelNames);
277
        $objectModel = $this->unify($objectModel);
278
        $this->verify($objectModel);
279
        $this->implement($objectModel);
280
        $this->completedObjectMap = $objectModel;
281
        $key = 'metadata';
282
        $objKey = 'objectmap';
283
        $this->handlePostBoot($isCaching, $hasCache, $key, $meta);
284
        $this->handlePostBoot($isCaching, $hasCache, $objKey, $objectModel);
285
        self::$isBooted = true;
286
    }
287
288
    /**
289
     * Register the application services.  Boot-time only.
290
     *
291
     * @return void
292
     */
293
    public function register()
294
    {
295
        $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...
296
            return new SimpleMetadataProvider('Data', self::$metaNAMESPACE);
297
        });
298
        $this->app->singleton('objectmap', function ($app) {
299
            return new Map();
300
        });
301
    }
302
303
    /**
304
     * @return array
305
     */
306
    protected function getCandidateModels()
307
    {
308
        $classes = $this->getClassMap();
309
        $ends = [];
310
        $startName = defined('PODATA_LARAVEL_APP_ROOT_NAMESPACE') ? PODATA_LARAVEL_APP_ROOT_NAMESPACE : 'App';
311
        foreach ($classes as $name) {
312
            if (\Illuminate\Support\Str::startsWith($name, $startName)) {
313
                if (in_array('AlgoWeb\\PODataLaravel\\Models\\MetadataTrait', class_uses($name)) &&
314
                is_subclass_of($name, '\\Illuminate\\Database\\Eloquent\\Model')) {
315
                    $ends[] = $name;
316
                }
317
            }
318
        }
319
        return $ends;
320
    }
321
322
    /**
323
     * @return MetadataGubbinsHolder
324
     */
325
    public function getRelationHolder()
326
    {
327
        return $this->relationHolder;
328
    }
329
330
    public function reset()
331
    {
332
        self::$isBooted = false;
333
        self::$afterExtract = null;
334
        self::$afterUnify = null;
335
        self::$afterVerify = null;
336
        self::$afterImplement = null;
337
    }
338
339
    /**
340
     * Resolve possible reverse relation property names.
341
     *
342
     * @param Model $source
343
     * @param $propName
344
     * @return null|string
345
     * @internal param Model $target
346
     */
347
    public function resolveReverseProperty(Model $source, $propName)
348
    {
349
        assert(is_string($propName), 'Property name must be string');
350
        $entity = $this->getObjectMap()->resolveEntity(get_class($source));
351
        if (null === $entity) {
352
            $msg = 'Source model not defined';
353
            throw new \InvalidArgumentException($msg);
354
        }
355
        $association = $entity->resolveAssociation($propName);
356
        if (null === $association) {
357
            return null;
358
        }
359
        $isFirst = $propName === $association->getFirst()->getRelationName();
360
        if (!$isFirst) {
361
            return $association->getFirst()->getRelationName();
362
        }
363
364
        assert($association instanceof AssociationMonomorphic);
365
        return $association->getLast()->getRelationName();
366
    }
367
368
    public function isRunningInArtisan()
369
    {
370
        return App::runningInConsole() && !App::runningUnitTests();
371
    }
372
}
373