Passed
Pull Request — master (#172)
by Alex
03:50
created

MetadataProvider::getCandidateModels()   A

Complexity

Conditions 5
Paths 4

Size

Total Lines 14
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 30

Importance

Changes 0
Metric Value
eloc 9
dl 0
loc 14
ccs 0
cts 0
cp 0
rs 9.6111
c 0
b 0
f 0
cc 5
nc 4
nop 0
crap 30
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 Illuminate\Support\Str;
19
use POData\Common\InvalidOperationException;
20 1
use POData\Providers\Metadata\ResourceEntityType;
21
use POData\Providers\Metadata\ResourceSet;
22 1
use POData\Providers\Metadata\ResourceStreamInfo;
23
use POData\Providers\Metadata\SimpleMetadataProvider;
24
use POData\Providers\Metadata\Type\TypeCode;
25 1
26
class MetadataProvider extends MetadataBaseProvider
27
{
28 1
    protected $multConstraints = ['0..1' => ['1'], '1' => ['0..1', '*'], '*' => ['1', '*']];
29 1
    protected static $metaNAMESPACE = 'Data';
30
    protected static $isBooted = false;
31
    const POLYMORPHIC = 'polyMorphicPlaceholder';
32
    const POLYMORPHIC_PLURAL = 'polyMorphicPlaceholders';
33
34
    /**
35
     * @var Map The completed object map set at post Implement;
36
     */
37
    private $completedObjectMap;
38
39
    /**
40
     * @return \AlgoWeb\PODataLaravel\Models\ObjectMap\Map
41
     */
42
    public function getObjectMap()
43
    {
44
        return $this->completedObjectMap;
45
    }
46
47
    protected static $afterExtract;
48
    protected static $afterUnify;
49
    protected static $afterVerify;
50
    protected static $afterImplement;
51
52
    public static function setAfterExtract(callable $method)
53
    {
54
        self::$afterExtract = $method;
55
    }
56
57
    public static function setAfterUnify(callable $method)
58
    {
59
        self::$afterUnify = $method;
60
    }
61
62
    public static function setAfterVerify(callable $method)
63
    {
64
        self::$afterVerify = $method;
65
    }
66
67
    public static function setAfterImplement(callable $method)
68
    {
69
        self::$afterImplement = $method;
70
    }
71
72
73
    protected $relationHolder;
74
75
    public function __construct($app)
76
    {
77
        parent::__construct($app);
78
        $this->relationHolder = new MetadataGubbinsHolder();
79
        self::$isBooted = false;
80
    }
81
82
    private function extract(array $modelNames)
83
    {
84
        $objectMap = App::make('objectmap');
85
        foreach ($modelNames as $modelName) {
86
            try {
87
                $modelInstance = App::make($modelName);
88
            } catch (BindingResolutionException $e) {
89
                // if we can't instantiate modelName for whatever reason, move on
90
                continue;
91
            }
92
            $gubbins = $modelInstance->extractGubbins();
93
            $isEmpty = 0 === count($gubbins->getFields());
94
            $inArtisan = $this->isRunningInArtisan();
95
            if (!($isEmpty && $inArtisan)) {
96
                $objectMap->addEntity($gubbins);
97
            }
98
        }
99
        if (null != self::$afterExtract) {
100
            $func = self::$afterExtract;
101
            $func($objectMap);
102
        }
103
        return $objectMap;
104
    }
105
106
    private function unify(Map $objectMap)
107
    {
108
        $mgh = $this->getRelationHolder();
109
        foreach ($objectMap->getEntities() as $entity) {
110
            $mgh->addEntity($entity);
111
        }
112
        $objectMap->setAssociations($mgh->getRelations());
113
        if (null != self::$afterUnify) {
114
            $func = self::$afterUnify;
115
            $func($objectMap);
116
        }
117
        return $objectMap;
118
    }
119
120
    private function verify(Map $objectModel)
121
    {
122
        $objectModel->isOK();
123
        if (null != self::$afterVerify) {
124
            $func = self::$afterVerify;
125
            $func($objectModel);
126
        }
127
    }
128
129
    private function implement(Map $objectModel)
130
    {
131
        $meta = App::make('metadata');
132
        $namespace = $meta->getContainerNamespace().'.';
133
134
        $entities = $objectModel->getEntities();
135
        foreach ($entities as $entity) {
136
            $baseType = null;
137
            $className = $entity->getClassName();
138
            $entityName = $entity->getName();
139
            $pluralName = Str::plural($entityName);
140
            $entityType = $meta->addEntityType(new \ReflectionClass($className), $entityName, null, false, $baseType);
141
            if ($entityType->hasBaseType() !== isset($baseType)) {
142
                throw new InvalidOperationException('');
143
            }
144
            $entity->setOdataResourceType($entityType);
145
            $this->implementProperties($entity);
146
            $meta->addResourceSet($pluralName, $entityType);
147
            $meta->oDataEntityMap[$className] = $meta->oDataEntityMap[$namespace.$entityName];
148
        }
149
        $metaCount = count($meta->oDataEntityMap);
150
        $entityCount = count($entities);
151
        $expected = 2 * $entityCount;
152
        if ($metaCount != $expected) {
153
            $msg = 'Expected ' . $expected . ' items, actually got '.$metaCount;
154
            throw new InvalidOperationException($msg);
155
        }
156
157
        if (0 === count($objectModel->getAssociations())) {
158
            return;
159
        }
160
        $assoc = $objectModel->getAssociations();
161
        foreach ($assoc as $association) {
162
            if (!$association->isOk()) {
163
                throw new InvalidOperationException('');
164
            }
165
            $this->implementAssociationsMonomorphic($objectModel, $association);
166
        }
167
        if (null != self::$afterImplement) {
168
            $func = self::$afterImplement;
169
            $func($objectModel);
170
        }
171
    }
172
173
    private function implementAssociationsMonomorphic(Map $objectModel, AssociationMonomorphic $associationUnderHammer)
174
    {
175
        $meta = App::make('metadata');
176
        $first = $associationUnderHammer->getFirst();
177
        $last = $associationUnderHammer->getLast();
178
        switch ($associationUnderHammer->getAssociationType()) {
179
            case AssociationType::NULL_ONE_TO_NULL_ONE():
180
            case AssociationType::NULL_ONE_TO_ONE():
181
            case AssociationType::ONE_TO_ONE():
182
                $meta->addResourceReferenceSinglePropertyBidirectional(
183
                    $objectModel->getEntities()[$first->getBaseType()]->getOdataResourceType(),
184
                    $objectModel->getEntities()[$last->getBaseType()]->getOdataResourceType(),
185
                    $first->getRelationName(),
186
                    $last->getRelationName()
187
                );
188
                break;
189
            case AssociationType::NULL_ONE_TO_MANY():
190
            case AssociationType::ONE_TO_MANY():
191
                if ($first->getMultiplicity()->getValue() == AssociationStubRelationType::MANY) {
192
                    $oneSide = $last;
193
                    $manySide = $first;
194
                } else {
195
                    $oneSide = $first;
196
                    $manySide = $last;
197
                }
198
                $meta->addResourceReferencePropertyBidirectional(
199
                    $objectModel->getEntities()[$oneSide->getBaseType()]->getOdataResourceType(),
200
                    $objectModel->getEntities()[$manySide->getBaseType()]->getOdataResourceType(),
201
                    $oneSide->getRelationName(),
202
                    $manySide->getRelationName()
203
                );
204
                break;
205
            case AssociationType::MANY_TO_MANY():
206
                $meta->addResourceSetReferencePropertyBidirectional(
207
                    $objectModel->getEntities()[$first->getBaseType()]->getOdataResourceType(),
208
                    $objectModel->getEntities()[$last->getBaseType()]->getOdataResourceType(),
209
                    $first->getRelationName(),
210
                    $last->getRelationName()
211
                );
212
        }
213
    }
214
215
    private function implementProperties(EntityGubbins $unifiedEntity)
216
    {
217
        $meta = App::make('metadata');
218
        $odataEntity = $unifiedEntity->getOdataResourceType();
219
        $keyFields = $unifiedEntity->getKeyFields();
220
        $fields = $unifiedEntity->getFields();
221
        foreach ($keyFields as $keyField) {
222
            $meta->addKeyProperty($odataEntity, $keyField->getName(), $keyField->getEdmFieldType());
223
        }
224
225
        foreach ($fields as $field) {
226
            if (in_array($field, $keyFields)) {
227
                continue;
228
            }
229
            if ($field->getPrimitiveType() == 'blob') {
230
                $odataEntity->setMediaLinkEntry(true);
231
                $streamInfo = new ResourceStreamInfo($field->getName());
232
                if (!$odataEntity->isMediaLinkEntry()) {
233
                    throw new InvalidOperationException('');
234
                }
235
236
                $odataEntity->addNamedStream($streamInfo);
237
                continue;
238
            }
239
240
            $default = $field->getDefaultValue();
241
            $isFieldBool = TypeCode::BOOLEAN == $field->getEdmFieldType();
242
            $default = $isFieldBool ? ($default ? 'true' : 'false') : strval($default);
243
244
            $meta->addPrimitiveProperty(
245
                $odataEntity,
246
                $field->getName(),
247
                $field->getEdmFieldType(),
248
                $field->getFieldType() == EntityFieldType::PRIMITIVE_BAG(),
249
                $default,
250
                $field->getIsNullable()
251
            );
252
        }
253
    }
254
255
    /**
256
     * Bootstrap the application services.  Post-boot.
257
     *
258
     * @param mixed $reset
259
     *
260
     * @return void
261
     */
262
    public function boot($reset = true)
263
    {
264
        self::$metaNAMESPACE = env('ODataMetaNamespace', 'Data');
265
        // If we aren't migrated, there's no DB tables to pull metadata _from_, so bail out early
266
        try {
267
            if (!Schema::hasTable(config('database.migrations'))) {
268
                return;
269
            }
270
        } catch (\Exception $e) {
271
            return;
272
        }
273
274
        if (false !== self::$isBooted) {
275
            throw new InvalidOperationException('Provider booted twice');
276
        }
277
        $isCaching = true === $this->getIsCaching();
278
        $meta = Cache::get('metadata');
279
        $objectMap = Cache::get('objectmap');
280
        $hasCache = null != $meta && null != $objectMap;
281
282
        if ($isCaching && $hasCache) {
283
            App::instance('metadata', $meta);
284
            App::instance('objectmap', $objectMap);
285
            return;
286
        }
287
        $meta = App::make('metadata');
288
        if (false !== $reset) {
289
            $this->reset();
290
        }
291
292
        $modelNames = $this->getCandidateModels();
293
        $objectModel = $this->extract($modelNames);
294
        $objectModel = $this->unify($objectModel);
295
        $this->verify($objectModel);
296
        $this->implement($objectModel);
297
        $this->completedObjectMap = $objectModel;
298
        $key = 'metadata';
299
        $objKey = 'objectmap';
300
        $this->handlePostBoot($isCaching, $hasCache, $key, $meta);
301
        $this->handlePostBoot($isCaching, $hasCache, $objKey, $objectModel);
302
        self::$isBooted = true;
303
    }
304
305
    /**
306
     * Register the application services.  Boot-time only.
307
     *
308
     * @return void
309
     */
310
    public function register()
311
    {
312
        $this->app->singleton('metadata', function ($app) {
0 ignored issues
show
Unused Code introduced by
The parameter $app is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

312
        $this->app->singleton('metadata', function (/** @scrutinizer ignore-unused */ $app) {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
313
            return new SimpleMetadataProvider('Data', self::$metaNAMESPACE);
314
        });
315
        $this->app->singleton('objectmap', function ($app) {
0 ignored issues
show
Unused Code introduced by
The parameter $app is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

315
        $this->app->singleton('objectmap', function (/** @scrutinizer ignore-unused */ $app) {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
316
            return new Map();
317
        });
318
    }
319
320
    /**
321
     * @return array
322
     */
323
    protected function getCandidateModels()
324
    {
325
        $classes = $this->getClassMap();
326
        $ends = [];
327
        $startName = $this->getAppNamespace();
328
        foreach ($classes as $name) {
329
            if (\Illuminate\Support\Str::startsWith($name, $startName)) {
330
                if (in_array('AlgoWeb\\PODataLaravel\\Models\\MetadataTrait', class_uses($name)) &&
331
                is_subclass_of($name, '\\Illuminate\\Database\\Eloquent\\Model')) {
332
                    $ends[] = $name;
333
                }
334
            }
335
        }
336
        return $ends;
337
    }
338
339
    /**
340
     * @return MetadataGubbinsHolder
341
     */
342
    public function getRelationHolder()
343
    {
344
        return $this->relationHolder;
345
    }
346
347
    public function reset()
348
    {
349
        self::$isBooted = false;
350
        self::$afterExtract = null;
351
        self::$afterUnify = null;
352
        self::$afterVerify = null;
353
        self::$afterImplement = null;
354
    }
355
356
    /**
357
     * Resolve possible reverse relation property names.
358
     *
359
     * @param Model $source
360
     * @param $propName
361
     * @return null|string
362
     * @internal param Model $target
363
     */
364
    public function resolveReverseProperty(Model $source, $propName)
365
    {
366
        if (!is_string($propName)) {
367
            throw new InvalidOperationException('Property name must be string');
368
        }
369
        $entity = $this->getObjectMap()->resolveEntity(get_class($source));
370
        if (null === $entity) {
371
            $msg = 'Source model not defined';
372
            throw new \InvalidArgumentException($msg);
373
        }
374
        $association = $entity->resolveAssociation($propName);
375
        if (null === $association) {
376
            return null;
377
        }
378
        $isFirst = $propName === $association->getFirst()->getRelationName();
379
        if (!$isFirst) {
380
            return $association->getFirst()->getRelationName();
381
        }
382
383
        if (!$association instanceof AssociationMonomorphic) {
384
            throw new InvalidOperationException('');
385
        }
386
        return $association->getLast()->getRelationName();
387
    }
388
389
    public function isRunningInArtisan()
390
    {
391
        return App::runningInConsole() && !App::runningUnitTests();
392
    }
393
}
394