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

api-platform / core / 20847864477

09 Jan 2026 09:47AM UTC coverage: 29.1% (+0.005%) from 29.095%
20847864477

Pull #7649

github

web-flow
Merge b342dd5db into d640d106b
Pull Request #7649: feat(validator): uuid/ulid parameter validation

0 of 4 new or added lines in 1 file covered. (0.0%)

15050 existing lines in 491 files now uncovered.

16996 of 58406 relevant lines covered (29.1%)

81.8 hits per line

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

98.0
/src/GraphQl/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\GraphQl\Serializer;
15

16
use ApiPlatform\GraphQl\State\Provider\NoopProvider;
17
use ApiPlatform\Metadata\ApiProperty;
18
use ApiPlatform\Metadata\GraphQl\Query;
19
use ApiPlatform\Metadata\IdentifiersExtractorInterface;
20
use ApiPlatform\Metadata\IriConverterInterface;
21
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
22
use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
23
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
24
use ApiPlatform\Metadata\ResourceAccessCheckerInterface;
25
use ApiPlatform\Metadata\ResourceClassResolverInterface;
26
use ApiPlatform\Metadata\Util\ClassInfoTrait;
27
use ApiPlatform\Serializer\CacheKeyTrait;
28
use ApiPlatform\Serializer\ItemNormalizer as BaseItemNormalizer;
29
use Psr\Log\LoggerInterface;
30
use Psr\Log\NullLogger;
31
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
32
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
33
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;
34
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
35

36
/**
37
 * GraphQL normalizer.
38
 *
39
 * @author Kévin Dunglas <dunglas@gmail.com>
40
 */
41
final class ItemNormalizer extends BaseItemNormalizer
42
{
43
    use CacheKeyTrait;
44
    use ClassInfoTrait;
45

46
    public const FORMAT = 'graphql';
47
    public const ITEM_RESOURCE_CLASS_KEY = '#itemResourceClass';
48
    public const ITEM_IDENTIFIERS_KEY = '#itemIdentifiers';
49

50
    private array $safeCacheKeysCache = [];
51

52
    public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, IriConverterInterface $iriConverter, private readonly IdentifiersExtractorInterface $identifiersExtractor, ResourceClassResolverInterface $resourceClassResolver, ?PropertyAccessorInterface $propertyAccessor = null, ?NameConverterInterface $nameConverter = null, ?ClassMetadataFactoryInterface $classMetadataFactory = null, ?LoggerInterface $logger = null, ?ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory = null, ?ResourceAccessCheckerInterface $resourceAccessChecker = null)
53
    {
UNCOV
54
        parent::__construct($propertyNameCollectionFactory, $propertyMetadataFactory, $iriConverter, $resourceClassResolver, $propertyAccessor, $nameConverter, $classMetadataFactory, $logger ?: new NullLogger(), $resourceMetadataCollectionFactory, $resourceAccessChecker);
2,409✔
55
    }
56

57
    /**
58
     * {@inheritdoc}
59
     */
60
    public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool
61
    {
UNCOV
62
        return self::FORMAT === $format && parent::supportsNormalization($data, $format, $context);
217✔
63
    }
64

65
    /**
66
     * {@inheritdoc}
67
     */
68
    public function getSupportedTypes(?string $format): array
69
    {
UNCOV
70
        return self::FORMAT === $format ? parent::getSupportedTypes($format) : [];
2,102✔
71
    }
72

73
    /**
74
     * {@inheritdoc}
75
     *
76
     * @param array<string, mixed> $context
77
     *
78
     * @throws UnexpectedValueException
79
     */
