Completed
Pull Request — master (#137)
by Alex
01:52
created

MetadataProvider::resolveReverseProperty()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 20
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

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