Completed
Push — master ( 04aca3...4b3d1a )
by
unknown
03:17
created

ItemNormalizer::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 6
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 3
nc 1
nop 7
1
<?php
2
3
/*
4
 * This file is part of the API Platform project.
5
 *
6
 * (c) Kévin Dunglas <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
declare(strict_types=1);
13
14
namespace ApiPlatform\Core\JsonApi\Serializer;
15
16
use ApiPlatform\Core\Api\IriConverterInterface;
17
use ApiPlatform\Core\Api\OperationType;
18
use ApiPlatform\Core\Api\ResourceClassResolverInterface;
19
use ApiPlatform\Core\Exception\InvalidArgumentException;
20
use ApiPlatform\Core\Exception\ItemNotFoundException;
21
use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
22
use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
23
use ApiPlatform\Core\Metadata\Property\PropertyMetadata;
24
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
25
use ApiPlatform\Core\Serializer\AbstractItemNormalizer;
26
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
27
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
28
29
/**
30
 * Converts between objects and array.
31
 *
32
 * @author Kévin Dunglas <[email protected]>
33
 * @author Amrouche Hamza <[email protected]>
34
 * @author Baptiste Meyer <[email protected]>
35
 */
36
final class ItemNormalizer extends AbstractItemNormalizer
37
{
38
    const FORMAT = 'jsonapi';
39
40
    private $componentsCache = [];
41
    private $resourceMetadataFactory;
42
43
    public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, IriConverterInterface $iriConverter, ResourceClassResolverInterface $resourceClassResolver, PropertyAccessorInterface $propertyAccessor = null, NameConverterInterface $nameConverter = null, ResourceMetadataFactoryInterface $resourceMetadataFactory)
44
    {
45
        parent::__construct($propertyNameCollectionFactory, $propertyMetadataFactory, $iriConverter, $resourceClassResolver, $propertyAccessor, $nameConverter);
46
47
        $this->resourceMetadataFactory = $resourceMetadataFactory;
48
    }
49
50
    /**
51
     * {@inheritdoc}
52
     */
53
    public function supportsNormalization($data, $format = null)
54
    {
55
        return self::FORMAT === $format && parent::supportsNormalization($data, $format);
56
    }
57
58
    /**
59
     * {@inheritdoc}
60
     */
61
    public function normalize($object, $format = null, array $context = [])
62
    {
63
        $context['cache_key'] = $this->getJsonApiCacheKey($format, $context);
64
65
        // Get and populate attributes data
66
        $objectAttributesData = parent::normalize($object, $format, $context);
67
68
        if (!is_array($objectAttributesData)) {
69
            return $objectAttributesData;
70
        }
71
72
        // Get and populate item type
73
        $resourceClass = $this->resourceClassResolver->getResourceClass($object, $context['resource_class'] ?? null, true);
74
        $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
75
76
        // Get and populate relations
77
        $components = $this->getComponents($object, $format, $context);
78
        $objectRelationshipsData = $this->getPopulatedRelations($object, $format, $context, $components['relationships']);
79
80
        $item = [
81
            'id' => $this->iriConverter->getIriFromItem($object),
82
            'type' => $resourceMetadata->getShortName(),
83
        ];
84
85
        if ($objectAttributesData) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $objectAttributesData of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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.

Loading history...
86
            $item['attributes'] = $objectAttributesData;
87
        }
88
89
        if ($objectRelationshipsData) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $objectRelationshipsData of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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.

Loading history...
90
            $item['relationships'] = $objectRelationshipsData;
91
        }
92
93
        return ['data' => $item];
94
    }
95
96
    /**
97
     * {@inheritdoc}
98
     */
99
    public function supportsDenormalization($data, $type, $format = null)
100
    {
101
        return self::FORMAT === $format && parent::supportsDenormalization($data, $type, $format);
102
    }
103
104
    /**
105
     * {@inheritdoc}
106
     */
107
    public function denormalize($data, $class, $format = null, array $context = [])
