Total Complexity | 63 |
Total Lines | 359 |
Duplicated Lines | 0 % |
Changes | 1 | ||
Bugs | 1 | Features | 0 |
Complex classes like ItemNormalizer often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use ItemNormalizer, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
43 | final class ItemNormalizer extends AbstractItemNormalizer |
||
44 | { |
||
45 | use CacheKeyTrait; |
||
46 | use ClassInfoTrait; |
||
47 | use ContextTrait; |
||
48 | |||
49 | public const FORMAT = 'jsonapi'; |
||
50 | |||
51 | private $componentsCache = []; |
||
52 | |||
53 | public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, IriConverterInterface $iriConverter, ResourceClassResolverInterface $resourceClassResolver, ?PropertyAccessorInterface $propertyAccessor, ?NameConverterInterface $nameConverter, ResourceMetadataFactoryInterface $resourceMetadataFactory, array $defaultContext = [], iterable $dataTransformers = []) |
||
54 | { |
||
55 | parent::__construct($propertyNameCollectionFactory, $propertyMetadataFactory, $iriConverter, $resourceClassResolver, $propertyAccessor, $nameConverter, null, null, false, $defaultContext, $dataTransformers, $resourceMetadataFactory); |
||
56 | } |
||
57 | |||
58 | /** |
||
59 | * {@inheritdoc} |
||
60 | */ |
||
61 | public function supportsNormalization($data, $format = null): bool |
||
62 | { |
||
63 | return self::FORMAT === $format && parent::supportsNormalization($data, $format); |
||
64 | } |
||
65 | |||
66 | /** |
||
67 | * {@inheritdoc} |
||
68 | */ |
||
69 | public function normalize($object, $format = null, array $context = []) |
||
70 | { |
||
71 | if (null !== $this->getOutputClass($this->getObjectClass($object), $context)) { |
||
72 | return parent::normalize($object, $format, $context); |
||
73 | } |
||
74 | |||
75 | if (!isset($context['cache_key'])) { |
||
76 | $context['cache_key'] = $this->getCacheKey($format, $context); |
||
77 | } |
||
78 | |||
79 | $resourceClass = $this->resourceClassResolver->getResourceClass($object, $context['resource_class'] ?? null); |
||
80 | $context = $this->initContext($resourceClass, $context); |
||
81 | $iri = $this->iriConverter->getIriFromItem($object); |
||
82 | $context['iri'] = $iri; |
||
83 | $context['api_normalize'] = true; |
||
84 | |||
85 | $data = parent::normalize($object, $format, $context); |
||
86 | if (!\is_array($data)) { |
||
87 | return $data; |
||
88 | } |
||
89 | |||
90 | $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass); |
||
91 | |||
92 | // Get and populate relations |
||
93 | $allRelationshipsData = $this->getComponents($object, $format, $context)['relationships']; |
||
94 | $populatedRelationContext = $context; |
||
95 | $relationshipsData = $this->getPopulatedRelations($object, $format, $populatedRelationContext, $allRelationshipsData); |
||
96 | $includedResourcesData = $this->getRelatedResources($object, $format, $context, $allRelationshipsData); |
||
97 | |||
98 | $resourceData = [ |
||
99 | 'id' => $context['iri'], |
||
100 | 'type' => $resourceMetadata->getShortName(), |
||
101 | ]; |
||
102 | |||
103 | if ($data) { |
||
|
|||
104 | $resourceData['attributes'] = $data; |
||
105 | } |
||
106 | |||
107 | if ($relationshipsData) { |
||
108 | $resourceData['relationships'] = $relationshipsData; |
||
109 | } |
||
110 | |||
111 | $document = ['data' => $resourceData]; |
||
112 | |||
113 | if ($includedResourcesData) { |
||
114 | $document['included'] = $includedResourcesData; |
||
115 | } |
||
116 | |||
117 | return $document; |
||
118 | } |
||
119 | |||
120 | /** |
||
121 | * {@inheritdoc} |
||
122 | */ |
||
123 | public function supportsDenormalization($data, $type, $format = null): bool |
||
124 | { |
||
125 | return self::FORMAT === $format && parent::supportsDenormalization($data, $type, $format); |
||
126 | } |
||
127 | |||
128 | /** |
||
129 | * {@inheritdoc} |
||
130 | * |
||
131 | * @throws NotNormalizableValueException |
||
132 | */ |
||
133 | public function denormalize($data, $class, $format = null, array $context = []) |
||
134 | { |
||
135 | // Avoid issues with proxies if we populated the object |
||
136 | if (!isset($context[self::OBJECT_TO_POPULATE]) && isset($data['data']['id'])) { |
||
137 | if (true !== ($context['api_allow_update'] ?? true)) { |
||
138 | throw new NotNormalizableValueException('Update is not allowed for this operation.'); |
||
139 | } |
||
140 | |||
141 | $context[self::OBJECT_TO_POPULATE] = $this->iriConverter->getItemFromIri( |
||
142 | $data['data']['id'], |
||
143 | $context + ['fetch_data' => false] |
||
144 | ); |
||
145 | } |
||
146 | |||
147 | // Merge attributes and relationships, into format expected by the parent normalizer |
||
148 | $dataToDenormalize = array_merge( |
||
149 | $data['data']['attributes'] ?? [], |
||
150 | $data['data']['relationships'] ?? [] |
||
151 | ); |
||
152 | |||
153 | return parent::denormalize( |
||
154 | $dataToDenormalize, |
||
155 | $class, |
||
156 | $format, |
||
157 | $context |
||
158 | ); |
||
159 | } |
||
160 | |||
161 | /** |
||
162 | * {@inheritdoc} |
||
163 | */ |
||
164 | protected function getAttributes($object, $format = null, array $context): array |
||
165 | { |
||
166 | return $this->getComponents($object, $format, $context)['attributes']; |
||
167 | } |
||
168 | |||
169 | /** |
||
170 | * {@inheritdoc} |
||
171 | */ |
||
172 | protected function setAttributeValue($object, $attribute, $value, $format = null, array $context = []): void |
||
173 | { |
||
174 | parent::setAttributeValue($object, $attribute, \is_array($value) && \array_key_exists('data', $value) ? $value['data'] : $value, $format, $context); |
||
175 | } |
||
176 | |||
177 | /** |
||
178 | * {@inheritdoc} |
||
179 | * |
||
180 | * @see http://jsonapi.org/format/#document-resource-object-linkage |
||
181 | * |
||
182 | * @throws RuntimeException |
||
183 | * @throws NotNormalizableValueException |
||
184 | */ |
||
185 | protected function denormalizeRelation(string $attributeName, PropertyMetadata $propertyMetadata, string $className, $value, ?string $format, array $context) |
||
186 | { |
||
187 | if (!\is_array($value) || !isset($value['id'], $value['type'])) { |
||
188 | throw new NotNormalizableValueException('Only resource linkage supported currently, see: http://jsonapi.org/format/#document-resource-object-linkage.'); |
||
189 | } |
||
190 | |||
191 | try { |
||
192 | return $this->iriConverter->getItemFromIri($value['id'], $context + ['fetch_data' => true]); |
||
193 | } catch (ItemNotFoundException $e) { |
||
194 | throw new RuntimeException($e->getMessage(), $e->getCode(), $e); |
||
195 | } |
||
196 | } |
||
197 | |||
198 | /** |
||
199 | * {@inheritdoc} |
||
200 | * |
||
201 | * @see http://jsonapi.org/format/#document-resource-object-linkage |
||
202 | * |
||
203 | * @throws LogicException |
||
204 | */ |
||
205 | protected function normalizeRelation(PropertyMetadata $propertyMetadata, $relatedObject, string $resourceClass, ?string $format, array $context) |
||
206 | { |
||
207 | if (null === $relatedObject) { |
||
208 | if (isset($context['operation_type'], $context['subresource_resources'][$resourceClass]) && OperationType::SUBRESOURCE === $context['operation_type']) { |
||
209 | $iri = $this->iriConverter->getItemIriFromResourceClass($resourceClass, $context['subresource_resources'][$resourceClass]); |
||
210 | } else { |
||
211 | if ($this->serializer instanceof NormalizerInterface) { |
||
212 | return $this->serializer->normalize($relatedObject, $format, $context); |
||
213 | } |
||
214 | throw new LogicException(sprintf('The injected serializer must be an instance of "%s".', NormalizerInterface::class)); |
||
215 | } |
||
216 | } else { |
||
217 | $iri = $this->iriConverter->getIriFromItem($relatedObject); |
||
218 | $context['iri'] = $iri; |
||
219 | |||
220 | if (isset($context['resources'])) { |
||
221 | $context['resources'][$iri] = $iri; |
||
222 | } |
||
223 | if (isset($context['api_included'])) { |
||
224 | if (!$this->serializer instanceof NormalizerInterface) { |
||
225 | throw new LogicException(sprintf('The injected serializer must be an instance of "%s".', NormalizerInterface::class)); |
||
226 | } |
||
227 | |||
228 | return $this->serializer->normalize($relatedObject, $format, $context); |
||
229 | } |
||
230 | } |
||
231 | |||
232 | return [ |
||
233 | 'data' => [ |
||
234 | 'type' => $this->resourceMetadataFactory->create($resourceClass)->getShortName(), |
||
235 | 'id' => $iri, |
||
236 | ], |
||
237 | ]; |
||
238 | } |
||
239 | |||
240 | /** |
||
241 | * {@inheritdoc} |
||
242 | */ |
||
243 | protected function isAllowedAttribute($classOrObject, $attribute, $format = null, array $context = []): bool |
||
244 | { |
||
245 | return preg_match('/^\\w[-\\w_]*$/', $attribute) && parent::isAllowedAttribute($classOrObject, $attribute, $format, $context); |
||
246 | } |
||
247 | |||
248 | /** |
||
249 | * Gets JSON API components of the resource: attributes, relationships, meta and links. |
||
250 | * |
||
251 | * @param object $object |
||
252 | */ |
||
253 | private function getComponents($object, ?string $format, array $context): array |
||
254 | { |
||
255 | $cacheKey = $this->getObjectClass($object).'-'.$context['cache_key']; |
||
256 | |||
257 | if (isset($this->componentsCache[$cacheKey])) { |
||
258 | return $this->componentsCache[$cacheKey]; |
||
259 | } |
||
260 | |||
261 | $attributes = parent::getAttributes($object, $format, $context); |
||
262 | |||
263 | $options = $this->getFactoryOptions($context); |
||
264 | |||
265 | $components = [ |
||
266 | 'links' => [], |
||
267 | 'relationships' => [], |
||
268 | 'attributes' => [], |
||
269 | 'meta' => [], |
||
270 | ]; |
||
271 | |||
272 | foreach ($attributes as $attribute) { |
||
273 | $propertyMetadata = $this |
||
274 | ->propertyMetadataFactory |
||
275 | ->create($context['resource_class'], $attribute, $options); |
||
276 | |||
277 | $type = $propertyMetadata->getType(); |
||
278 | $isOne = $isMany = false; |
||
279 | |||
280 | if (null !== $type) { |
||
281 | if ($type->isCollection()) { |
||
282 | $isMany = ($type->getCollectionValueType() && $className = $type->getCollectionValueType()->getClassName()) ? $this->resourceClassResolver->isResourceClass($className) : false; |
||
283 | } else { |
||
284 | $isOne = ($className = $type->getClassName()) ? $this->resourceClassResolver->isResourceClass($className) : false; |
||
285 | } |
||
286 | } |
||
287 | |||
288 | if (!isset($className) || !$isOne && !$isMany) { |
||
289 | $components['attributes'][] = $attribute; |
||
290 | |||
291 | continue; |
||
292 | } |
||
293 | |||
294 | $relation = [ |
||
295 | 'name' => $attribute, |
||
296 | 'type' => $this->resourceMetadataFactory->create($className)->getShortName(), |
||
297 | 'cardinality' => $isOne ? 'one' : 'many', |
||
298 | ]; |
||
299 | |||
300 | $components['relationships'][] = $relation; |
||
301 | } |
||
302 | |||
303 | if (false !== $context['cache_key']) { |
||
304 | $this->componentsCache[$cacheKey] = $components; |
||
305 | } |
||
306 | |||
307 | return $components; |
||
308 | } |
||
309 | |||
310 | /** |
||
311 | * Populates relationships keys. |
||
312 | * |
||
313 | * @param object $object |
||
314 | * |
||
315 | * @throws UnexpectedValueException |
||
316 | */ |
||
317 | private function getPopulatedRelations($object, ?string $format, array $context, array $relationships): array |
||
318 | { |
||
319 | $data = []; |
||
320 | |||
321 | if (!isset($context['resource_class'])) { |
||
322 | return $data; |
||
323 | } |
||
324 | |||
325 | unset($context['api_included']); |
||
326 | foreach ($relationships as $relationshipDataArray) { |
||
327 | $relationshipName = $relationshipDataArray['name']; |
||
328 | |||
329 | $attributeValue = $this->getAttributeValue($object, $relationshipName, $format, $context); |
||
330 | |||
331 | if ($this->nameConverter) { |
||
332 | $relationshipName = $this->nameConverter->normalize($relationshipName, $context['resource_class'], self::FORMAT, $context); |
||
333 | } |
||
334 | |||
335 | if (!$attributeValue) { |
||
336 | continue; |
||
337 | } |
||
338 | |||
339 | $data[$relationshipName] = [ |
||
340 | 'data' => [], |
||
341 | ]; |
||
342 | |||
343 | // Many to one relationship |
||
344 | if ('one' === $relationshipDataArray['cardinality']) { |
||
345 | unset($attributeValue['data']['attributes']); |
||
346 | $data[$relationshipName] = $attributeValue; |
||
347 | |||
348 | continue; |
||
349 | } |
||
350 | |||
351 | // Many to many relationship |
||
352 | foreach ($attributeValue as $attributeValueElement) { |
||
353 | if (!isset($attributeValueElement['data'])) { |
||
354 | throw new UnexpectedValueException(sprintf('The JSON API attribute \'%s\' must contain a "data" key.', $relationshipName)); |
||
355 | } |
||
356 | unset($attributeValueElement['data']['attributes']); |
||
357 | $data[$relationshipName]['data'][] = $attributeValueElement['data']; |
||
358 | } |
||
359 | } |
||
360 | |||
361 | return $data; |
||
362 | } |
||
363 | |||
364 | /** |
||
365 | * Populates included keys. |
||
366 | */ |
||
367 | private function getRelatedResources($object, ?string $format, array $context, array $relationships): array |
||
402 | } |
||
403 | } |
||
404 |
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.