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

api-platform / core / 10903050455

17 Sep 2024 12:29PM UTC coverage: 7.684% (+0.7%) from 6.96%
10903050455

push

github

web-flow
fix: swagger ui with route identifier (#6616)

2 of 6 new or added lines in 6 files covered. (33.33%)

9000 existing lines in 286 files now uncovered.

12668 of 164863 relevant lines covered (7.68%)

22.93 hits per line

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

96.8
/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\Metadata\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);
158✔
47
    }
48

49
    public function getSupportedTypes($format): array
50
    {
51
        return self::FORMAT === $format ? parent::getSupportedTypes($format) : [];
2,373✔
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);
160✔
60
        if ($this->getOutputClass($context)) {
160✔
61
            return parent::normalize($object, $format, $context);
9✔
62
        }
63

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

69
        $context = $this->initContext($resourceClass, $context);
160✔
70

71
        $iri = $context['iri'] ??= $this->iriConverter->getIriFromResource($object, UrlGeneratorInterface::ABS_PATH, $context['operation'] ?? null, $context);
160✔
72
        $context['object'] = $object;
160✔
73
        $context['format'] = $format;
160✔
74
        $context['api_normalize'] = true;
160✔
75

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

80
        $data = parent::normalize($object, $format, $context);
160✔
81

82
        if (!\is_array($data)) {
160✔
83
            return $data;
×
84
        }
85

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

97
        return $metadata + $data;
157✔
98
    }
99

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

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

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

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

134
        if (isset($this->componentsCache[$cacheKey])) {
160✔
135
            return $this->componentsCache[$cacheKey];
157✔
136
        }
137

138
        $attributes = parent::getAttributes($object, $format, $context);
160✔
139
        $options = $this->getFactoryOptions($context);
160✔
140

141
        $components = [
160✔
142
            'states' => [],
160✔
143
            'links' => [],
160✔
144
            'embedded' => [],
160✔
145
        ];
160✔
146

147
        foreach ($attributes as $attribute) {
160✔
148
            $propertyMetadata = $this->propertyMetadataFactory->create($context['resource_class'], $attribute, $options);
157✔
149

150
            $types = $propertyMetadata->getBuiltinTypes() ?? [];
157✔
151

152
            // prevent declaring $attribute as attribute if it's already declared as relationship
153
            $isRelationship = false;
157✔
154

155
            foreach ($types as $type) {
157✔
156
                $isOne = $isMany = false;
151✔
157

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

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

173
                $relation = ['name' => $attribute, 'cardinality' => $isOne ? 'one' : 'many', 'iri' => null, 'operation' => null];
105✔
174

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

UNCOV
181
                    $operation = $this->resourceMetadataCollectionFactory->create($className)->getOperation(
3✔
UNCOV
182
                        operationName: $uriTemplate,
3✔
UNCOV
183
                        httpOperation: true
3✔
UNCOV
184
                    );
3✔
185

UNCOV
186
                    $relation['iri'] = $this->iriConverter->getIriFromResource($object, UrlGeneratorInterface::ABS_PATH, $operation, $childContext);
3✔
UNCOV
187
                    $relation['operation'] = $operation;
3✔
UNCOV
188
                    $cacheKey = null;
3✔
189
                }
190

191
                if ($propertyMetadata->isReadableLink()) {
105✔
192
                    $components['embedded'][] = $relation;
31✔
193
                }
194

195
                $components['links'][] = $relation;
105✔
196
                $isRelationship = true;
105✔
197
            }
198

199
            // if all types are not relationships, declare it as an attribute
200
            if (!$isRelationship) {
157✔
201
                $components['states'][] = $attribute;
157✔
202
            }
203
        }
204

205
        if ($cacheKey && false !== $context['cache_key']) {
160✔
206
            $this->componentsCache[$cacheKey] = $components;
157✔
207
        }
208

209
        return $components;
160✔
210
    }
211

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

219
        $attributesMetadata = \array_key_exists($class, $this->attributesMetadataCache) ?
160✔
220
            $this->attributesMetadataCache[$class] :
157✔
221
            $this->attributesMetadataCache[$class] = $this->classMetadataFactory ? $this->classMetadataFactory->getMetadataFor($class)->getAttributesMetadata() : null;
160✔
222

223
        $key = '_'.$type;
160✔
224
        foreach ($components[$type] as $relation) {
160✔
225
            if (null !== $attributesMetadata && $this->isMaxDepthReached($attributesMetadata, $class, $relation['name'], $context)) {
105✔
226
                continue;
12✔
227
            }
228

229
            $relationName = $relation['name'];
105✔
230
            if ($this->nameConverter) {
105✔
231
                $relationName = $this->nameConverter->normalize($relationName, $class, $format, $context);
102✔
232
            }
233

234
            // if we specify the uriTemplate, then the link takes the uriTemplate defined.
235
            if ('links' === $type && $iri = $relation['iri']) {
105✔
UNCOV
236
                $data[$key][$relationName]['href'] = $iri;
3✔
UNCOV
237
                continue;
3✔
238
            }
239

240
            $childContext = $this->createChildContext($context, $relationName, $format);
105✔
241
            unset($childContext['iri'], $childContext['uri_variables'], $childContext['operation'], $childContext['operation_name']);
105✔
242

243
            if ($operation = $relation['operation']) {
105✔
UNCOV
244
                $childContext['operation'] = $operation;
3✔
UNCOV
245
                $childContext['operation_name'] = $operation->getName();
3✔
246
            }
247

248
            $attributeValue = $this->getAttributeValue($object, $relation['name'], $format, $childContext);
105✔
249

250
            if (empty($attributeValue)) {
102✔
251
                continue;
62✔
252
            }
253

254
            if ('one' === $relation['cardinality']) {
68✔
255
                if ('links' === $type) {
58✔
256
                    $data[$key][$relationName]['href'] = $this->getRelationIri($attributeValue);
55✔
257
                    continue;
55✔
258
                }
259

260
                $data[$key][$relationName] = $attributeValue;
25✔
261
                continue;
25✔
262
            }
263

264
            // many
UNCOV
265
            $data[$key][$relationName] = [];
25✔
UNCOV
266
            foreach ($attributeValue as $rel) {
25✔
UNCOV
267
                if ('links' === $type) {
25✔
UNCOV
268
                    $rel = ['href' => $this->getRelationIri($rel)];
22✔
269
                }
270

UNCOV
271
                $data[$key][$relationName][] = $rel;
25✔
272
            }
273
        }
274

275
        return $data;
157✔
276
    }
277

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

289
        return \is_string($rel) ? $rel : $rel['_links']['self']['href'];
65✔
290
    }
291

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

307
        $key = \sprintf(self::DEPTH_KEY_PATTERN, $class, $attribute);
12✔
308
        if (!isset($context[$key])) {
12✔
309
            $context[$key] = 1;
12✔
310

311
            return false;
12✔
312
        }
313

314
        if ($context[$key] === $maxDepth) {
12✔
315
            return true;
12✔
316
        }
317

318
        ++$context[$key];
×
319

320
        return false;
×
321
    }
322
}
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