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

EntityUtility::validateMetadata()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 17
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 17
rs 9.4285
cc 3
eloc 11
nc 3
nop 3
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);
0 ignored issues
show
Documentation introduced by
$metadata->extends 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...
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
        foreach ($metadata->getAttributes() as $key => $attribute) {
197
            if ($key != $attribute->key) {
198
                throw MetadataException::invalidMetadata($metadata->type, sprintf('Attribute key mismtach. "%s" !== "%s"', $key, $attribute->key));
0 ignored issues
show
Bug introduced by
Accessing type on the interface As3\Modlr\Metadata\Interfaces\AttributeInterface suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
199
            }
200
201 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...
202
                throw MetadataException::invalidMetadata($metadata->type, sprintf('The attribute key "%s" is invalid based on the configured name format "%s"', $attribute->key, $this->config->getFieldKeyFormat()));
0 ignored issues
show
Bug introduced by
Accessing type on the interface As3\Modlr\Metadata\Interfaces\AttributeInterface suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
203
            }
204
205
            if (false === $this->typeFactory->hasType($attribute->dataType)) {
206
                throw MetadataException::invalidMetadata($metadata->type, sprintf('The data type "%s" for attribute "%s" is invalid', $attribute->dataType, $attribute->key));
0 ignored issues
show
Bug introduced by
Accessing type on the interface As3\Modlr\Metadata\Interfaces\AttributeInterface suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
207
            }
208
209 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...
210
                $parent = $mf->getMetadataForType($metadata->extends);
0 ignored issues
show
Documentation introduced by
$metadata->extends 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...
211
                if ($parent->hasAttribute($attribute->key)) {
212
                    throw MetadataException::invalidMetadata($metadata->type, sprintf('Parent entity type "%s" already contains attribute field "%s"', $parent->type, $attribute->key));
213
                }
214
            }
215
216
            if (true === $attribute->isCalculated()) {
217
                if (false === class_exists($attribute->calculated['class']) || false === method_exists($attribute->calculated['class'], $attribute->calculated['method'])) {
218
                    throw MetadataException::invalidMetadata($metadata->type, sprintf('The attribute field "%s" is calculated, but was unable to find method "%s:%s"', $attribute->key, $attribute->calculated['class'], $attribute->calculated['method']));
0 ignored issues
show
Bug introduced by
Accessing type on the interface As3\Modlr\Metadata\Interfaces\AttributeInterface suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
219
                }
220
            }
221
        }
222
        return true;
223
    }
224
225
    /**
226
     * Validates entity relationships on EntityMetadata.
227
     *
228
     * @param   EmbedInterface  $metadata
229
     * @param   MetadataFactory $mf
230
     * @return  bool
231
     */
232
    public function validateMetadataEmbeds(EmbedInterface $metadata, MetadataFactory $mf)
233
    {
234
        foreach ($metadata->getEmbeds() as $key => $embeddedProp) {
235
            $this->validateMetadataAttributes($embeddedProp->embedMeta, $mf);
0 ignored issues
show
Documentation introduced by
$embeddedProp->embedMeta is of type string, but the function expects a object<As3\Modlr\Metadat...ces\AttributeInterface>.

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...
236
            $this->validateMetadataEmbeds($embeddedProp->embedMeta, $mf);
0 ignored issues
show
Documentation introduced by
$embeddedProp->embedMeta is of type string, but the function expects a object<As3\Modlr\Metadat...erfaces\EmbedInterface>.

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...
237
        }
238
        return true;
239
    }
240
241
    /**
242
     * Validates entity relationships on EntityMetadata.
243
     *
244
     * @param   EntityMetadata  $metadata
245
     * @param   MetadataFactory $mf
246
     * @return  bool
247
     * @throws  MetadataException
248
     */
249
    public function validateMetadataRelationships(EntityMetadata $metadata, MetadataFactory $mf)
250
    {
251
        foreach ($metadata->getRelationships() as $key => $relationship) {
252
            if ($key != $relationship->key) {
253
                throw MetadataException::invalidMetadata($metadata->type, sprintf('Relationship key mismtach. "%s" !== "%s"', $key, $relationship->key));
254
            }
255 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...
256
                throw MetadataException::invalidMetadata($metadata->type, sprintf('The relationship key "%s" is invalid based on the configured name format "%s"', $relationship->key, $this->config->getFieldKeyFormat()));
257
            }
258 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...
259
                $parent = $mf->getMetadataForType($metadata->extends);
0 ignored issues
show
Documentation introduced by
$metadata->extends 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...
260
                if ($parent->hasRelationship($relationship->key)) {
261
                    throw MetadataException::invalidMetadata($metadata->type, sprintf('Parent entity type "%s" already contains relationship field "%s"', $parent->type, $relationship->key));
262
                }
263
            }
264 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...
265
                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()));
266
            }
267
            if (false === $mf->metadataExists($relationship->entityType)) {
268
                throw MetadataException::invalidMetadata($metadata->type, sprintf('The related model "%s" for relationship "%s" does not exist.', $relationship->entityType, $relationship->key));
269
            }
270
            if (true === $relationship->isInverse) {
271
                if ('one' === $relationship->relType) {
272
                    throw MetadataException::invalidMetadata($metadata->type, 'The relationship is inverse and one, which is currently not supported.');
273
                }
274
                if (empty($relationship->inverseField)) {
275
                    throw MetadataException::invalidMetadata($metadata->type, 'The relationship is inverse, but no inverse field was specified.');
276
                }
277
                $related = ($relationship->entityType === $metadata->type) ? $metadata : $mf->getMetadataForType($relationship->entityType);
278
                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...
279
                    throw MetadataException::invalidMetadata($metadata->type, 'The relationship is inverse, but the related model does not contain the specified inverse field.');
280
                }
281
                $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...
282
                if (true === $relatedRel->isInverse) {
283
                    throw MetadataException::invalidMetadata($metadata->type, 'The relationship is inverse, but the relationship it references is also inverse.');
284
                }
285
                if ('one' !== $relatedRel->relType) {
286
                    throw MetadataException::invalidMetadata($metadata->type, 'The relationship is inverse and many, but it\'s reference is not a type of one.');
287
                }
288
289
            }
290
        }
291
        return true;
292
    }
293
294
    /**
295
     * Formats an entity type, based on configuration.
296
     *
297
     * @param   string  $type
298
     * @return  string
299
     */
300
    public function formatEntityType($type)
301
    {
302
        return $this->formatValue($this->config->getEntityFormat(), $type);
303
    }
304
305
    /**
306
     * Formats a field key, based on configuration.
307
     *
308
     * @param   string  $key
309
     * @return  string
310
     */
311
    public function formatFieldKey($key)
312
    {
313
        return $this->formatValue($this->config->getFieldKeyFormat(), $key);
314
    }
315
316
    /**
317
     * Formats a value, based on a formatting type.
318
     *
319
     * @param   string  $format
320
     * @param   string  $value
321
     * @return  string
322
     * @throws  RuntimeException
323
     */
324
    protected function formatValue($format, $value)
325
    {
326
        switch ($format) {
327
            case 'dash':
328
                return $this->inflector->dasherize($value);
329
            case 'underscore':
330
                return $this->inflector->underscore($value);
331
            case 'studlycaps':
332
                return $this->inflector->studlify($value);
333
            case 'camelcase':
334
                return $this->inflector->camelize($value);
335
            default:
336
                throw new RuntimeException('Unable to format value');
337
        }
338
    }
339
}
340