Passed
Pull Request — master (#184)
by Alex
06:35
created

MetadataProvider::setAfterUnify()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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