80
    public function normalize(mixed $data, ?string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null
81
    {
UNCOV
82
        $resourceClass = $this->getObjectClass($data);
217✔
83

UNCOV
84
        if ($this->getOutputClass($context)) {
217✔
85
            $context['graphql_identifiers'] = [
6✔
86
                self::ITEM_RESOURCE_CLASS_KEY => $context['operation']->getClass(),
6✔
87
                self::ITEM_IDENTIFIERS_KEY => $this->identifiersExtractor->getIdentifiersFromItem($data, $context['operation'] ?? null),
6✔
88
            ];
6✔
89

90
            return parent::normalize($data, $format, $context);
6✔
91
        }
92

UNCOV
93
        if ($this->isCacheKeySafe($context)) {
217✔
UNCOV
94
            $context['cache_key'] = $this->getCacheKey($format, $context);
185✔
95
        } else {
UNCOV
96
            $context['cache_key'] = false;
50✔
97
        }
98

UNCOV
99
        unset($context['operation_name'], $context['operation']); // Remove operation and operation_name only when cache key has been created
217✔
UNCOV
100
        $normalizedData = parent::normalize($data, $format, $context);
217✔
UNCOV
101
        if (!\is_array($normalizedData)) {
217✔
102
            throw new UnexpectedValueException('Expected data to be an array.');
×
103
        }
104

UNCOV
105
        if (isset($context['graphql_identifiers'])) {
217✔
106
            $normalizedData += $context['graphql_identifiers'];
6✔
UNCOV
107
        } elseif (!($context['no_resolver_data'] ?? false)) {
213✔
UNCOV
108
            $normalizedData[self::ITEM_RESOURCE_CLASS_KEY] = $resourceClass;
211✔
UNCOV
109
            $normalizedData[self::ITEM_IDENTIFIERS_KEY] = $this->identifiersExtractor->getIdentifiersFromItem($data, $context['operation'] ?? null);
211✔
110
        }
111

UNCOV
112
        return $normalizedData;
217✔
113
    }
114

115
    /**
116
     * {@inheritdoc}
117
     */
118
    protected function normalizeCollectionOfRelations(ApiProperty $propertyMetadata, iterable $attributeValue, string $resourceClass, ?string $format, array $context): array
119
    {
120
        // check for nested collection
121
        $operation = $this->resourceMetadataCollectionFactory?->create($resourceClass)->getOperation(forceCollection: true, forceGraphQl: true);
41✔
122
        if ($operation instanceof Query && $operation->getNested() && !$operation->getResolver() && (!$operation->getProvider() || NoopProvider::class === $operation->getProvider())) {
41✔
123
            return [...$attributeValue];
4✔
124
        }
125

126
        // to-many are handled directly by the GraphQL resolver
127
        return [];
41✔
128
    }
129

130
    /**
131
     * {@inheritdoc}
132
     */
133
    public function supportsDenormalization(mixed $data, string $type, ?string $format = null, array $context = []): bool
134
    {
135
        return self::FORMAT === $format && parent::supportsDenormalization($data, $type, $format, $context);
65✔
136
    }
137

138
    /**
139
     * {@inheritdoc}
140
     */
141
    protected function getAllowedAttributes(string|object $classOrObject, array $context, bool $attributesAsString = false): array|bool
142
    {
UNCOV
143
        $allowedAttributes = parent::getAllowedAttributes($classOrObject, $context, $attributesAsString);
225✔
144

UNCOV
145
        if (($context['api_denormalize'] ?? false) && \is_array($allowedAttributes) && false !== ($indexId = array_search('id', $allowedAttributes, true))) {
225✔
146
            $allowedAttributes[] = '_id';
9✔
147
            array_splice($allowedAttributes, (int) $indexId, 1);
9✔
148
        }
149

UNCOV
150
        return $allowedAttributes;
225✔
151
    }
152

153
    /**
154
     * {@inheritdoc}
155
     */
156
    protected function setAttributeValue(object $object, string $attribute, mixed $value, ?string $format = null, array $context = []): void
157
    {
158
        if ('_id' === $attribute) {
50✔
159
            $attribute = 'id';
3✔
160
        }
161

162
        parent::setAttributeValue($object, $attribute, $value, $format, $context);
50✔
163
    }
164

165
    /**
166
     * Check if any property contains a security grants, which makes the cache key not safe,
167
     * as allowed_properties can differ for 2 instances of the same object.
168
     */
169
    private function isCacheKeySafe(array $context): bool
170
    {
UNCOV
171
        if (!isset($context['resource_class']) || !$this->resourceClassResolver->isResourceClass($context['resource_class'])) {
217✔
172
            return false;
8✔
173
        }
UNCOV
174
        $resourceClass = $this->resourceClassResolver->getResourceClass(null, $context['resource_class']);
213✔
UNCOV
175
        if (isset($this->safeCacheKeysCache[$resourceClass])) {
213✔
UNCOV
176
            return $this->safeCacheKeysCache[$resourceClass];
70✔
177
        }
UNCOV
178
        $options = $this->getFactoryOptions($context);
213✔
UNCOV
179
        $propertyNames = $this->propertyNameCollectionFactory->create($resourceClass, $options);
213✔
180

UNCOV
181
        $this->safeCacheKeysCache[$resourceClass] = true;
213✔
UNCOV
182
        foreach ($propertyNames as $propertyName) {
213✔
UNCOV
183
            $propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $propertyName, $options);
213✔
UNCOV
184
            if (null !== $propertyMetadata->getSecurity()) {
213✔
UNCOV
185
                $this->safeCacheKeysCache[$resourceClass] = false;
42✔
UNCOV
186
                break;
42✔
187
            }
188
        }
189

UNCOV
190
        return $this->safeCacheKeysCache[$resourceClass];
213✔
191
    }
192
}
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