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