• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

api-platform / core / 6693023961

30 Oct 2023 12:51PM UTC coverage: 67.319%. Remained the same
6693023961

push

github

web-flow
ci: php cs fixer (#5905)

--


fix cs


fix last cs

259 of 259 new or added lines in 83 files covered. (100.0%)

15610 of 23188 relevant lines covered (67.32%)

10.87 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

47.46
/src/JsonApi/Serializer/ItemNormalizer.php
1
<?php
2

3
/*
4
 * This file is part of the API Platform project.
5
 *
6
 * (c) Kévin Dunglas <dunglas@gmail.com>
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\JsonApi\Serializer;
15

16
use ApiPlatform\Api\ResourceClassResolverInterface;
17
use ApiPlatform\Api\UrlGeneratorInterface;
18
use ApiPlatform\Core\Api\IriConverterInterface as LegacyIriConverterInterface;
19
use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
20
use ApiPlatform\Core\Metadata\Property\PropertyMetadata;
21
use ApiPlatform\Core\Metadata\Resource\ResourceMetadata;
22
use ApiPlatform\Exception\ItemNotFoundException;
23
use ApiPlatform\Metadata\ApiProperty;
24
use ApiPlatform\Metadata\Resource\ResourceMetadataCollection;
25
use ApiPlatform\Serializer\AbstractItemNormalizer;
26
use ApiPlatform\Serializer\CacheKeyTrait;
27
use ApiPlatform\Serializer\ContextTrait;
28
use ApiPlatform\Symfony\Security\ResourceAccessCheckerInterface;
29
use ApiPlatform\Util\ClassInfoTrait;
30
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
31
use Symfony\Component\PropertyInfo\Type;
32
use Symfony\Component\Serializer\Exception\LogicException;
33
use Symfony\Component\Serializer\Exception\NotNormalizableValueException;
34
use Symfony\Component\Serializer\Exception\RuntimeException;
35
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
36
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
37
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
38

39
/**
40
 * Converts between objects and array.
41
 *
42
 * @author Kévin Dunglas <dunglas@gmail.com>
43
 * @author Amrouche Hamza <hamza.simperfit@gmail.com>
44
 * @author Baptiste Meyer <baptiste.meyer@gmail.com>
45
 */
46
final class ItemNormalizer extends AbstractItemNormalizer
47
{
48
    use CacheKeyTrait;
49
    use ClassInfoTrait;
50
    use ContextTrait;
51

52
    public const FORMAT = 'jsonapi';
53

54
    private $componentsCache = [];
55

56
    public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, $propertyMetadataFactory, $iriConverter, ResourceClassResolverInterface $resourceClassResolver, ?PropertyAccessorInterface $propertyAccessor, ?NameConverterInterface $nameConverter, $resourceMetadataFactory, array $defaultContext = [], iterable $dataTransformers = [], ResourceAccessCheckerInterface $resourceAccessChecker = null)
57
    {
58
        parent::__construct($propertyNameCollectionFactory, $propertyMetadataFactory, $iriConverter, $resourceClassResolver, $propertyAccessor, $nameConverter, null, null, false, $defaultContext, $dataTransformers, $resourceMetadataFactory, $resourceAccessChecker);
37✔
59
    }
60

61
    public function supportsNormalization($data, $format = null, array $context = []): bool
62
    {
63
        return self::FORMAT === $format && parent::supportsNormalization($data, $format, $context);
14✔
64
    }
65

66
    /**
67
     * @param mixed|null $format
68
     *
69
     * @return array|string|int|float|bool|\ArrayObject|null
70
     */
71
    public function normalize($object, $format = null, array $context = [])
72
    {
73
        $resourceClass = $this->getObjectClass($object);
4✔
74
        if ($this->getOutputClass($resourceClass, $context)) {
4✔
75
            return parent::normalize($object, $format, $context);
×
76
        }
77

78
        if ($this->resourceClassResolver->isResourceClass($resourceClass)) {
4✔
79
            $resourceClass = $this->resourceClassResolver->getResourceClass($object, $context['resource_class'] ?? null);
4✔
80
        }
81

82
        $context = $this->initContext($resourceClass, $context);
4✔
83
        $iri = $this->iriConverter instanceof LegacyIriConverterInterface ? $this->iriConverter->getIriFromItem($object) : $this->iriConverter->getIriFromResource($object, UrlGeneratorInterface::ABS_PATH, $context['operation'] ?? null, $context);
4✔
84
        $context['iri'] = $iri;
4✔
85
        $context['api_normalize'] = true;
4✔
86

87
        if (!isset($context['cache_key'])) {
4✔
88
            $context['cache_key'] = $this->getCacheKey($format, $context);
4✔
89
        }
90

91
        $data = parent::normalize($object, $format, $context);
4✔
92
        if (!\is_array($data)) {
3✔
93
            return $data;
1✔
94
        }
95

96
        // Get and populate relations
97
        $allRelationshipsData = $this->getComponents($object, $format, $context)['relationships'];
2✔
98
        $populatedRelationContext = $context;
2✔
99
        $relationshipsData = $this->getPopulatedRelations($object, $format, $populatedRelationContext, $allRelationshipsData);
2✔
100

101
        // Do not include primary resources
102
        $context['api_included_resources'] = [$context['iri']];
2✔
103

104
        $includedResourcesData = $this->getRelatedResources($object, $format, $context, $allRelationshipsData);
2✔
105

106
        $resourceData = [
2✔
107
            'id' => $context['iri'],
2✔
108
            'type' => $this->getResourceShortName($resourceClass),
2✔
109
        ];
2✔
110

111
        if ($data) {
2✔
112
            $resourceData['attributes'] = $data;
2✔
113
        }
114

115
        if ($relationshipsData) {
2✔
116
            $resourceData['relationships'] = $relationshipsData;
×
117
        }
118

119
        $document = ['data' => $resourceData];
2✔
120

121
        if ($includedResourcesData) {
2✔
122
            $document['included'] = $includedResourcesData;
×
123
        }
124

125
        return $document;
2✔
126
    }
127

128
    public function supportsDenormalization($data, $type, $format = null, array $context = []): bool
129
    {
130
        return self::FORMAT === $format && parent::supportsDenormalization($data, $type, $format, $context);
4✔
131
    }
132

133
    /**
134
     * @param mixed|null $format
135
     *
136
     * @throws NotNormalizableValueException
137
     */
138
    public function denormalize($data, $class, $format = null, array $context = [])
139
    {
140
        // Avoid issues with proxies if we populated the object
141
        if (!isset($context[self::OBJECT_TO_POPULATE]) && isset($data['data']['id'])) {
5✔
142
            if (true !== ($context['api_allow_update'] ?? true)) {
1✔
143
                throw new NotNormalizableValueException('Update is not allowed for this operation.');
1✔
144
            }
145

146
            $context[self::OBJECT_TO_POPULATE] = $this->iriConverter instanceof LegacyIriConverterInterface ? $this->iriConverter->getItemFromIri(
×
147
                $data['data']['id'],
×
148
                $context + ['fetch_data' => false]
×
149
            ) : $this->iriConverter->getResourceFromIri(
×
150
                $data['data']['id'],
×
151
                $context + ['fetch_data' => false]
×
152
            );
×
153
        }
154

155
        // Merge attributes and relationships, into format expected by the parent normalizer
156
        $dataToDenormalize = array_merge(
4✔
157
            $data['data']['attributes'] ?? [],
4✔
158
            $data['data']['relationships'] ?? []
4✔
159
        );
4✔
160

161
        return parent::denormalize(
4✔
162
            $dataToDenormalize,
4✔
163
            $class,
4✔
164
            $format,
4✔
165
            $context
4✔
166
        );
4✔
167
    }
168

169
    protected function getAttributes($object, $format = null, array $context = []): array
170
    {
171
        return $this->getComponents($object, $format, $context)['attributes'];
3✔
172
    }
173

174
    protected function setAttributeValue($object, $attribute, $value, $format = null, array $context = []): void
175
    {
176
        parent::setAttributeValue($object, $attribute, \is_array($value) && \array_key_exists('data', $value) ? $value['data'] : $value, $format, $context);
4✔
177
    }
178

179
    /**
180
     * @see http://jsonapi.org/format/#document-resource-object-linkage
181
     *
182
     * @param ApiProperty|PropertyMetadata $propertyMetadata
183
     *
184
     * @throws RuntimeException
185
     * @throws NotNormalizableValueException
186
     */
187
    protected function denormalizeRelation(string $attributeName, $propertyMetadata, string $className, $value, ?string $format, array $context)
188
    {
189
        if (!\is_array($value) || !isset($value['id'], $value['type'])) {
2✔
190
            throw new NotNormalizableValueException('Only resource linkage supported currently, see: http://jsonapi.org/format/#document-resource-object-linkage.');
1✔
191
        }
192

193
        try {
194
            return $this->iriConverter instanceof LegacyIriConverterInterface ? $this->iriConverter->getItemFromIri($value['id'], $context + ['fetch_data' => true]) : $this->iriConverter->getResourceFromIri($value['id'], $context + ['fetch_data' => true]);
1✔
195
        } catch (ItemNotFoundException $e) {
×
196
            throw new RuntimeException($e->getMessage(), $e->getCode(), $e);
×
197
        }
198
    }
199

200
    /**
201
     * @param ApiProperty|PropertyMetadata $propertyMetadata
202
     *
203
     * @see http://jsonapi.org/format/#document-resource-object-linkage
204
     */
205
    protected function normalizeRelation($propertyMetadata, $relatedObject, string $resourceClass, ?string $format, array $context)
206
    {
207
        if (null !== $relatedObject) {
×
208
            $iri = $this->iriConverter instanceof LegacyIriConverterInterface ? $this->iriConverter->getIriFromItem($relatedObject) : $this->iriConverter->getIriFromResource($relatedObject);
×
209
            $context['iri'] = $iri;
×
210

211
            if (isset($context['resources'])) {
×
212
                $context['resources'][$iri] = $iri;
×
213
            }
214
        }
215

216
        if (null === $relatedObject || isset($context['api_included'])) {
×
217
            if (!$this->serializer instanceof NormalizerInterface) {
×
218
                throw new LogicException(sprintf('The injected serializer must be an instance of "%s".', NormalizerInterface::class));
×
219
            }
220

221
            $normalizedRelatedObject = $this->serializer->normalize($relatedObject, $format, $context);
×
222
            if (!\is_string($normalizedRelatedObject) && !\is_array($normalizedRelatedObject) && !$normalizedRelatedObject instanceof \ArrayObject && null !== $normalizedRelatedObject) {
×
223
                throw new UnexpectedValueException('Expected normalized relation to be an IRI, array, \ArrayObject or null');
×
224
            }
225

226
            return $normalizedRelatedObject;
×
227
        }
228

229
        return [
×
230
            'data' => [
×
231
                'type' => $this->getResourceShortName($resourceClass),
×
232
                'id' => $iri,
×
233
            ],
×
234
        ];
×
235
    }
236

237
    protected function isAllowedAttribute($classOrObject, $attribute, $format = null, array $context = []): bool
238
    {
239
        return preg_match('/^\\w[-\\w_]*$/', $attribute) && parent::isAllowedAttribute($classOrObject, $attribute, $format, $context);
7✔
240
    }
241

242
    /**
243
     * Gets JSON API components of the resource: attributes, relationships, meta and links.
244
     *
245
     * @param object $object
246
     */
247
    private function getComponents($object, ?string $format, array $context): array
248
    {
249
        $cacheKey = $this->getObjectClass($object).'-'.$context['cache_key'];
3✔
250

251
        if (isset($this->componentsCache[$cacheKey])) {
3✔
252
            return $this->componentsCache[$cacheKey];
2✔
253
        }
254

255
        $attributes = parent::getAttributes($object, $format, $context);
3✔
256

257
        $options = $this->getFactoryOptions($context);
3✔
258

259
        $components = [
3✔
260
            'links' => [],
3✔
261
            'relationships' => [],
3✔
262
            'attributes' => [],
3✔
263
            'meta' => [],
3✔
264
        ];
3✔
265

266
        foreach ($attributes as $attribute) {
3✔
267
            /** @var ApiProperty|PropertyMetadata */
268
            $propertyMetadata = $this
3✔
269
                ->propertyMetadataFactory
3✔
270
                ->create($context['resource_class'], $attribute, $options);
3✔
271

272
            // TODO: 3.0 support multiple types, default value of types will be [] instead of null
273
            $type = $propertyMetadata instanceof PropertyMetadata ? $propertyMetadata->getType() : ($propertyMetadata->getBuiltinTypes()[0] ?? null);
3✔
274
            $isOne = $isMany = false;
3✔
275

276
            if (null !== $type) {
3✔
277
                if ($type->isCollection()) {
×
278
                    $collectionValueType = method_exists(Type::class, 'getCollectionValueTypes') ? ($type->getCollectionValueTypes()[0] ?? null) : $type->getCollectionValueType();
×
279
                    $isMany = ($collectionValueType && $className = $collectionValueType->getClassName()) ? $this->resourceClassResolver->isResourceClass($className) : false;
×
280
                } else {
281
                    $isOne = ($className = $type->getClassName()) ? $this->resourceClassResolver->isResourceClass($className) : false;
×
282
                }
283
            }
284

285
            if (!isset($className) || !$isOne && !$isMany) {
3✔
286
                $components['attributes'][] = $attribute;
3✔
287

288
                continue;
3✔
289
            }
290

291
            $relation = [
×
292
                'name' => $attribute,
×
293
                'type' => $this->getResourceShortName($className),
×
294
                'cardinality' => $isOne ? 'one' : 'many',
×
295
            ];
×
296

297
            $components['relationships'][] = $relation;
×
298
        }
299

300
        if (false !== $context['cache_key']) {
3✔
301
            $this->componentsCache[$cacheKey] = $components;
3✔
302
        }
303

304
        return $components;
3✔
305
    }
306

307
    /**
308
     * Populates relationships keys.
309
     *
310
     * @param object $object
311
     *
312
     * @throws UnexpectedValueException
313
     */
314
    private function getPopulatedRelations($object, ?string $format, array $context, array $relationships): array
315
    {
316
        $data = [];
2✔
317

318
        if (!isset($context['resource_class'])) {
2✔
319
            return $data;
×
320
        }
321

322
        unset($context['api_included']);
2✔
323
        foreach ($relationships as $relationshipDataArray) {
2✔
324
            $relationshipName = $relationshipDataArray['name'];
×
325

326
            $attributeValue = $this->getAttributeValue($object, $relationshipName, $format, $context);
×
327

328
            if ($this->nameConverter) {
×
329
                $relationshipName = $this->nameConverter->normalize($relationshipName, $context['resource_class'], self::FORMAT, $context);
×
330
            }
331

332
            if (!$attributeValue) {
×
333
                continue;
×
334
            }
335

336
            $data[$relationshipName] = [
×
337
                'data' => [],
×
338
            ];
×
339

340
            // Many to one relationship
341
            if ('one' === $relationshipDataArray['cardinality']) {
×
342
                unset($attributeValue['data']['attributes']);
×
343
                $data[$relationshipName] = $attributeValue;
×
344

345
                continue;
×
346
            }
347

348
            // Many to many relationship
349
            foreach ($attributeValue as $attributeValueElement) {
×
350
                if (!isset($attributeValueElement['data'])) {
×
351
                    throw new UnexpectedValueException(sprintf('The JSON API attribute \'%s\' must contain a "data" key.', $relationshipName));
×
352
                }
353
                unset($attributeValueElement['data']['attributes']);
×
354
                $data[$relationshipName]['data'][] = $attributeValueElement['data'];
×
355
            }
356
        }
357

358
        return $data;
2✔
359
    }
360

361
    /**
362
     * Populates included keys.
363
     */
364
    private function getRelatedResources($object, ?string $format, array $context, array $relationships): array
365
    {
366
        if (!isset($context['api_included'])) {
2✔
367
            return [];
2✔
368
        }
369

370
        $included = [];
×
371
        foreach ($relationships as $relationshipDataArray) {
×
372
            $relationshipName = $relationshipDataArray['name'];
×
373

374
            if (!$this->shouldIncludeRelation($relationshipName, $context)) {
×
375
                continue;
×
376
            }
377

378
            $relationContext = $context;
×
379
            $relationContext['api_included'] = $this->getIncludedNestedResources($relationshipName, $context);
×
380

381
            $attributeValue = $this->getAttributeValue($object, $relationshipName, $format, $relationContext);
×
382

383
            if (!$attributeValue) {
×
384
                continue;
×
385
            }
386

387
            // Many to many relationship
388
            $attributeValues = $attributeValue;
×
389
            // Many to one relationship
390
            if ('one' === $relationshipDataArray['cardinality']) {
×
391
                $attributeValues = [$attributeValue];
×
392
            }
393

394
            foreach ($attributeValues as $attributeValueElement) {
×
395
                if (isset($attributeValueElement['data'])) {
×
396
                    $this->addIncluded($attributeValueElement['data'], $included, $context);
×
397
                    if (isset($attributeValueElement['included']) && \is_array($attributeValueElement['included'])) {
×
398
                        foreach ($attributeValueElement['included'] as $include) {
×
399
                            $this->addIncluded($include, $included, $context);
×
400
                        }
401
                    }
402
                }
403
            }
404
        }
405

406
        return $included;
×
407
    }
408

409
    /**
410
     * Add data to included array if it's not already included.
411
     */
412
    private function addIncluded(array $data, array &$included, array &$context): void
413
    {
414
        if (isset($data['id']) && !\in_array($data['id'], $context['api_included_resources'], true)) {
×
415
            $included[] = $data;
×
416
            // Track already included resources
417
            $context['api_included_resources'][] = $data['id'];
×
418
        }
419
    }
420

421
    /**
422
     * Figures out if the relationship is in the api_included hash or has included nested resources (path).
423
     */
424
    private function shouldIncludeRelation(string $relationshipName, array $context): bool
425
    {
426
        $normalizedName = $this->nameConverter ? $this->nameConverter->normalize($relationshipName, $context['resource_class'], self::FORMAT, $context) : $relationshipName;
×
427

428
        return \in_array($normalizedName, $context['api_included'], true) || \count($this->getIncludedNestedResources($relationshipName, $context)) > 0;
×
429
    }
430

431
    /**
432
     * Returns the names of the nested resources from a path relationship.
433
     */
434
    private function getIncludedNestedResources(string $relationshipName, array $context): array
435
    {
436
        $normalizedName = $this->nameConverter ? $this->nameConverter->normalize($relationshipName, $context['resource_class'], self::FORMAT, $context) : $relationshipName;
×
437

438
        $filtered = array_filter($context['api_included'] ?? [], static function (string $included) use ($normalizedName) {
×
439
            return str_starts_with($included, $normalizedName.'.');
×
440
        });
×
441

442
        return array_map(static function (string $nested) {
×
443
            return substr($nested, strpos($nested, '.') + 1);
×
444
        }, $filtered);
×
445
    }
446

447
    // TODO: 3.0 remove
448
    private function getResourceShortName(string $resourceClass): string
449
    {
450
        /** @var ResourceMetadata|ResourceMetadataCollection */
451
        $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
2✔
452

453
        if ($resourceMetadata instanceof ResourceMetadata) {
2✔
454
            return $resourceMetadata->getShortName();
2✔
455
        }
456

457
        return $resourceMetadata->getOperation()->getShortName();
×
458
    }
459
}
460

461
class_alias(ItemNormalizer::class, \ApiPlatform\Core\JsonApi\Serializer\ItemNormalizer::class);
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc