Completed
Pull Request — master (#1036)
by Hector
03:29
created

ItemNormalizer::getComponents()   C

Complexity

Conditions 13
Paths 35

Size

Total Lines 68
Code Lines 42

Duplication

Lines 7
Ratio 10.29 %

Importance

Changes 0
Metric Value
dl 7
loc 68
rs 5.761
c 0
b 0
f 0
cc 13
eloc 42
nc 35
nop 3

How to fix   Long Method    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
/*
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\ResourceClassResolverInterface;
18
use ApiPlatform\Core\DataProvider\ItemDataProviderInterface;
19
use ApiPlatform\Core\Exception\InvalidArgumentException;
20
use ApiPlatform\Core\Exception\RuntimeException;
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 ApiPlatform\Core\Serializer\ContextTrait;
27
use ApiPlatform\Core\Util\ClassInfoTrait;
28
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
29
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
30
31
/**
32
 * Converts between objects and array.
33
 *
34
 * @author Kévin Dunglas <[email protected]>
35
 * @author Amrouche Hamza <[email protected]>
36
 */
37
final class ItemNormalizer extends AbstractItemNormalizer
38
{
39
    use ContextTrait;
40
    use ClassInfoTrait;
41
42
    const FORMAT = 'jsonapi';
43
44
    private $componentsCache = [];
45
46
    private $resourceMetadataFactory;
47
48
    private $itemDataProvider;
49
50 View Code Duplication
    public function __construct(
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...
51
        PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory,
52
        PropertyMetadataFactoryInterface $propertyMetadataFactory,
53
        IriConverterInterface $iriConverter,
54
        ResourceClassResolverInterface $resourceClassResolver,
55
        PropertyAccessorInterface $propertyAccessor = null,
56
        NameConverterInterface $nameConverter = null,
57
        ResourceMetadataFactoryInterface $resourceMetadataFactory,
58
        ItemDataProviderInterface $itemDataProvider
59
    ) {
60
        parent::__construct(
61
            $propertyNameCollectionFactory,
62
            $propertyMetadataFactory,
63
            $iriConverter,
64
            $resourceClassResolver,
65
            $propertyAccessor,
66
            $nameConverter
67
        );
68
69
        $this->resourceMetadataFactory = $resourceMetadataFactory;
70
        $this->itemDataProvider = $itemDataProvider;
71
    }
72
73
    /**
74
     * {@inheritdoc}
75
     */
76
    public function supportsNormalization($data, $format = null)
77
    {
78
        return self::FORMAT === $format && parent::supportsNormalization($data, $format);
79
    }
80
81
    /**
82
     * {@inheritdoc}
83
     */
84
    public function normalize($object, $format = null, array $context = [])
85
    {
86
        $context['cache_key'] = $this->getCacheKey($format, $context);
87
88
        // Get and populate attributes data
89
        $objectAttributesData = parent::normalize($object, $format, $context);
90
91
        if (!is_array($objectAttributesData)) {
92
            return $objectAttributesData;
93
        }
94
95
        // Get and populate identifier if existent
96
        $identifier = $this->getIdentifierFromItem($object);
97
98
        // Get and populate item type
99
        $resourceClass = $this->resourceClassResolver->getResourceClass(
100
            $object,
101
            $context['resource_class'] ?? null,
102
            true
103
        );
104
        $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
105
106
        // Get and populate relations
107
        $components = $this->getComponents($object, $format, $context);
108
        $objectRelationshipsData = $this->getPopulatedRelations(
109
            $object,
110
            $format,
111
            $context,
112
            $components
113
        );
114
115
        $item = [
116
            // The id attribute must be a string
117
            // See: http://jsonapi.org/format/#document-resource-object-identification
118
            'id' => (string) $identifier,
119
            'type' => $resourceMetadata->getShortName(),
120
        ];
121
122
        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...
123
            $item['attributes'] = $objectAttributesData;
124
        }
125
126
        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...
127
            $item['relationships'] = $objectRelationshipsData;
128
        }
129
130
        return ['data' => $item];
131
    }
132
133
    /**
134
     * {@inheritdoc}
135
     */
136
    public function supportsDenormalization($data, $type, $format = null)
137
    {
138
        return self::FORMAT === $format && parent::supportsDenormalization($data, $type, $format);
139
    }
140
141
    /**
142
     * {@inheritdoc}
143
     */
