Passed
Pull Request — master (#184)
by Alex
08:09
created

MetadataProvider::resolveReverseProperty()   A

Complexity

Conditions 6
Paths 6

Size

Total Lines 24
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 42

Importance

Changes 4
Bugs 1 Features 1
Metric Value
eloc 15
c 4
b 1
f 1
dl 0
loc 24
ccs 0
cts 0
cp 0
rs 9.2222
cc 6
nc 6
nop 2
crap 42
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\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 Illuminate\Support\Str;
19
use POData\Common\InvalidOperationException;
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
    /**
81
     * @param array $modelNames
82
     * @return Map
83
     * @throws InvalidOperationException
84
     * @throws \Doctrine\DBAL\DBALException
85
     * @throws \ReflectionException
86
     */
87
    private function extract(array $modelNames)
88
    {
89
        /** @var Map $objectMap */
90
        $objectMap = App::make('objectmap');
91
        foreach ($modelNames as $modelName) {
92
            try {
93
                /** @var MetadataTrait $modelInstance */
94
                $modelInstance = App::make($modelName);
95
            } catch (BindingResolutionException $e) {
96
                // if we can't instantiate modelName for whatever reason, move on
97
                continue;
98
            }
99
            $gubbins = $modelInstance->extractGubbins();
100
            $isEmpty = 0 === count($gubbins->getFields());
101
            $inArtisan = $this->isRunningInArtisan();
102
            if (!($isEmpty && $inArtisan)) {
103
                $objectMap->addEntity($gubbins);
104
            }
105
        }
106
        $this->handleCustomFunction($objectMap, self::$afterExtract);
107
        return $objectMap;
108
    }
109
110
    /**
111
     * @param Map $objectMap
112
     * @return Map
113
     * @throws InvalidOperationException
114
     */
115
    private function unify(Map $objectMap)
116
    {
117
        /** @var MetadataGubbinsHolder $mgh */
118
        $mgh = $this->getRelationHolder();
119
        foreach ($objectMap->getEntities() as $entity) {
120
            $mgh->addEntity($entity);
121
        }
122
        $objectMap->setAssociations($mgh->getRelations());
123
124
        $this->handleCustomFunction($objectMap, self::$afterUnify);
125
        return $objectMap;
126
    }
127
128
    private function verify(Map $objectModel)
129
    {
130
        $objectModel->isOK();
131
        $this->handleCustomFunction($objectModel, self::$afterVerify);
132
    }
133
134
    /**
135
     * @param Map $objectModel
136
     * @throws InvalidOperationException
137
     * @throws \ReflectionException
138
     */
139
    private function implement(Map $objectModel)
140
    {
141
        /** @var SimpleMetadataProvider $meta */
142
        $meta = App::make('metadata');
143
        $namespace = $meta->getContainerNamespace().'.';
144
145
        $entities = $objectModel->getEntities();
146
        foreach ($entities as $entity) {
147
            $baseType = null;
148
            $className = $entity->getClassName();
149
            $entityName = $entity->getName();
150
            $pluralName = Str::plural($entityName);
151
            $entityType = $meta->addEntityType(new \ReflectionClass($className), $entityName, null, false, $baseType);
152
            if ($entityType->hasBaseType() !== isset($baseType)) {
153
                throw new InvalidOperationException('');
154
            }
155
            $entity->setOdataResourceType($entityType);
156
            $this->implementProperties($entity);
157
            $meta->addResourceSet($pluralName, $entityType);
158
            $meta->oDataEntityMap[$className] = $meta->oDataEntityMap[$namespace.$entityName];
159
        }
160
        $metaCount = count($meta->oDataEntityMap);
161
        $entityCount = count($entities);
162
        $expected = 2 * $entityCount;
163
        if ($metaCount != $expected) {
164
            $msg = 'Expected ' . $expected . ' items, actually got '.$metaCount;
165
            throw new InvalidOperationException($msg);
166
        }
167
168
        if (0 === count($objectModel->getAssociations())) {
169
            return;
170
        }
171
        $assoc = $objectModel->getAssociations();
172
        foreach ($assoc as $association) {
173
            if (!$association->isOk()) {
174
                throw new InvalidOperationException('');
175
            }
176
            $this->implementAssociationsMonomorphic($objectModel, $association);
177
        }
178
        $this->handleCustomFunction($objectModel, self::$afterImplement);
179
    }
180
181
    /**
182
     * @param Map $objectModel
183
     * @param AssociationMonomorphic $associationUnderHammer
184
     * @throws InvalidOperationException
185
     */
186
    private function implementAssociationsMonomorphic(Map $objectModel, AssociationMonomorphic $associationUnderHammer)
187
    {
188
        /** @var SimpleMetadataProvider $meta */
189
        $meta = App::make('metadata');
190
        $first = $associationUnderHammer->getFirst();
191
        $last = $associationUnderHammer->getLast();
192
        switch ($associationUnderHammer->getAssociationType()) {
193
            case AssociationType::NULL_ONE_TO_NULL_ONE():
194
            case AssociationType::NULL_ONE_TO_ONE():
195
            case AssociationType::ONE_TO_ONE():
196
                $meta->addResourceReferenceSinglePropertyBidirectional(
197
                    $objectModel->getEntities()[$first->getBaseType()]->getOdataResourceType(),
198
                    $objectModel->getEntities()[$last->getBaseType()]->getOdataResourceType(),
199
                    $first->getRelationName(),
200
                    $last->getRelationName()
201
                );
202
                break;
203
            case AssociationType::NULL_ONE_TO_MANY():
204
            case AssociationType::ONE_TO_MANY():
205
                if ($first->getMultiplicity()->getValue() == AssociationStubRelationType::MANY) {
206
                    $oneSide = $last;
207
                    $manySide = $first;
208
                } else {
209
                    $oneSide = $first;
210
                    $manySide = $last;
211
                }
212
                $meta->addResourceReferencePropertyBidirectional(
213
                    $objectModel->getEntities()[$oneSide->getBaseType()]->getOdataResourceType(),
214
                    $objectModel->getEntities()[$manySide->getBaseType()]->getOdataResourceType(),
215
                    $oneSide->getRelationName(),
216
                    $manySide->getRelationName()
217
                );
218
                break;
219
            case AssociationType::MANY_TO_MANY():
220
                $meta->addResourceSetReferencePropertyBidirectional(
221
                    $objectModel->getEntities()[$first->getBaseType()]->getOdataResourceType(),
222
                    $objectModel->getEntities()[$last->getBaseType()]->getOdataResourceType(),
223
                    $first->getRelationName(),
224
                    $last->getRelationName()
225
                );
226
        }
227
    }
228
229
    /**
230
     * @param EntityGubbins $unifiedEntity
231
     * @throws InvalidOperationException
232
     */
233
    private function implementProperties(EntityGubbins $unifiedEntity)
234
    {
235
        /** @var SimpleMetadataProvider $meta */
236
        $meta = App::make('metadata');
237
        $odataEntity = $unifiedEntity->getOdataResourceType();
238
        $keyFields = $unifiedEntity->getKeyFields();
239
        $fields = $unifiedEntity->getFields();
240
        foreach ($keyFields as $keyField) {
241
            $meta->addKeyProperty($odataEntity, $keyField->getName(), $keyField->getEdmFieldType());
242
        }
243
244
        foreach ($fields as $field) {
245
            if (in_array($field, $keyFields)) {
246
                continue;
247
            }
248
            if ($field->getPrimitiveType() == 'blob') {
249
                $odataEntity->setMediaLinkEntry(true);
250
                $streamInfo = new ResourceStreamInfo($field->getName());
251
                $odataEntity->addNamedStream($streamInfo);
252
                continue;
253
            }
254
255
            $default = $field->getDefaultValue();
256
            $isFieldBool = TypeCode::BOOLEAN == $field->getEdmFieldType();
257
            $default = $isFieldBool ? ($default ? 'true' : 'false') : strval($default);
258
259
            $meta->addPrimitiveProperty(
260
                $odataEntity,
261
                $field->getName(),
262
                $field->getEdmFieldType(),
263
                $field->getFieldType()->getValue() == EntityFieldType::PRIMITIVE_BAG()->getValue(),
264
                $default,
265
                $field->getIsNullable()
266
            );
267
        }
268
    }
269
270
    /**
271
     * Bootstrap the application services.  Post-boot.
272
     *
273
     * @param mixed $reset
274
     *
275
     * @return void
276
     * @throws InvalidOperationException
277
     * @throws \ReflectionException
278
     * @throws \Doctrine\DBAL\DBALException
279
     */
280
    public function boot($reset = true)
281
    {
282
        self::$metaNAMESPACE = env('ODataMetaNamespace', 'Data');
283
        // If we aren't migrated, there's no DB tables to pull metadata _from_, so bail out early
284
        try {
285
            if (!Schema::hasTable(config('database.migrations'))) {
286
                return;
287
            }
288
        } catch (\Exception $e) {
289
            return;
290
        }
291
292
        if (false !== self::$isBooted) {
293
            throw new InvalidOperationException('Provider booted twice');
294
        }
295
        $isCaching = true === $this->getIsCaching();
296
        $meta = Cache::get('metadata');
297
        $objectMap = Cache::get('objectmap');
298
        $hasCache = null != $meta && null != $objectMap;
299
300
        if ($isCaching && $hasCache) {
301
            App::instance('metadata', $meta);
302
            App::instance('objectmap', $objectMap);
303
            self::$isBooted = true;
304
            return;
305
        }
306
        $meta = App::make('metadata');
307
        if (false !== $reset) {
308
            $this->reset();
309
        }
310
311
        $modelNames = $this->getCandidateModels();
312
        $objectModel = $this->extract($modelNames);
313
        $objectModel = $this->unify($objectModel);
314
        $this->verify($objectModel);
315
        $this->implement($objectModel);
316
        $this->completedObjectMap = $objectModel;
317
        $key = 'metadata';
318
        $objKey = 'objectmap';
319
        $this->handlePostBoot($isCaching, $hasCache, $key, $meta);
320
        $this->handlePostBoot($isCaching, $hasCache, $objKey, $objectModel);
321
        self::$isBooted = true;
322
    }
323
324
    /**
325
     * Register the application services.  Boot-time only.
326
     *
327
     * @return void
328
     */
329
    public function register()
330
    {
331
        $this->app->singleton('metadata', function () {
332
            return new SimpleMetadataProvider('Data', self::$metaNAMESPACE);
333
        });
334
        $this->app->singleton('objectmap', function () {
335
            return new Map();
336
        });
337
    }
338
339
    /**
340
     * @return array
341
     */
342
    protected function getCandidateModels()
343
    {
344
        $classes = $this->getClassMap();
345
        $ends = [];
346
        $startName = $this->getAppNamespace();
347
        foreach ($classes as $name) {
348
            if (Str::startsWith($name, $startName)) {
349
                if (in_array('AlgoWeb\\PODataLaravel\\Models\\MetadataTrait', class_uses($name))) {
350
                    if (is_subclass_of($name, '\\Illuminate\\Database\\Eloquent\\Model')) {
351
                        $ends[] = $name;
352
                    }
353
                }
354
            }
355
        }
356
        return $ends;
357
    }
358
359
    /**
360
     * @return MetadataGubbinsHolder
361
     */
362
    public function getRelationHolder()
363
    {
364
        return $this->relationHolder;
365
    }
366
367
    public function reset()
368
    {
369
        self::$isBooted = false;
370
        self::$afterExtract = null;
371
        self::$afterUnify = null;
372
        self::$afterVerify = null;
373
        self::$afterImplement = null;
374
    }
375
376
    /**
377
     * Resolve possible reverse relation property names.
378
     *
379
     * @param Model $source
380
     * @param $propName
381
     * @return null|string
382
     * @internal param Model $target
383
     * @throws InvalidOperationException
384
     */
385
    public function resolveReverseProperty(Model $source, $propName)
386
    {
387
        if (!is_string($propName)) {
388
            throw new InvalidOperationException('Property name must be string');
389
        }
390
        $entity = $this->getObjectMap()->resolveEntity(get_class($source));
391
        if (null === $entity) {
392
            $msg = 'Source model not defined';
393
            throw new \InvalidArgumentException($msg);
394
        }
395
        $association = $entity->resolveAssociation($propName);
396
397
        if (null === $association) {
398
            return null;
399
        }
400
        $isFirst = $propName === $association->getFirst()->getRelationName();
401
        if (!$isFirst) {
402
            return $association->getFirst()->getRelationName();
403
        }
404
405
        if (!$association instanceof AssociationMonomorphic) {
406
            throw new InvalidOperationException('');
407
        }
408
        return $association->getLast()->getRelationName();
409
    }
410
411
    public function isRunningInArtisan()
412
    {
413
        return App::runningInConsole() && !App::runningUnitTests();
414
    }
415
416
    /**
417
     * Encapsulate applying self::$after{FOO} calls
418
     *
419
     * @param mixed $parm
420
     * @param callable|null $func
421
     */
422
    private function handleCustomFunction($parm, callable $func = null)
423
    {
424
        if (null != $func) {
425
            $func($parm);
426
        }
427
    }
428
}
429