Completed
Pull Request — master (#143)
by Alex
01:54
created

MetadataProvider::getObjectMap()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 4
ccs 0
cts 3
cp 0
rs 10
cc 1
eloc 2
nc 1
nop 0
crap 2
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 = new Map();
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 (null === $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
            $meta->addPrimitiveProperty(
226
                $odataEntity,
227
                $field->getName(),
228
                $field->getEdmFieldType(),
229
                $field->getFieldType() == EntityFieldType::PRIMITIVE_BAG(),
230
                strval($field->getDefaultValue()),
231
                $field->getIsNullable()
232
            );
233
        }
234
    }
235
236
    /**
237
     * Bootstrap the application services.  Post-boot.
238
     *
239
     * @param mixed $reset
240
     *
241
     * @return void
242
     */
243
    public function boot($reset = true)
244
    {
245
        self::$metaNAMESPACE = env('ODataMetaNamespace', 'Data');
246
        // If we aren't migrated, there's no DB tables to pull metadata _from_, so bail out early
247
        try {
248
            if (!Schema::hasTable(config('database.migrations'))) {
249
                return;
250
            }
251
        } catch (\Exception $e) {
252
            return;
253
        }
254
255
        assert(false === self::$isBooted, 'Provider booted twice');
256
        $isCaching = true === $this->getIsCaching();
257
        $meta = Cache::get('metadata');
258
        $objectMap = Cache::get('objectmap');
259
        $hasCache = null != $meta && null != $objectMap;
260
261
        if ($isCaching && $hasCache) {
262
            App::instance('metadata', $meta);
263
            App::instance('objectmap', $objectMap);
1 ignored issue
show
Bug introduced by
The method instance() does not exist on Illuminate\Support\Facades\App. Did you maybe mean clearResolvedInstance()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
264
            return;
265
        }
266
        $meta = App::make('metadata');
267
        if (false !== $reset) {
268
            $this->reset();
269
        }
270
271
        $modelNames = $this->getCandidateModels();
272
        $objectModel = $this->extract($modelNames);
273
        $objectModel = $this->unify($objectModel);
274
        $this->verify($objectModel);
275
        $this->implement($objectModel);
276
        $this->completedObjectMap = $objectModel;
277
        $key = 'metadata';
278
        $objKey = 'objectmap';
279
        $this->handlePostBoot($isCaching, $hasCache, $key, $meta);
280
        $this->handlePostBoot($isCaching, $hasCache, $objKey, $objectModel);
281
        self::$isBooted = true;
282
    }
283
284
    /**
285
     * Register the application services.  Boot-time only.
286
     *
287
     * @return void
288
     */
289
    public function register()
290
    {
291
        $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...
292
            return new SimpleMetadataProvider('Data', self::$metaNAMESPACE);
293
        });
294
        $this->app->singleton('objectmap', function ($app) {
1 ignored issue
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...
295
            return new Map();
296
        });
297
    }
298
299
    /**
300
     * @return array
301
     */
302
    protected function getCandidateModels()
303
    {
304
        $classes = $this->getClassMap();
305
        $ends = [];
306
        $startName = defined('PODATA_LARAVEL_APP_ROOT_NAMESPACE') ? PODATA_LARAVEL_APP_ROOT_NAMESPACE : 'App';
307
        foreach ($classes as $name) {
308
            if (\Illuminate\Support\Str::startsWith($name, $startName)) {
309
                if (in_array('AlgoWeb\\PODataLaravel\\Models\\MetadataTrait', class_uses($name)) &&
310
                is_subclass_of($name, '\\Illuminate\\Database\\Eloquent\\Model')) {
311
                    $ends[] = $name;
312
                }
313
            }
314
        }
315
        return $ends;
316
    }
317
318
    /**
319
     * @return MetadataGubbinsHolder
320
     */
321
    public function getRelationHolder()
322
    {
323
        return $this->relationHolder;
324
    }
325
326
    public function reset()
327
    {
328
        self::$isBooted = false;
329
        self::$afterExtract = null;
330
        self::$afterUnify = null;
331
        self::$afterVerify = null;
332
        self::$afterImplement = null;
333
    }
334
335
    /**
336
     * Resolve possible reverse relation property names.
337
     *
338
     * @param Model $source
339
     * @param $propName
340
     * @return null|string
341
     * @internal param Model $target
342
     */
343
    public function resolveReverseProperty(Model $source, $propName)
344
    {
345
        assert(is_string($propName), 'Property name must be string');
346
        $entity = $this->getObjectMap()->resolveEntity(get_class($source));
347
        if (null === $entity) {
348
            $msg = 'Source model not defined';
349
            throw new \InvalidArgumentException($msg);
350
        }
351
        $association = $entity->resolveAssociation($propName);
352
        if (null === $association) {
353
            return null;
354
        }
355
        $isFirst = $propName === $association->getFirst()->getRelationName();
356
        if (!$isFirst) {
357
            return $association->getFirst()->getRelationName();
358
        }
359
360
        assert($association instanceof AssociationMonomorphic);
361
        return $association->getLast()->getRelationName();
362
    }
363
364
    public function isRunningInArtisan()
365
    {
366
        return App::runningInConsole() && !App::runningUnitTests();
367
    }
368
}
369