Completed
Push — master ( 260504...c2ab20 )
by Alex
18s queued 11s
created

MetadataProvider::extract()   A

Complexity

Conditions 5
Paths 4

Size

Total Lines 21
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 30

Importance

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