144
    public function denormalize($data, $class, $format = null, array $context = [])
145
    {
146
        // Avoid issues with proxies if we populated the object
147 View Code Duplication
        if (isset($data['data']['id']) && !isset($context['object_to_populate'])) {
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...
148
            if (isset($context['api_allow_update']) && true !== $context['api_allow_update']) {
149
                throw new InvalidArgumentException('Update is not allowed for this operation.');
150
            }
151
152
            $context['object_to_populate'] = $this->iriConverter->getItemFromIri(
153
                $data['data']['id'],
154
                $context + ['fetch_data' => false]
155
            );
156
        }
157
158
        // Merge attributes and relations previous to apply parents denormalizing
159
        $dataToDenormalize = array_merge(
160
            $data['data']['attributes'] ?? [],
161
            $data['data']['relationships'] ?? []
162
        );
163
164
        return parent::denormalize(
165
            $dataToDenormalize,
166
            $class,
167
            $format,
168
            $context
169
        );
170
    }
171
172
    /**
173
     * {@inheritdoc}
174
     */
175
    protected function getAttributes($object, $format, array $context)
176
    {
177
        return $this->getComponents($object, $format, $context)['attributes'];
178
    }
179
180
    /**
181
     * Gets JSON API components of the resource: attributes, relationships, meta and links.
182
     *
183
     * @param object      $object
184
     * @param string|null $format
185
     * @param array       $context
186
     *
187
     * @return array
188
     */
189
    private function getComponents($object, string $format = null, array $context)
190
    {
191
        if (isset($this->componentsCache[$context['cache_key']])) {
192
            return $this->componentsCache[$context['cache_key']];
193
        }
194
195
        $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...
196
197
        $options = $this->getFactoryOptions($context);
198
199
        $typeShortName = $className = '';
200
201
        $components = [
202
            'links' => [],
203
            'relationships' => [],
204
            'attributes' => [],
205
            'meta' => [],
206
        ];
207
208
        foreach ($attributes as $attribute) {
209
            $propertyMetadata = $this
210
                ->propertyMetadataFactory
211
                ->create($context['resource_class'], $attribute, $options);
212
213
            $type = $propertyMetadata->getType();
214
            $isOne = $isMany = false;
215
216
            if (null !== $type) {
217 View Code Duplication
                if ($type->isCollection()) {
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...
218
                    $valueType = $type->getCollectionValueType();
219
220
                    $isMany = null !== $valueType
221
                        && ($className = $valueType->getClassName())
222
                        && $this->resourceClassResolver->isResourceClass($className);
223
                } else {
224
                    $className = $type->getClassName();
225
226
                    $isOne = null !== $className
227
                        && $this->resourceClassResolver->isResourceClass($className);
228
                }
229
230
                $typeShortName = '';
231
232
                if ($className && $this->resourceClassResolver->isResourceClass($className)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $className of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
233
                    $typeShortName = $this
234
                        ->resourceMetadataFactory
235
                        ->create($className)
236
                        ->getShortName();
237
                }
238
            }
239
240
            if (!$isOne && !$isMany) {
241
                $components['attributes'][] = $attribute;
242
243
                continue;
244
            }
245
246
            $relation = [
247
                'name' => $attribute,
248
                'type' => $typeShortName,
249
                'cardinality' => $isOne ? 'one' : 'many',
250
            ];
251
252
            $components['relationships'][] = $relation;
253
        }
254
255
        return $this->componentsCache[$context['cache_key']] = $components;
256
    }
257
258
    /**
259
     * Populates links and relationships keys.
260
     *
261
     * @param array       $data
0 ignored issues
show
Bug introduced by
There is no parameter named $data. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
262
     * @param object      $object
263
     * @param string|null $format
264
     * @param array       $context
265
     * @param array       $components
266
     * @param string      $type
267
     *
268
     * @return array
269
     */
