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

api-platform / core / 3713134090

pending completion
3713134090

Pull #5254

github

GitHub
Merge b2ec54b3c into ac711530f
Pull Request #5254: [OpenApi] Add ApiResource::openapi and deprecate openapiContext

197 of 197 new or added lines in 5 files covered. (100.0%)

10372 of 12438 relevant lines covered (83.39%)

11.97 hits per line

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

88.0
/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\Serializer\AbstractItemNormalizer;
18
use ApiPlatform\Serializer\CacheKeyTrait;
19
use ApiPlatform\Serializer\ContextTrait;
20
use ApiPlatform\Util\ClassInfoTrait;
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);
15✔
47
    }
48

49
    /**
50
     * {@inheritdoc}
51
     */
52
    public function normalize(mixed $object, string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null
53
    {
54
        $resourceClass = $this->getObjectClass($object);
3✔
55
        if ($this->getOutputClass($context)) {
3✔
56
            return parent::normalize($object, $format, $context);
×
57
        }
58

59
        if (!isset($context['cache_key'])) {
3✔
60
            $context['cache_key'] = $this->getCacheKey($format, $context);
3✔
61
        }
62

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

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

72
        $data = parent::normalize($object, $format, $context);
3✔
73
        if (!\is_array($data)) {
3✔
74
            return $data;
×
75
        }
76

77
        $metadata = [
3✔
78
            '_links' => [
3✔
79
                'self' => [
3✔
80
                    'href' => $iri,
3✔
81
                ],
3✔
82
            ],
3✔
83
        ];
3✔
84
        $components = $this->getComponents($object, $format, $context);
3✔
85
        $metadata = $this->populateRelation($metadata, $object, $format, $context, $components, 'links');
3✔
86
        $metadata = $this->populateRelation($metadata, $object, $format, $context, $components, 'embedded');
3✔
87

88
        return $metadata + $data;
3✔
89
    }
90

91
    /**
92
     * {@inheritdoc}
93
     */
94
    public function supportsDenormalization(mixed $data, string $type, string $format = null, array $context = []): bool
95
    {
96
        // prevent the use of lower priority normalizers (e.g. serializer.normalizer.object) for this format
97
        return self::FORMAT === $format;
4✔
98
    }
99

100
    /**
101
     * {@inheritdoc}
102
     *
103
     * @throws LogicException
104
     */
105
    public function denormalize(mixed $data, string $type, string $format = null, array $context = []): never
106
    {
107
        throw new LogicException(sprintf('%s is a read-only format.', self::FORMAT));
1✔
108
    }
109

110
    /**
111
     * {@inheritdoc}
112
     */
113
    protected function getAttributes($object, $format = null, array $context = []): array
114
    {
115
        return $this->getComponents($object, $format, $context)['states'];
3✔
116
    }
117

118
    /**
119
     * Gets HAL components of the resource: states, links and embedded.
120
     */
121
    private function getComponents(object $object, ?string $format, array $context): array
122
    {
123
        $cacheKey = $this->getObjectClass($object).'-'.$context['cache_key'];
3✔
124

125
        if (isset($this->componentsCache[$cacheKey])) {
3✔
126
            return $this->componentsCache[$cacheKey];
2✔
127
        }
128

129
        $attributes = parent::getAttributes($object, $format, $context);
3✔
130
        $options = $this->getFactoryOptions($context);
3✔
131

132
        $components = [
3✔
133
            'states' => [],
3✔
134
            'links' => [],
3✔
135
            'embedded' => [],
3✔
136
        ];
3✔
137

138
        foreach ($attributes as $attribute) {
3✔
139
            $propertyMetadata = $this->propertyMetadataFactory->create($context['resource_class'], $attribute, $options);
3✔
140

141
            // TODO: 3.0 support multiple types, default value of types will be [] instead of null
142
            $type = $propertyMetadata->getBuiltinTypes()[0] ?? null;
3✔
143
            $isOne = $isMany = false;
3✔
144

145
            if (null !== $type) {
3✔
146
                if ($type->isCollection()) {
3✔
147
                    $valueType = $type->getCollectionValueTypes()[0] ?? null;
×
148
                    $isMany = null !== $valueType && ($className = $valueType->getClassName()) && $this->resourceClassResolver->isResourceClass($className);
×
149
                } else {
150
                    $className = $type->getClassName();
3✔
151
                    $isOne = $className && $this->resourceClassResolver->isResourceClass($className);
3✔
152
                }
153
            }
154

155
            if (!$isOne && !$isMany) {
3✔
156
                $components['states'][] = $attribute;
3✔
157
                continue;
3✔
158
            }
159

160
            $relation = ['name' => $attribute, 'cardinality' => $isOne ? 'one' : 'many'];
3✔
161
            if ($propertyMetadata->isReadableLink()) {
3✔
162
                $components['embedded'][] = $relation;
1✔
163
            }
164

165
            $components['links'][] = $relation;
3✔
166
        }
167

168
        if (false !== $context['cache_key']) {
3✔
169
            $this->componentsCache[$cacheKey] = $components;
2✔
170
        }
171

172
        return $components;
3✔
173
    }
174

175
    /**
176
     * Populates _links and _embedded keys.
177
     */
178
    private function populateRelation(array $data, object $object, ?string $format, array $context, array $components, string $type): array
179
    {
180
        $class = $this->getObjectClass($object);
3✔
181

182
        $attributesMetadata = \array_key_exists($class, $this->attributesMetadataCache) ?
3✔
183
            $this->attributesMetadataCache[$class] :
3✔
184
            $this->attributesMetadataCache[$class] = $this->classMetadataFactory ? $this->classMetadataFactory->getMetadataFor($class)->getAttributesMetadata() : null;
3✔
185

186
        $key = '_'.$type;
3✔
187
        foreach ($components[$type] as $relation) {
3✔
188
            if (null !== $attributesMetadata && $this->isMaxDepthReached($attributesMetadata, $class, $relation['name'], $context)) {
3✔
189
                continue;
1✔
190
            }
191

192
            $attributeValue = $this->getAttributeValue($object, $relation['name'], $format, $context);
3✔
193
            if (empty($attributeValue)) {
3✔
194
                continue;
1✔
195
            }
196

197
            $relationName = $relation['name'];
3✔
198
            if ($this->nameConverter) {
3✔
199
                $relationName = $this->nameConverter->normalize($relationName, $class, $format, $context);
2✔
200
            }
201

202
            if ('one' === $relation['cardinality']) {
3✔
203
                if ('links' === $type) {
3✔
204
                    $data[$key][$relationName]['href'] = $this->getRelationIri($attributeValue);
3✔
205
                    continue;
3✔
206
                }
207

208
                $data[$key][$relationName] = $attributeValue;
1✔
209
                continue;
1✔
210
            }
211

212
            // many
213
            $data[$key][$relationName] = [];
×
214
            foreach ($attributeValue as $rel) {
×
215
                if ('links' === $type) {
×
216
                    $rel = ['href' => $this->getRelationIri($rel)];
×
217
                }
218

219
                $data[$key][$relationName][] = $rel;
×
220
            }
221
        }
222

223
        return $data;
3✔
224
    }
225

226
    /**
227
     * Gets the IRI of the given relation.
228
     *
229
     * @throws UnexpectedValueException
230
     */
231
    private function getRelationIri(mixed $rel): string
232
    {
233
        if (!(\is_array($rel) || \is_string($rel))) {
3✔
234
            throw new UnexpectedValueException('Expected relation to be an IRI or array');
×
235
        }
236

237
        return \is_string($rel) ? $rel : $rel['_links']['self']['href'];
3✔
238
    }
239

240
    /**
241
     * Is the max depth reached for the given attribute?
242
     *
243
     * @param AttributeMetadataInterface[] $attributesMetadata
244
     */
245
    private function isMaxDepthReached(array $attributesMetadata, string $class, string $attribute, array &$context): bool
246
    {
247
        if (
248
            !($context[self::ENABLE_MAX_DEPTH] ?? false) ||
1✔
249
            !isset($attributesMetadata[$attribute]) ||
1✔
250
            null === $maxDepth = $attributesMetadata[$attribute]->getMaxDepth()
1✔
251
        ) {
252
            return false;
1✔
253
        }
254

255
        $key = sprintf(self::DEPTH_KEY_PATTERN, $class, $attribute);
1✔
256
        if (!isset($context[$key])) {
1✔
257
            $context[$key] = 1;
1✔
258

259
            return false;
1✔
260
        }
261

262
        if ($context[$key] === $maxDepth) {
1✔
263
            return true;
1✔
264
        }
265

266
        ++$context[$key];
×
267

268
        return false;
×
269
    }
270
}
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