Completed
Pull Request — master (#79)
by Jacob
02:58
created

YamlFileDriver::setEmbeds()   B

Complexity

Conditions 5
Paths 9

Size

Total Lines 21
Code Lines 12

Duplication

Lines 3
Ratio 14.29 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 3
loc 21
rs 8.7624
cc 5
eloc 12
nc 9
nop 2
1
<?php
2
3
namespace As3\Modlr\Metadata\Driver;
4
5
use As3\Modlr\Metadata;
6
use As3\Modlr\Exception\RuntimeException;
7
use As3\Modlr\Exception\MetadataException;
8
use Symfony\Component\Yaml\Yaml;
9
10
/**
11
 * The YAML metadata file driver.
12
 *
13
 * @author Jacob Bare <[email protected]>
14
 */
15
final class YamlFileDriver extends AbstractFileDriver
16
{
17
    /**
18
     * An in-memory cache of parsed metadata mappings (from file).
19
     *
20
     * @var array
21
     */
22
    private $mappings = [
23
        'model' => [],
24
        'mixin' => [],
25
        'embed' => [],
26
    ];
27
28
    /**
29
     * {@inheritDoc}
30
     */
31
    protected function loadMetadataFromFile($type, $file)
32
    {
33
        $mapping = $this->getMapping('model', $type, $file);
34
35
        $metadata = new Metadata\EntityMetadata($type);
36
37
        if (isset($mapping['entity']['abstract'])) {
38
            $metadata->setAbstract(true);
39
        }
40
41 View Code Duplication
        if (isset($mapping['entity']['polymorphic'])) {
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...
42
            $metadata->setPolymorphic(true);
43
            $metadata->ownedTypes = $this->getOwnedTypes($metadata->type);
44
        }
45
46
        if (isset($mapping['entity']['extends'])) {
47
            $metadata->extends = $mapping['entity']['extends'];
48
        }
49
50 View Code Duplication
        if (isset($mapping['entity']['defaultValues']) && is_array($mapping['entity']['defaultValues'])) {
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...
51
            $metadata->defaultValues = $mapping['entity']['defaultValues'];
52
        }
53
54
        $this->setPersistence($metadata, $mapping['entity']['persistence']);
55
        $this->setSearch($metadata, $mapping['entity']['search']);
56
        $this->setAttributes($metadata, $mapping['attributes']);
57
        $this->setRelationships($metadata, $mapping['relationships']);
58
        $this->setEmbeds($metadata, $mapping['embeds']);
59
        $this->setMixins($metadata, $mapping['mixins']);
60
        return $metadata;
61
    }
62
63
    /**
64
     * {@inheritDoc}
65
     */
66
    protected function loadEmbedFromFile($embedName, $file)
67
    {
68
        $mapping = $this->getMapping('embed', $embedName, $file);
69
70
        $embed = new Metadata\EmbedMetadata($embedName);
71
        $this->setAttributes($embed, $mapping['attributes']);
72
        $this->setEmbeds($embed, $mapping['embeds']);
73
        $this->setMixins($embed, $mapping['mixins']);
74
        return $embed;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $embed; (As3\Modlr\Metadata\EmbedMetadata) is incompatible with the return type declared by the abstract method As3\Modlr\Metadata\Drive...iver::loadEmbedFromFile of type As3\Modlr\Metadata\Driver\EmbedMetadata|null.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
75
    }
76
77
    /**
78
     * {@inheritDoc}
79
     */
80
    protected function loadMixinFromFile($mixinName, $file)
81
    {
82
        $mapping = $this->getMapping('mixin', $mixinName, $file);
83
84
        $mixin = new Metadata\MixinMetadata($mixinName);
85
86
        $this->setAttributes($mixin, $mapping['attributes']);
87
        $this->setRelationships($mixin, $mapping['relationships']);
88
        return $mixin;
89
    }
90
91
    /**
92
     * {@inheritDoc}
93
     */
94
    public function getTypeHierarchy($type, array $types = [])
95
    {
96
        $path = $this->getFilePath('model', $type);
97
        $mapping = $this->getMapping('model', $type, $path);
98
99
        $types[] = $type;
100
        if (isset($mapping['entity']['extends']) && $mapping['entity']['extends'] !== $type) {
101
            return $this->getTypeHierarchy($mapping['entity']['extends'], $types);
102
        }
103
        return array_reverse($types);
104
    }
105
106
    /**
107
     * {@inheritDoc}
108
     */
109
    public function getOwnedTypes($type, array $types = [])
110
    {
111
        $path = $this->getFilePath('model', $type);
112
        $superMapping = $this->getMapping('model', $type, $path);
113
114
        $abstract = isset($superMapping['entity']['abstract']) && true === $superMapping['entity']['abstract'];
115
116
        foreach ($this->getAllTypeNames() as $searchType) {
117
118
            if ($type === $searchType && false === $abstract) {
119
                $types[] = $type;
120
                continue;
121
            }
122
            if (0 !== strpos($searchType, $type)) {
123
                continue;
124
            }
125
126
            $path = $this->getFilePath('model', $searchType);
127
            $mapping = $this->getMapping('model', $searchType, $path);
128
129
            if (!isset($mapping['entity']['extends']) || $mapping['entity']['extends'] !== $type) {
130
                continue;
131
            }
132
            $types[] = $searchType;
133
        }
134
        return $types;
135
    }
136
137
    /**
138
     * Gets the metadata mapping information from the YAML file.
139
     *
140
     * @param   string  $metaType   The metadata type, either mixin or model.
141
     * @param   string  $key        The metadata key name, either the mixin name or model type.
142
     * @param   string  $file       The YAML file location.
143
     * @return  array
144
     * @throws  MetadataException
145
     */
146
    private function getMapping($metaType, $key, $file)
147
    {
148
        if (isset($this->mappings[$metaType][$key])) {
149
            // Set to array cache to prevent multiple gets/parses.
150
            return $this->mappings[$metaType][$key];
151
        }
152
153
        $contents = Yaml::parse(file_get_contents($file));
154
        if (!isset($contents[$key])) {
155
            throw MetadataException::fatalDriverError($key, sprintf('No mapping key was found at the beginning of the YAML file. Expected "%s"', $key));
156
        }
157
        return $this->mappings[$metaType][$key] = $this->setDefaults($metaType, $contents[$key]);
158
    }
159
160
    /**
161
     * Sets the entity persistence metadata from the metadata mapping.
162
     *
163
     * @param   Metadata\EntityMetadata     $metadata
164
     * @param   array                       $mapping
165
     * @return  Metadata\EntityMetadata
166
     */
167 View Code Duplication
    protected function setPersistence(Metadata\EntityMetadata $metadata, array $mapping)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
168
    {
169
        $persisterKey = isset($mapping['key']) ? $mapping['key'] : null;
170
        $factory = $this->getPersistenceMetadataFactory($persisterKey);
171
172
        $persistence = $factory->createInstance($mapping);
173
174
        $metadata->setPersistence($persistence);
175
        return $metadata;
176
    }
177
178
    /**
179
     * Sets the entity search metadata from the metadata mapping.
180
     *
181
     * @param   Metadata\EntityMetadata     $metadata
182
     * @param   array                       $mapping
183
     * @return  Metadata\EntityMetadata
184
     */
185 View Code Duplication
    protected function setSearch(Metadata\EntityMetadata $metadata, array $mapping)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
186
    {
187
        $clientKey = isset($mapping['key']) ? $mapping['key'] : null;
188
        if (null === $clientKey) {
189
            // Search is not enabled for this model.
190
            return $metadata;
191
        }
192
193
        $factory = $this->getSearchMetadataFactory($clientKey);
194
195
        $search = $factory->createInstance($mapping);
196
197
        $metadata->setSearch($search);
198
        return $metadata;
199
    }
200
201
    /**
202
     * Sets the entity attribute metadata from the metadata mapping.
203
     *
204
     * @param   Metadata\Interfaces\AttributeInterface  $metadata
205
     * @param   array                                   $attrMapping
206
     * @return  Metadata\EntityMetadata
207
     */
208
    protected function setAttributes(Metadata\Interfaces\AttributeInterface $metadata, array $attrMapping)
209
    {
210
        foreach ($attrMapping as $key => $mapping) {
211
            if (!is_array($mapping)) {
212
                $mapping = ['type' => null];
213
            }
214
215
            if (!isset($mapping['type'])) {
216
                $mapping['type'] = null;
217
            }
218
219
            if (!isset($mapping['search'])) {
220
                $mapping['search'] = [];
221
            }
222
223
            $attribute = new Metadata\AttributeMetadata($key, $mapping['type'], $this->isMixin($metadata));
224
225
            if (isset($mapping['description'])) {
226
                $attribute->description = $mapping['description'];
227
            }
228
229
            if (isset($mapping['defaultValue'])) {
230
                $attribute->defaultValue = $mapping['defaultValue'];
231
            }
232
233
            if (isset($mapping['calculated']) && is_array($mapping['calculated'])) {
234
                $calculated = $mapping['calculated'];
235
                if (isset($calculated['class']) && isset($calculated['method'])) {
236
                    $attribute->calculated['class']  =  $calculated['class'];
237
                    $attribute->calculated['method'] =  $calculated['method'];
238
                }
239
            }
240
241
            if (isset($mapping['search']['autocomplete'])) {
242
                $attribute->setAutocomplete(true);
243
            } else if (isset($mapping['search']['store'])) {
244
                $attribute->setSearchProperty(true);
245
            }
246
247
            if (isset($mapping['save'])) {
248
                $attribute->enableSave($mapping['save']);
249
            }
250
251
            $metadata->addAttribute($attribute);
252
        }
253
        return $metadata;
254
    }
255
256
    /**
257
     * Sets the entity embed metadata from the metadata mapping.
258
     *
259
     * @param   Metadata\Interfaces\EmbedInterface  $metadata
260
     * @param   array                               $embedMapping
261
     * @return  Metadata\EntityMetadata
262
     */
263
    protected function setEmbeds(Metadata\Interfaces\EmbedInterface $metadata, array $embedMapping)
264
    {
265
        foreach ($embedMapping as $key => $mapping) {
266 View Code Duplication
            if (!is_array($mapping)) {
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...
267
                $mapping = ['type' => null, 'entity' => null];
268
            }
269
270
            if (!isset($mapping['type'])) {
271
                $mapping['type'] = null;
272
            }
273
274
            if (!isset($mapping['entity'])) {
275
                $mapping['entity'] = null;
276
            }
277
278
            $embedMeta = $this->loadMetadataForEmbed($mapping['entity']);
279
            $property = new Metadata\EmbeddedPropMetadata($key, $mapping['type'], $embedMeta, $this->isMixin($metadata));
0 ignored issues
show
Bug introduced by
It seems like $embedMeta defined by $this->loadMetadataForEmbed($mapping['entity']) on line 278 can also be of type null or object<As3\Modlr\Metadata\EntityMetadata> or object<As3\Modlr\Metadata\MixinMetadata>; however, As3\Modlr\Metadata\Embed...Metadata::__construct() does only seem to accept object<As3\Modlr\Metadata\EmbedMetadata>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
280
            $metadata->addEmbed($property);
281
        }
282
        return $metadata;
283
    }
284
285
    /**
286
     * Sets creates mixin metadata instances from a set of mixin mappings ands sets them to the entity metadata instance.
287
     *
288
     * @param   Metadata\Interfaces\MixinInterface  $metadata
289
     * @param   array                   $mixins
290
     * @return  Metadata\Interfaces\MixinInterface
291
     */
292
    protected function setMixins(Metadata\Interfaces\MixinInterface $metadata, array $mixins)
293
    {
294
        foreach ($mixins as $mixinName) {
295
            $mixinMeta = $this->loadMetadataForMixin($mixinName);
296
            $metadata->addMixin($mixinMeta);
0 ignored issues
show
Bug introduced by
It seems like $mixinMeta defined by $this->loadMetadataForMixin($mixinName) on line 295 can also be of type null or object<As3\Modlr\Metadata\EntityMetadata>; however, As3\Modlr\Metadata\Inter...inInterface::addMixin() does only seem to accept object<As3\Modlr\Metadata\MixinMetadata>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
297
        }
298
        return $metadata;
299
    }
300
301
    /**
302
     * Sets the entity relationship metadata from the metadata mapping.
303
     *
304
     * @param   Metadata\Interfaces\RelationshipInterface   $metadata
305
     * @param   array                                       $relMapping
306
     * @return  Metadata\Interfaces\RelationshipInterface
307
     * @throws  RuntimeException If the related entity type was not found.
308
     */
309
    protected function setRelationships(Metadata\Interfaces\RelationshipInterface $metadata, array $relMapping)
310
    {
311
        foreach ($relMapping as $key => $mapping) {
312 View Code Duplication
            if (!is_array($mapping)) {
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...
313
                $mapping = ['type' => null, 'entity' => null];
314
            }
315
316
            if (!isset($mapping['type'])) {
317
                $mapping['type'] = null;
318
            }
319
320
            if (!isset($mapping['entity'])) {
321
                $mapping['entity'] = null;
322
            }
323
324
            if (!isset($mapping['search'])) {
325
                $mapping['search'] = [];
326
            }
327
328
            $relationship = new Metadata\RelationshipMetadata($key, $mapping['type'], $mapping['entity'], $this->isMixin($metadata));
329
330
            if (isset($mapping['description'])) {
331
                $relationship->description = $mapping['description'];
332
            }
333
334
            if (isset($mapping['inverse'])) {
335
                $relationship->isInverse = true;
336
                if (isset($mapping['field'])) {
337
                    $relationship->inverseField = $mapping['field'];
338
                }
339
            }
340
341
            $path = $this->getFilePath('model', $mapping['entity']);
342
            $relatedEntityMapping = $this->getMapping('model', $mapping['entity'], $path);
343
344 View Code Duplication
            if (isset($relatedEntityMapping['entity']['polymorphic'])) {
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...
345
                $relationship->setPolymorphic(true);
346
                $relationship->ownedTypes = $this->getOwnedTypes($mapping['entity']);
347
            }
348
349
            if (isset($mapping['search']['store'])) {
350
                $relationship->setSearchProperty(true);
351
            }
352
353
            if (isset($mapping['save'])) {
354
                $relationship->enableSave($mapping['save']);
355
            }
356
357
            $metadata->addRelationship($relationship);
358
        }
359
        return $metadata;
360
    }
361
362
    /**
363
     * Determines if a metadata instance is a mixin.
364
     *
365
     * @param   Metadata\Interfaces\PropertyInterface   $metadata
366
     * @return  bool
367
     */
368
    protected function isMixin(Metadata\Interfaces\PropertyInterface $metadata)
369
    {
370
        return $metadata instanceof Metadata\MixinMetadata;
371
    }
372
373
    /**
374
     * Sets default values to the metadata mapping array.
375
     *
376
     * @param   string  $metaType   The metadata type, either model or mixin.
377
     * @param   mixed   $mapping    The parsed mapping data.
378
     * @return  array
379
     */
380
    protected function setDefaults($metaType, $mapping)
381
    {
382
        if (!is_array($mapping)) {
383
            $mapping = [];
384
        }
385
386
        $mapping = $this->setRootDefault('attributes', $mapping);
387
        $mapping = $this->setRootDefault('embeds', $mapping);
388
        $mapping = $this->setRootDefault('mixins', $mapping);
389
        if ('embed' === $metaType) {
390
            return $mapping;
391
        }
392
393
        $mapping = $this->setRootDefault('relationships', $mapping);
394
        if ('mixin' === $metaType) {
395
            return $mapping;
396
        }
397
        $this->setRootDefault('entity', $mapping);
398
399 View Code Duplication
        if (!isset($mapping['entity']['persistence']) || !is_array($mapping['entity']['persistence'])) {
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...
400
            $mapping['entity']['persistence'] = [];
401
        }
402
403 View Code Duplication
        if (!isset($mapping['entity']['search']) || !is_array($mapping['entity']['search'])) {
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...
404
            $mapping['entity']['search'] = [];
405
        }
406
        return $mapping;
407
    }
408
409
    /**
410
     * Sets a root level default value to a metadata mapping array.
411
     *
412
     * @param   string  $key
413
     * @param   array   $mapping
414
     * @return  array
415
     */
416
    private function setRootDefault($key, array $mapping)
417
    {
418
        if (!isset($mapping[$key]) || !is_array($mapping[$key])) {
419
            $mapping[$key] = [];
420
        }
421
        return $mapping;
422
    }
423
424
    /**
425
     * {@inheritDoc}
426
     */
427
    protected function getExtension()
428
    {
429
        return 'yml';
430
    }
431
}
432