270
    private function getPopulatedRelations(
271
        $object,
272
        string $format = null,
273
        array $context,
274
        array $components,
275
        string $type = 'relationships'
276
    ): array {
277
        $data = [];
278
279
        $identifier = '';
0 ignored issues
show
Unused Code introduced by
$identifier is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
280
        foreach ($components[$type] as $relationshipDataArray) {
281
            $relationshipName = $relationshipDataArray['name'];
282
283
            $attributeValue = $this->getAttributeValue(
284
                $object,
285
                $relationshipName,
286
                $format,
287
                $context
288
            );
289
290
            if ($this->nameConverter) {
291
                $relationshipName = $this->nameConverter->normalize($relationshipName);
292
            }
293
294
            if (!$attributeValue) {
295
                continue;
296
            }
297
298
            $data[$relationshipName] = [
299
                'data' => [],
300
            ];
301
302
            // Many to one relationship
303
            if ('one' === $relationshipDataArray['cardinality']) {
304
                $data[$relationshipName] = $attributeValue;
305
306
                continue;
307
            }
308
309
            // Many to many relationship
310
            foreach ($attributeValue as $attributeValueElement) {
311
                if (!isset($attributeValueElement['data'])) {
312
                    throw new RuntimeException(sprintf(
313
                        'Expected \'data\' attribute in collection for attribute \'%s\'',
314
                        $relationshipName
315
                    ));
316
                }
317
318
                $data[$relationshipName]['data'][] = $attributeValueElement['data'];
319
            }
320
        }
321
322
        return $data;
323
    }
324
325
    /**
326
     * Gets the IRI of the given relation.
327
     *
328
     * @param array|string $rel
329
     *
330
     * @return string
331
     */
332
    private function getRelationIri($rel): string
333
    {
334
        return $rel['links']['self'] ?? $rel;
335
    }
336
337
    /**
338
     * Gets the cache key to use.
339
     *
340
     * @param string|null $format
341
     * @param array       $context
342
     *
343
     * @return bool|string
344
     */
345 View Code Duplication
    private function getCacheKey(string $format = null, array $context)
0 ignored issues
show
Bug introduced by
Consider using a different method name as you override a private method of the parent class.

Overwriting private methods is generally fine as long as you also use private visibility. It might still be preferable for understandability to use a different method name.

Loading history...
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...
346
    {
347
        try {
348
            return md5($format.serialize($context));
349
        } catch (\Exception $exception) {
350
            // The context cannot be serialized, skip the cache
351
            return false;
352
        }
353
    }
354
355
    /**
356
     * Denormalizes a resource linkage relation.
357
     *
358
     * See: http://jsonapi.org/format/#document-resource-object-linkage
359
     *
360
     * @param string           $attributeName    [description]
361
     * @param PropertyMetadata $propertyMetadata [description]
362
     * @param string           $className        [description]
363
     * @param [type]           $data             [description]
0 ignored issues
show
Documentation introduced by
The doc-type [type] could not be parsed: Unknown type name "" at position 0. [(view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
364
     * @param string|null      $format           [description]
365
     * @param array            $context          [description]
366
     *
367
     * @return [type] [description]
0 ignored issues
show
Documentation introduced by
The doc-type [type] could not be parsed: Unknown type name "" at position 0. [(view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
368
     */
369
    protected function denormalizeRelation(
370
        string $attributeName,
371
        PropertyMetadata $propertyMetadata,
372
        string $className,
373
        $data,
374
        string $format = null,
375
        array $context
376
    ) {
377
        // Null is allowed for empty to-one relationships, see
378
        // http://jsonapi.org/format/#document-resource-object-linkage
379
        if (null === $data['data']) {
380
            return;
381
        }
382
383
        // An empty array is allowed for empty to-many relationships, see
384
        // http://jsonapi.org/format/#document-resource-object-linkage
385
        if ([] === $data['data']) {
386
            return;
387
        }
388
389
        if (!isset($data['data'])) {
390
            throw new InvalidArgumentException(
391
                'Key \'data\' expected. Only resource linkage currently supported, see: http://jsonapi.org/format/#document-resource-object-linkage'
392
            );
393
        }
394
395
        $data = $data['data'];
396
397
        if (!is_array($data) || 2 !== count($data)) {
398
            throw new InvalidArgumentException(
399
                'Only resource linkage supported currently supported, see: http://jsonapi.org/format/#document-resource-object-linkage'
400
            );
401
        }
402
403
        if (!isset($data['id'])) {
404
            throw new InvalidArgumentException(
405
                'Only resource linkage supported currently supported, see: http://jsonapi.org/format/#document-resource-object-linkage'
406
            );
407
        }
408
409
        return $this->itemDataProvider->getItem(
410
            $this->resourceClassResolver->getResourceClass(null, $className),
411
            $data['id']
412
        );
413
    }
