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

api-platform / core / 6148660584

11 Sep 2023 03:40PM UTC coverage: 37.077% (-0.1%) from 37.185%
6148660584

push

github

soyuka
chore(symfony): security after validate when validator installed

10090 of 27214 relevant lines covered (37.08%)

19.39 hits per line

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

80.33
/src/Hal/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\Hal\Serializer;
15

16
use ApiPlatform\Api\UrlGeneratorInterface;
17
use ApiPlatform\Metadata\Util\ClassInfoTrait;
18
use ApiPlatform\Serializer\AbstractItemNormalizer;
19
use ApiPlatform\Serializer\CacheKeyTrait;
20
use ApiPlatform\Serializer\ContextTrait;
21
use Symfony\Component\Serializer\Exception\LogicException;
22
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
23
use Symfony\Component\Serializer\Mapping\AttributeMetadataInterface;
24

25
/**
26
 * Converts between objects and array including HAL metadata.
27
 *
28
 * @author Kévin Dunglas <dunglas@gmail.com>
29
 */
30
final class ItemNormalizer extends AbstractItemNormalizer
31
{
32
    use CacheKeyTrait;
33
    use ClassInfoTrait;
34
    use ContextTrait;
35

36
    public const FORMAT = 'jsonhal';
37

38
    private array $componentsCache = [];
39
    private array $attributesMetadataCache = [];
40

41
    /**
42
     * {@inheritdoc}
43
     */
44
    public function supportsNormalization(mixed $data, string $format = null, array $context = []): bool
45
    {
46
        return self::FORMAT === $format && parent::supportsNormalization($data, $format, $context);
6✔
47
    }
48

49
    public function getSupportedTypes($format): array
50
    {
51
        return self::FORMAT === $format ? parent::getSupportedTypes($format) : [];
57✔
52
    }
53

54
    /**
55
     * {@inheritdoc}
56
     */
57
    public function normalize(mixed $object, string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null
58
    {
59
        $resourceClass = $this->getObjectClass($object);
12✔
60
        if ($this->getOutputClass($context)) {
12✔
61
            return parent::normalize($object, $format, $context);
×
62
        }
63

64
        if ($this->resourceClassResolver->isResourceClass($resourceClass)) {
12✔
65
            $resourceClass = $this->resourceClassResolver->getResourceClass($object, $context['resource_class'] ?? null);
12✔
66
        }
67

68
        $context = $this->initContext($resourceClass, $context);
12✔
69
        $iri = $this->iriConverter->getIriFromResource($object, UrlGeneratorInterface::ABS_PATH, $context['operation'] ?? null, $context);
12✔
70

71
        $context['iri'] = $iri;
12✔
72
        $context['api_normalize'] = true;
12✔
73

74
        if (!isset($context['cache_key'])) {
12✔
75
            $context['cache_key'] = $this->getCacheKey($format, $context);
12✔
76
        }
77

78
        $data = parent::normalize($object, $format, $context);
12✔
79

80
        if (!\is_array($data)) {
12✔
81
            return $data;
×
82
        }
83

84
        $metadata = [
12✔
85
            '_links' => [
12✔
86
                'self' => [
12✔
87
                    'href' => $iri,
12✔
88
                ],
12✔
89
            ],
12✔
90
        ];
12✔
91
        $components = $this->getComponents($object, $format, $context);
12✔
92
        $metadata = $this->populateRelation($metadata, $object, $format, $context, $components, 'links');
12✔
93
        $metadata = $this->populateRelation($metadata, $object, $format, $context, $components, 'embedded');
12✔
94

95
        return $metadata + $data;
12✔
96
    }
97

98
    /**
99
     * {@inheritdoc}
100
     */
101
    public function supportsDenormalization(mixed $data, string $type, string $format = null, array $context = []): bool
102
    {
103
        // prevent the use of lower priority normalizers (e.g. serializer.normalizer.object) for this format
104
        return self::FORMAT === $format;
3✔
105
    }
106

107
    /**
108
     * {@inheritdoc}
109
     *
110
     * @throws LogicException
111
     */
112
    public function denormalize(mixed $data, string $type, string $format = null, array $context = []): never
113
    {
114
        throw new LogicException(sprintf('%s is a read-only format.', self::FORMAT));
3✔
115
    }
116

117
    /**
118
     * {@inheritdoc}
119
     */
120
    protected function getAttributes($object, $format = null, array $context = []): array
121
    {
122
        return $this->getComponents($object, $format, $context)['states'];
12✔
123
    }
124

125
    /**
126
     * Gets HAL components of the resource: states, links and embedded.
127
     */
128
    private function getComponents(object $object, ?string $format, array $context): array
129
    {
130
        $cacheKey = $this->getObjectClass($object).'-'.$context['cache_key'];
12✔
131

132
        if (isset($this->componentsCache[$cacheKey])) {
12✔
133
            return $this->componentsCache[$cacheKey];
9✔
134
        }
135

136
        $attributes = parent::getAttributes($object, $format, $context);
12✔
137
        $options = $this->getFactoryOptions($context);
12✔
138

139
        $components = [
12✔
140
            'states' => [],
12✔
141
            'links' => [],
12✔
142
            'embedded' => [],
12✔
143
        ];
12✔
144

145
        foreach ($attributes as $attribute) {
12✔
146
            $propertyMetadata = $this->propertyMetadataFactory->create($context['resource_class'], $attribute, $options);
12✔
147

148
            $types = $propertyMetadata->getBuiltinTypes() ?? [];
12✔
149

150
            // prevent declaring $attribute as attribute if it's already declared as relationship
151
            $isRelationship = false;
12✔
152

153
            foreach ($types as $type) {
12✔
154
                $isOne = $isMany = false;
12✔
155

156
                if (null !== $type) {
12✔
157
                    if ($type->isCollection()) {
12✔
158
                        $valueType = $type->getCollectionValueTypes()[0] ?? null;
×
159
                        $isMany = null !== $valueType && ($className = $valueType->getClassName()) && $this->resourceClassResolver->isResourceClass($className);
×
160
                    } else {
161
                        $className = $type->getClassName();
12✔
162
                        $isOne = $className && $this->resourceClassResolver->isResourceClass($className);
12✔
163
                    }
164
                }
165

166
                if (!$isOne && !$isMany) {
12✔
167
                    // don't declare it as an attribute too quick: maybe the next type is a valid resource
168
                    continue;
12✔
169
                }
170

171
                $relation = ['name' => $attribute, 'cardinality' => $isOne ? 'one' : 'many', 'iri' => null, 'operation' => null];
9✔
172

173
                // if we specify the uriTemplate, generates its value for link definition
174
                // @see ApiPlatform\Serializer\AbstractItemNormalizer:getAttributeValue logic for intentional duplicate content
175
                if (($className ?? false) && $uriTemplate = $propertyMetadata->getUriTemplate()) {
9✔
176
                    $childContext = $this->createChildContext($context, $attribute, $format);
×
177
                    unset($childContext['iri'], $childContext['uri_variables'], $childContext['resource_class'], $childContext['operation'], $childContext['operation_name']);
×
178

179
                    $operation = $this->resourceMetadataCollectionFactory->create($className)->getOperation(
×
180
                        operationName: $uriTemplate,
×
181
                        httpOperation: true
×
182
                    );
×
183

184
                    $relation['iri'] = $this->iriConverter->getIriFromResource($object, UrlGeneratorInterface::ABS_PATH, $operation, $childContext);
×
185
                    $relation['operation'] = $operation;
×
186
                }
187

188
                if ($propertyMetadata->isReadableLink()) {
9✔
189
                    $components['embedded'][] = $relation;
3✔
190
                }
191

192
                $components['links'][] = $relation;
9✔
193
                $isRelationship = true;
9✔
194
            }
195

196
            // if all types are not relationships, declare it as an attribute
197
            if (!$isRelationship) {
12✔
198
                $components['states'][] = $attribute;
12✔
199
            }
200
        }
201

202
        if (false !== $context['cache_key']) {
12✔
203
            $this->componentsCache[$cacheKey] = $components;
9✔
204
        }
205

206
        return $components;
12✔
207
    }
208

209
    /**
210
     * Populates _links and _embedded keys.
211
     */
212
    private function populateRelation(array $data, object $object, ?string $format, array $context, array $components, string $type): array
213
    {
214
        $class = $this->getObjectClass($object);
12✔
215

216
        $attributesMetadata = \array_key_exists($class, $this->attributesMetadataCache) ?
12✔
217
            $this->attributesMetadataCache[$class] :
12✔
218
            $this->attributesMetadataCache[$class] = $this->classMetadataFactory ? $this->classMetadataFactory->getMetadataFor($class)->getAttributesMetadata() : null;
12✔
219

220
        $key = '_'.$type;
12✔
221
        foreach ($components[$type] as $relation) {
12✔
222
            if (null !== $attributesMetadata && $this->isMaxDepthReached($attributesMetadata, $class, $relation['name'], $context)) {
9✔
223
                continue;
3✔
224
            }
225

226
            $relationName = $relation['name'];
9✔
227
            if ($this->nameConverter) {
9✔
228
                $relationName = $this->nameConverter->normalize($relationName, $class, $format, $context);
6✔
229
            }
230

231
            // if we specify the uriTemplate, then the link takes the uriTemplate defined.
232
            if ('links' === $type && $iri = $relation['iri']) {
9✔
233
                $data[$key][$relationName]['href'] = $iri;
×
234
                continue;
×
235
            }
236

237
            $childContext = $this->createChildContext($context, $relationName, $format);
9✔
238
            unset($childContext['iri'], $childContext['uri_variables'], $childContext['operation'], $childContext['operation_name']);
9✔
239

240
            if ($operation = $relation['operation']) {
9✔
241
                $childContext['operation'] = $operation;
×
242
                $childContext['operation_name'] = $operation->getName();
×
243
            }
244

245
            $attributeValue = $this->getAttributeValue($object, $relation['name'], $format, $childContext);
9✔
246

247
            if (empty($attributeValue)) {
9✔
248
                continue;
3✔
249
            }
250

251
            if ('one' === $relation['cardinality']) {
9✔
252
                if ('links' === $type) {
9✔
253
                    $data[$key][$relationName]['href'] = $this->getRelationIri($attributeValue);
9✔
254
                    continue;
9✔
255
                }
256

257
                $data[$key][$relationName] = $attributeValue;
3✔
258
                continue;
3✔
259
            }
260

261
            // many
262
            $data[$key][$relationName] = [];
×
263
            foreach ($attributeValue as $rel) {
×
264
                if ('links' === $type) {
×
265
                    $rel = ['href' => $this->getRelationIri($rel)];
×
266
                }
267

268
                $data[$key][$relationName][] = $rel;
×
269
            }
270
        }
271

272
        return $data;
12✔
273
    }
274

275
    /**
276
     * Gets the IRI of the given relation.
277
     *
278
     * @throws UnexpectedValueException
279
     */
280
    private function getRelationIri(mixed $rel): string
281
    {
282
        if (!(\is_array($rel) || \is_string($rel))) {
9✔
283
            throw new UnexpectedValueException('Expected relation to be an IRI or array');
×
284
        }
285

286
        return \is_string($rel) ? $rel : $rel['_links']['self']['href'];
9✔
287
    }
288

289
    /**
290
     * Is the max depth reached for the given attribute?
291
     *
292
     * @param AttributeMetadataInterface[] $attributesMetadata
293
     */
294
    private function isMaxDepthReached(array $attributesMetadata, string $class, string $attribute, array &$context): bool
295
    {
296
        if (
297
            !($context[self::ENABLE_MAX_DEPTH] ?? false)
3✔
298
            || !isset($attributesMetadata[$attribute])
3✔
299
            || null === $maxDepth = $attributesMetadata[$attribute]->getMaxDepth()
3✔
300
        ) {
301
            return false;
3✔
302
        }
303

304
        $key = sprintf(self::DEPTH_KEY_PATTERN, $class, $attribute);
3✔
305
        if (!isset($context[$key])) {
3✔
306
            $context[$key] = 1;
3✔
307

308
            return false;
3✔
309
        }
310

311
        if ($context[$key] === $maxDepth) {
3✔
312
            return true;
3✔
313
        }
314

315
        ++$context[$key];
×
316

317
        return false;
×
318
    }
319
}
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

© 2025 Coveralls, Inc