108
    {
109
        // Avoid issues with proxies if we populated the object
110
        if (isset($data['data']['id']) && !isset($context[self::OBJECT_TO_POPULATE])) {
111 View Code Duplication
            if (isset($context['api_allow_update']) && true !== $context['api_allow_update']) {
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...
112
                throw new InvalidArgumentException('Update is not allowed for this operation.');
113
            }
114
115
            $context[self::OBJECT_TO_POPULATE] = $this->iriConverter->getItemFromIri(
116
                $data['data']['id'],
117
                $context + ['fetch_data' => false]
118
            );
119
        }
120
121
        // Merge attributes and relations previous to apply parents denormalizing
122
        $dataToDenormalize = array_merge(
123
            $data['data']['attributes'] ?? [],
124
            $data['data']['relationships'] ?? []
125
        );
126
127
        return parent::denormalize(
128
            $dataToDenormalize,
129
            $class,
130
            $format,
131
            $context
132
        );
133
    }
134
135
    /**
136
     * {@inheritdoc}
137
     */
138
    protected function getAttributes($object, $format = null, array $context)
139
    {
140
        return $this->getComponents($object, $format, $context)['attributes'];
141
    }
142
143
    /**
144
     * {@inheritdoc}
145
     */
146
    protected function setAttributeValue($object, $attribute, $value, $format = null, array $context = [])
147
    {
148
        parent::setAttributeValue($object, $attribute, is_array($value) && array_key_exists('data', $value) ? $value['data'] : $value, $format, $context);
149
    }
150
151
    /**
152
     * {@inheritdoc}
153
     *
154
     * @see http://jsonapi.org/format/#document-resource-object-linkage
155
     */
156
    protected function denormalizeRelation(string $attributeName, PropertyMetadata $propertyMetadata, string $className, $value, string $format = null, array $context)
157
    {
158
        // Give a chance to other normalizers (e.g.: DateTimeNormalizer)
159 View Code Duplication
        if (!$this->resourceClassResolver->isResourceClass($className)) {
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...
160
            $context['resource_class'] = $className;
161
162
            return $this->serializer->denormalize($value, $className, $format, $context);
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Symfony\Component\Serializer\SerializerInterface as the method denormalize() does only exist in the following implementations of said interface: Symfony\Component\Serializer\Serializer.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
163
        }
164
165
        if (!is_array($value) || !isset($value['id'], $value['type'])) {
166
            throw new InvalidArgumentException('Only resource linkage supported currently, see: http://jsonapi.org/format/#document-resource-object-linkage.');
167
        }
168
169
        try {
170
            return $this->iriConverter->getItemFromIri($value['id'], $context + ['fetch_data' => true]);
171
        } catch (ItemNotFoundException $e) {
172
            throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e);
173
        }
174
    }
175
176
    /**
177
     * {@inheritdoc}
178
     *
179
     * @see http://jsonapi.org/format/#document-resource-object-linkage
180
     */
181
    protected function normalizeRelation(PropertyMetadata $propertyMetadata, $relatedObject, string $resourceClass, string $format = null, array $context)
182
    {
183
        if (null === $relatedObject) {
184
            if (isset($context['operation_type']) && OperationType::SUBRESOURCE === $context['operation_type'] && isset($context['subresource_resources'][$resourceClass])) {
185
                $iri = $this->iriConverter->getItemIriFromResourceClass($resourceClass, $context['subresource_resources'][$resourceClass]);
186
            } else {
187
                unset($context['resource_class']);
188
189
                return $this->serializer->normalize($relatedObject, $format, $context);
190
            }
191
        } else {
192
            $iri = $this->iriConverter->getIriFromItem($relatedObject);
193
194
            if (isset($context['resources'])) {
195
                $context['resources'][$iri] = $iri;
196
            }
197
        }
198
199
        return ['data' => [
200
            'type' => $this->resourceMetadataFactory->create($resourceClass)->getShortName(),
201
            'id' => $iri,
202
        ]];
203
    }
204
205
    /**
206
     * {@inheritdoc}
207
     */
208
    protected function isAllowedAttribute($classOrObject, $attribute, $format = null, array $context = [])
209
    {
210
        return preg_match('/^\\w[-\\w_]*$/', $attribute) && parent::isAllowedAttribute($classOrObject, $attribute, $format, $context);
211
    }
212
213
    /**
214
     * Gets JSON API components of the resource: attributes, relationships, meta and links.
215
     *
216
     * @param object      $object
217
     * @param string|null $format
218
     * @param array       $context
219
     *
220
     * @return array
221
     */
222
    private function getComponents($object, string $format = null, array $context)