414
415
    /**
416
     * Normalizes a relation as resource linkage relation.
417
     *
418
     * See: http://jsonapi.org/format/#document-resource-object-linkage
419
     *
420
     * For example, it may return the following array:
421
     *
422
     * [
423
     *     'data' => [
424
     *         'type' => 'dummy',
425
     *         'id' => '1'
426
     *     ]
427
     * ]
428
     *
429
     * @param PropertyMetadata $propertyMetadata
430
     * @param mixed            $relatedObject
431
     * @param string           $resourceClass
432
     * @param string|null      $format
433
     * @param array            $context
434
     *
435
     * @return string|array
436
     */
437
    protected function normalizeRelation(
438
        PropertyMetadata $propertyMetadata,
439
        $relatedObject,
440
        string $resourceClass,
441
        string $format = null,
442
        array $context
443
    ) {
444
        $resourceClass = $this->resourceClassResolver->getResourceClass(
445
            $relatedObject,
446
            null,
447
            true
448
        );
449
450
        $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
451
452
        $identifier = $this->getIdentifierFromItem($relatedObject);
453
454
        return ['data' => [
455
            'type' => $resourceMetadata->getShortName(),
456
            'id' => (string) $identifier,
457
        ]];
458
    }
459
460
    private function getIdentifierFromItem($item)
461
    {
462
        $identifiers = $this->getIdentifiersFromItem($item);
463
464
        if (count($identifiers) > 1) {
465
            throw new RuntimeException(sprintf(
466
                'Multiple identifiers are not supported during serialization of relationships (Entity: \'%s\')',
467
                $resourceClass
468
            ));
469
        }
470
471
        return reset($identifiers);
472
    }
473
474
    /**
475
     * Find identifiers from an Item (Object).
476
     *
477
     * Taken from ApiPlatform\Core\Bridge\Symfony\Routing\IriConverter
478
     *
479
     * @param object $item
480
     *
481
     * @throws RuntimeException
482
     *
483
     * @return array
484
     */
485 View Code Duplication
    private function getIdentifiersFromItem($item): array
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...
486
    {
487
        $identifiers = [];
488
        $resourceClass = $this->getObjectClass($item);
489
490
        foreach ($this->propertyNameCollectionFactory->create($resourceClass) as $propertyName) {
491
            $propertyMetadata = $this
492
                ->propertyMetadataFactory
493
                ->create($resourceClass, $propertyName);
494
495
            $identifier = $propertyMetadata->isIdentifier();
496
            if (null === $identifier || false === $identifier) {
497
                continue;
498
            }
499
500
            $identifiers[$propertyName] = $this
501
                ->propertyAccessor
502
                ->getValue($item, $propertyName);
503
504
            if (!is_object($identifiers[$propertyName])) {
505
                continue;
506
            }
507
508
            $relatedResourceClass = $this->getObjectClass($identifiers[$propertyName]);
509
            $relatedItem = $identifiers[$propertyName];
510
511
            unset($identifiers[$propertyName]);
512
513
            foreach (
514
                $this
515
                    ->propertyNameCollectionFactory
516
                    ->create($relatedResourceClass)
517
                    as $relatedPropertyName
518
            ) {
519
                $propertyMetadata = $this
520
                    ->propertyMetadataFactory
521
                    ->create($relatedResourceClass, $relatedPropertyName);
522
523
                if ($propertyMetadata->isIdentifier()) {
524
                    if (isset($identifiers[$propertyName])) {
525
                        throw new RuntimeException(sprintf(
526
                            'Composite identifiers not supported in "%s" through relation "%s" of "%s" used as identifier',
527
                            $relatedResourceClass,
528
                            $propertyName,
529
                            $resourceClass
530
                        ));
531
                    }
532
533
                    $identifiers[$propertyName] = $this
534
                        ->propertyAccessor
535
                        ->getValue(
536
                            $relatedItem,
537
                            $relatedPropertyName
538
                        );
539
                }
540
            }
541
542
            if (!isset($identifiers[$propertyName])) {
543
                throw new RuntimeException(sprintf(
544
                    'No identifier found in "%s" through relation "%s" of "%s" used as identifier',
545
                    $relatedResourceClass,
546
                    $propertyName,
547
                    $resourceClass
548
                ));
549
            }
550
        }
551
552
        return $identifiers;
553
    }
554
}
555