EntityUtility::validateMetadataInheritance()   C
last analyzed

Complexity

Conditions 12
Paths 16

Size

Total Lines 39
Code Lines 21

Duplication

Lines 9
Ratio 23.08 %

Importance

Changes 0
Metric Value
dl 9
loc 39
rs 5.1612
c 0
b 0
f 0
cc 12
eloc 21
nc 16
nop 2

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace As3\Modlr\Util;
4
5
use As3\Modlr\DataTypes\TypeFactory;
6
use As3\Modlr\Exception\MetadataException;
7
use As3\Modlr\Exception\RuntimeException;
8
use As3\Modlr\Metadata\EntityMetadata;
9
use As3\Modlr\Metadata\Interfaces\EmbedInterface;
10
use As3\Modlr\Metadata\Interfaces\AttributeInterface;
11
use As3\Modlr\Metadata\MetadataFactory;
12
use As3\Modlr\Rest\RestConfiguration;
13
14
/**
15
 * Responsibile for formatting entity names, such as entity types and field keys.
16
 *
17
 * @author Jacob Bare <[email protected]>
18
 */
19
class EntityUtility
20
{
21
    /**
22
     * @var RestConfiguration
23
     */
24
    private $config;
25
26
    /**
27
     * @var Inflector
28
     */
29
    private $inflector;
30
31
    /**
32
     * @var TypeFactory
33
     */
34
    private $typeFactory;
35
36
    /**
37
     * Constructor.
38
     *
39
     * @param   RestConfiguration   $config
40
     */
41
    public function __construct(RestConfiguration $config, TypeFactory $typeFactory)
42
    {
43
        $this->config = $config;
44
        $this->typeFactory = $typeFactory;
45
        $this->inflector = new Inflector();
46
    }
47
48
    /**
49
     * Gets the validator service.
50
     *
51
     * @return  Validator
52
     */
53
    public function getValidator()
54
    {
55
        return $this->config->getValidator();
56
    }
57
58
    /**
59
     * Gets the REST configuration object.
60
     *
61
     * @return  RestConfiguration
62
     */
63
    public function getRestConfig()
64
    {
65
        return $this->config;
66
    }
67
68
    /**
69
     * Determines if a field key is valid, based on configuration.
70
     *
71
     * @param   string  $value
72
     * @return  bool
73
     */
74
    public function isFieldKeyValid($value)
75
    {
76
        return $this->getValidator()->isNameValid($this->config->getFieldKeyFormat(), $value);
77
    }
78
79
    /**
80
     * Determines if an entity type, based on configuration.
81
     *
82
     * @param   string  $value
83
     * @return  bool
84
     */
85
    public function isEntityTypeValid($value)
86
    {
87
        return $this->getValidator()->isNameValid($this->config->getEntityFormat(), $value);
88
    }
89
90
    /**
91
     * Validates EntityMetadata.
92
     *
93
     * @param   string          $requestedType
94
     * @param   EntityMetadata  $metadata
95
     * @param   MetadataFactory $mf
96
     * @return  bool
97
     * @throws  MetadataException
98
     */
99
    public function validateMetadata($requestedType, EntityMetadata $metadata, MetadataFactory $mf)
100
    {
101
        if ($requestedType !== $metadata->type) {
102
            throw MetadataException::invalidMetadata($requestedType, 'Metadata type mismatch.');
103
        }
104
        $this->validateMetadataType($metadata);
105
106
        if (null === $metadata->persistence) {
107
            throw MetadataException::invalidMetadata($requestedType, 'No persistence metadata was found. All models must use a persistence layer.');
108
        }
109
110
        $this->validateMetadataInheritance($metadata, $mf);
111
        $this->validateMetadataAttributes($metadata, $mf);
112
        $this->validateMetadataRelationships($metadata, $mf);
113
        $this->validateMetadataEmbeds($metadata, $mf);
114
        return true;
115
    }
116
117
    /**
118
     * Validates the proper entity type on EntityMetadata.
119
     *
120
     * @param   EntityMetadata  $metadata
121
     * @return  bool
122
     * @throws  MetadataException
123
     */
124
    public function validateMetadataType(EntityMetadata $metadata)
125
    {
126
        if (true === $metadata->isChildEntity()) {
127
            $parentType = $metadata->getParentEntityType();
128
            if (0 !== strpos($metadata->type, $parentType)) {
129
                throw MetadataException::invalidMetadata($metadata->type, sprintf('Child class types must be prefixed by the parent class. Expected "%s" prefix.', $parentType));
130
            }
131
        }
132 View Code Duplication
        if (false === $this->isEntityTypeValid($metadata->type)) {
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...
133
            throw MetadataException::invalidMetadata($metadata->type, sprintf('The entity type is invalid based on the configured name format "%s"', $this->config->getEntityFormat()));
134
        }
135
        return true;
136
    }
137
138
    /**
139
     * Validates the proper entity inheritance on EntityMetadata.
140
     *
141
     * @param   EntityMetadata  $metadata
142
     * @param   MetadataFactory $mf
143
     * @return  bool
144
     * @throws  MetadataException
145
     */
146
    public function validateMetadataInheritance(EntityMetadata $metadata, MetadataFactory $mf)
147
    {
148
        if (true === $metadata->isPolymorphic()) {
149
            foreach ($metadata->ownedTypes as $child) {
150
151 View Code Duplication
                if (false === $this->isEntityTypeValid($child)) {
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...
152
                    throw MetadataException::invalidMetadata($metadata->type, sprintf('The owned entity type "%s" is invalid based on the configured name format "%s"', $child, $this->config->getEntityFormat()));
153
                }
154
155 View Code Duplication
                if (false === $mf->metadataExists($child)) {
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...
156
                    throw MetadataException::invalidMetadata($metadata->type, sprintf('The entity owns a type "%s" that does not exist.', $child));
157
                }
158
            }
159
        }
160
161
        if (false === $metadata->isPolymorphic() && true === $metadata->isAbstract()) {
162
            throw MetadataException::invalidMetadata($metadata->type, 'An entity must be polymorphic to be abstract.');
163
        }
164
165
        if (false === $metadata->isChildEntity()) {
166
            return true;
167
        }
168
        if (true === $metadata->isPolymorphic()) {
169
            throw MetadataException::invalidMetadata($metadata->type, 'An entity cannot both be polymorphic and be a child.');
170
        }
171
        if ($metadata->extends === $metadata->type) {
172
            throw MetadataException::invalidMetadata($metadata->type, 'An entity cannot extend itself.');
173
        }
174
175 View Code Duplication
        if (false === $mf->metadataExists($metadata->extends)) {
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...
176
            throw MetadataException::invalidMetadata($metadata->type, sprintf('The parent entity type "%s" does not exist.', $metadata->extends));
177
        }
178
179
        $parent = $mf->getMetadataForType($metadata->extends);
180
        if (false === $parent->isPolymorphic()) {
181
            throw MetadataException::invalidMetadata($metadata->type, sprintf('Parent classes must be polymorphic. Parent entity "%s" is not polymorphic.', $metadata->extends));
182
        }
183
        return true;
184
    }
185
186
    /**
187
     * Validates entity attributes on the AttributeInterface.
188
     *
189
     * @param   AttributeInterface  $metadata
190
     * @param   MetadataFactory     $mf
191
     * @return  bool
192
     * @throws  MetadataException
193
     */
194
    public function validateMetadataAttributes(AttributeInterface $metadata, MetadataFactory $mf)
195
    {
196
        $type = $metadata instanceof EntityMetadata ? $metadata->type : (property_exists($metadata, 'name') ? $metadata->name : '');
197
        foreach ($metadata->getAttributes() as $key => $attribute) {
198
199
            if ($key != $attribute->key) {
200
                throw MetadataException::invalidMetadata($type, sprintf('Attribute key mismtach. "%s" !== "%s"', $key, $attribute->key));
201
            }
202
203 View Code Duplication
            if (false === $this->isFieldKeyValid($attribute->key)) {
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...
204
                throw MetadataException::invalidMetadata($type, sprintf('The attribute key "%s" is invalid based on the configured name format "%s"', $attribute->key, $this->config->getFieldKeyFormat()));
205
            }
206
207
            if (false === $this->typeFactory->hasType($attribute->dataType)) {
208
                throw MetadataException::invalidMetadata($type, sprintf('The data type "%s" for attribute "%s" is invalid', $attribute->dataType, $attribute->key));
209
            }
210
211 View Code Duplication
            if ($metadata instanceof EntityMetadata && true === $metadata->isChildEntity()) {
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...
212
                $parent = $mf->getMetadataForType($metadata->extends);
213
                if ($parent->hasAttribute($attribute->key)) {
214
                    throw MetadataException::invalidMetadata($type, sprintf('Parent entity type "%s" already contains attribute field "%s"', $parent->type, $attribute->key));
215
                }
216
            }
217
218
            if (true === $attribute->isCalculated()) {
219
                if (false === class_exists($attribute->calculated['class']) || false === method_exists($attribute->calculated['class'], $attribute->calculated['method'])) {
220
                    throw MetadataException::invalidMetadata($type, sprintf('The attribute field "%s" is calculated, but was unable to find method "%s:%s"', $attribute->key, $attribute->calculated['class'], $attribute->calculated['method']));
221
                }
222
            }
223
        }
224
        return true;
225
    }
226
227
    /**
228
     * Validates entity relationships on EntityMetadata.
229
     *
230
     * @param   EmbedInterface  $metadata
231
     * @param   MetadataFactory $mf
232
     * @return  bool
233
     */
234
    public function validateMetadataEmbeds(EmbedInterface $metadata, MetadataFactory $mf)
235
    {
236
        foreach ($metadata->getEmbeds() as $key => $embeddedProp) {
237
            $this->validateMetadataAttributes($embeddedProp->embedMeta, $mf);
238
            if ($metadata !== $embeddedProp->embedMeta) {
239
                // Only re-validate if the embedded prop reference isn't the same as the parent.
240
                $this->validateMetadataEmbeds($embeddedProp->embedMeta, $mf);
241
            }
242
        }
243
        return true;
244
    }
245
246
    /**
247
     * Validates entity relationships on EntityMetadata.
248
     *
249
     * @param   EntityMetadata  $metadata
250
     * @param   MetadataFactory $mf
251
     * @return  bool
252
     * @throws  MetadataException
253
     */
254
    public function validateMetadataRelationships(EntityMetadata $metadata, MetadataFactory $mf)
255
    {
256
        foreach ($metadata->getRelationships() as $key => $relationship) {
257
            if ($key != $relationship->key) {
258
                throw MetadataException::invalidMetadata($metadata->type, sprintf('Relationship key mismtach. "%s" !== "%s"', $key, $relationship->key));
259
            }
260 View Code Duplication
            if (false === $this->isFieldKeyValid($relationship->key)) {
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...
261
                throw MetadataException::invalidMetadata($metadata->type, sprintf('The relationship key "%s" is invalid based on the configured name format "%s"', $relationship->key, $this->config->getFieldKeyFormat()));
262
            }
263 View Code Duplication
            if (true === $metadata->isChildEntity()) {
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...
264
                $parent = $mf->getMetadataForType($metadata->extends);
265
                if ($parent->hasRelationship($relationship->key)) {
266
                    throw MetadataException::invalidMetadata($metadata->type, sprintf('Parent entity type "%s" already contains relationship field "%s"', $parent->type, $relationship->key));
267
                }
268
            }
269 View Code Duplication
            if (false === $this->isEntityTypeValid($relationship->entityType)) {
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...
270
                throw MetadataException::invalidMetadata($metadata->type, sprintf('The related model "%s" for relationship "%s" is invalid based on the configured name format "%s"', $relationship->entityType, $relationship->key, $this->config->getEntityFormat()));
271
            }
272
            if (false === $mf->metadataExists($relationship->entityType)) {
273
                throw MetadataException::invalidMetadata($metadata->type, sprintf('The related model "%s" for relationship "%s" does not exist.', $relationship->entityType, $relationship->key));
274
            }
275
            if (true === $relationship->isInverse) {
276
                if ('one' === $relationship->relType) {
277
                    throw MetadataException::invalidMetadata($metadata->type, 'The relationship is inverse and one, which is currently not supported.');
278
                }
279
                if (empty($relationship->inverseField)) {
280
                    throw MetadataException::invalidMetadata($metadata->type, 'The relationship is inverse, but no inverse field was specified.');
281
                }
282
                $related = ($relationship->entityType === $metadata->type) ? $metadata : $mf->getMetadataForType($relationship->entityType);
283
                if (false === $related->hasRelationship($relationship->inverseField)) {
0 ignored issues
show
Documentation introduced by
$relationship->inverseField is of type boolean, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
284
                    throw MetadataException::invalidMetadata($metadata->type, 'The relationship is inverse, but the related model does not contain the specified inverse field.');
285
                }
286
                $relatedRel = $related->getRelationship($relationship->inverseField);
0 ignored issues
show
Documentation introduced by
$relationship->inverseField is of type boolean, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
287
                if (true === $relatedRel->isInverse) {
288
                    throw MetadataException::invalidMetadata($metadata->type, 'The relationship is inverse, but the relationship it references is also inverse.');
289
                }
290
                if ('one' !== $relatedRel->relType) {
291
                    throw MetadataException::invalidMetadata($metadata->type, 'The relationship is inverse and many, but it\'s reference is not a type of one.');
292
                }
293
294
            }
295
        }
296
        return true;
297
    }
298
299
    /**
300
     * Formats an entity type, based on configuration.
301
     *
302
     * @param   string  $type
303
     * @return  string
304
     */
305
    public function formatEntityType($type)
306
    {
307
        return $this->formatValue($this->config->getEntityFormat(), $type);
308
    }
309
310
    /**
311
     * Formats a field key, based on configuration.
312
     *
313
     * @param   string  $key
314
     * @return  string
315
     */
316
    public function formatFieldKey($key)
317
    {
318
        return $this->formatValue($this->config->getFieldKeyFormat(), $key);
319
    }
320
321
    /**
322
     * Formats a value, based on a formatting type.
323
     *
324
     * @param   string  $format
325
     * @param   string  $value
326
     * @return  string
327
     * @throws  RuntimeException
328
     */
329
    protected function formatValue($format, $value)
330
    {
331
        switch ($format) {
332
            case 'dash':
333
                return $this->inflector->dasherize($value);
334
            case 'underscore':
335
                return $this->inflector->underscore($value);
336
            case 'studlycaps':
337
                return $this->inflector->studlify($value);
338
            case 'camelcase':
339
                return $this->inflector->camelize($value);
340
            default:
341
                throw new RuntimeException('Unable to format value');
342
        }
343
    }
344
}
345