223
    {
224
        if (isset($this->componentsCache[$context['cache_key']])) {
225
            return $this->componentsCache[$context['cache_key']];
226
        }
227
228
        $attributes = parent::getAttributes($object, $format, $context);
0 ignored issues
show
Comprehensibility Bug introduced by
It seems like you call parent on a different method (getAttributes() instead of getComponents()). Are you sure this is correct? If so, you might want to change this to $this->getAttributes().

This check looks for a call to a parent method whose name is different than the method from which it is called.

Consider the following code:

class Daddy
{
    protected function getFirstName()
    {
        return "Eidur";
    }

    protected function getSurName()
    {
        return "Gudjohnsen";
    }
}

class Son
{
    public function getFirstName()
    {
        return parent::getSurname();
    }
}

The getFirstName() method in the Son calls the wrong method in the parent class.

Loading history...
229
230
        $options = $this->getFactoryOptions($context);
231
232
        $components = [
233
            'links' => [],
234
            'relationships' => [],
235
            'attributes' => [],
236
            'meta' => [],
237
        ];
238
239
        foreach ($attributes as $attribute) {
240
            $propertyMetadata = $this
241
                ->propertyMetadataFactory
242
                ->create($context['resource_class'], $attribute, $options);
243
244
            $type = $propertyMetadata->getType();
245
            $isOne = $isMany = false;
246
247
            if (null !== $type) {
248
                if ($type->isCollection()) {
249
                    $isMany = ($type->getCollectionValueType() && $className = $type->getCollectionValueType()->getClassName()) ? $this->resourceClassResolver->isResourceClass($className) : false;
0 ignored issues
show
Bug introduced by
The variable $className does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
250
                } else {
251
                    $isOne = ($className = $type->getClassName()) ? $this->resourceClassResolver->isResourceClass($className) : false;
252
                }
253
            }
254
255
            if (!isset($className) || !$isOne && !$isMany) {
256
                $components['attributes'][] = $attribute;
257
258
                continue;
259
            }
260
261
            $relation = [
262
                'name' => $attribute,
263
                'type' => $this->resourceMetadataFactory->create($className)->getShortName(),
264
                'cardinality' => $isOne ? 'one' : 'many',
265
            ];
266
267
            $components['relationships'][] = $relation;
268
        }
269
270
        return $this->componentsCache[$context['cache_key']] = $components;
271
    }
272
273
    /**
274
     * Populates relationships keys.
275
     *
276
     * @param object      $object
277
     * @param string|null $format
278
     * @param array       $context
279
     * @param array       $relationships
280
     *
281
     * @throws InvalidArgumentException
282
     *
283
     * @return array
284
     */
285
    private function getPopulatedRelations($object, string $format = null, array $context, array $relationships): array
286
    {
287
        $data = [];
288
289
        foreach ($relationships as $relationshipDataArray) {
290
            $relationshipName = $relationshipDataArray['name'];
291
292
            $attributeValue = $this->getAttributeValue($object, $relationshipName, $format, $context);
293
294
            if ($this->nameConverter) {
295
                $relationshipName = $this->nameConverter->normalize($relationshipName);
296
            }
297
298
            if (!$attributeValue) {
299
                continue;
300
            }
301
302
            $data[$relationshipName] = [
303
                'data' => [],
304
            ];
305
306
            // Many to one relationship
307
            if ('one' === $relationshipDataArray['cardinality']) {
308
                $data[$relationshipName] = $attributeValue;
309
310
                continue;
311
            }
312
313
            // Many to many relationship
314
            foreach ($attributeValue as $attributeValueElement) {
315
                if (!isset($attributeValueElement['data'])) {
316
                    throw new InvalidArgumentException(sprintf('The JSON API attribute \'%s\' must contain a "data" key.', $relationshipName));
317
                }
318
319
                $data[$relationshipName]['data'][] = $attributeValueElement['data'];
320
            }
321
        }
322
323
        return $data;
324
    }
325
326
    /**
327
     * Gets the cache key to use.
328
     *
329
     * @param string|null $format
330
     * @param array       $context
331
     *
332
     * @return bool|string
333
     */
334 View Code Duplication
    private function getJsonApiCacheKey(string $format = null, array $context)
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...
335
    {
336
        try {
337
            return md5($format.serialize($context));
338
        } catch (\Exception $exception) {
339
            // The context cannot be serialized, skip the cache
340
            return false;
341
        }
342
    }
343
}
344