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

api-platform / core / 6067528200

04 Sep 2023 12:12AM UTC coverage: 36.875% (-21.9%) from 58.794%
6067528200

Pull #5791

github

web-flow
Merge 64157e578 into d09cfc9d2
Pull Request #5791: fix: strip down any sql function name

3096 of 3096 new or added lines in 205 files covered. (100.0%)

9926 of 26918 relevant lines covered (36.87%)

6.5 hits per line

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

88.57
/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
        $context['iri'] = $iri;
12✔
71
        $context['api_normalize'] = true;
12✔
72

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

77
        $data = parent::normalize($object, $format, $context);
12✔
78
        if (!\is_array($data)) {
12✔
79
            return $data;
×
80
        }
81

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

93
        return $metadata + $data;
12✔
94
    }
95

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

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

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

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

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

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

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

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

146
            $types = $propertyMetadata->getBuiltinTypes() ?? [];
12✔
147

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

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

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

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

169
                $relation = ['name' => $attribute, 'cardinality' => $isOne ? 'one' : 'many'];
9✔
170
                if ($propertyMetadata->isReadableLink()) {
9✔
171
                    $components['embedded'][] = $relation;
3✔
172
                }
173

174
                $components['links'][] = $relation;
9✔
175
                $isRelationship = true;
9✔
176
            }
177

178
            // if all types are not relationships, declare it as an attribute
179
            if (!$isRelationship) {
12✔
180
                $components['states'][] = $attribute;
12✔
181
            }
182
        }
183

184
        if (false !== $context['cache_key']) {
12✔
185
            $this->componentsCache[$cacheKey] = $components;
9✔
186
        }
187

188
        return $components;
12✔
189
    }
190

191
    /**
192
     * Populates _links and _embedded keys.
193
     */
194
    private function populateRelation(array $data, object $object, ?string $format, array $context, array $components, string $type): array
195
    {
196
        $class = $this->getObjectClass($object);
12✔
197

198
        $attributesMetadata = \array_key_exists($class, $this->attributesMetadataCache) ?
12✔
199
            $this->attributesMetadataCache[$class] :
12✔
200
            $this->attributesMetadataCache[$class] = $this->classMetadataFactory ? $this->classMetadataFactory->getMetadataFor($class)->getAttributesMetadata() : null;
12✔
201

202
        $key = '_'.$type;
12✔
203
        foreach ($components[$type] as $relation) {
12✔
204
            if (null !== $attributesMetadata && $this->isMaxDepthReached($attributesMetadata, $class, $relation['name'], $context)) {
9✔
205
                continue;
3✔
206
            }
207

208
            $attributeValue = $this->getAttributeValue($object, $relation['name'], $format, $context);
9✔
209
            if (empty($attributeValue)) {
9✔
210
                continue;
3✔
211
            }
212

213
            $relationName = $relation['name'];
9✔
214
            if ($this->nameConverter) {
9✔
215
                $relationName = $this->nameConverter->normalize($relationName, $class, $format, $context);
6✔
216
            }
217

218
            if ('one' === $relation['cardinality']) {
9✔
219
                if ('links' === $type) {
9✔
220
                    $data[$key][$relationName]['href'] = $this->getRelationIri($attributeValue);
9✔
221
                    continue;
9✔
222
                }
223

224
                $data[$key][$relationName] = $attributeValue;
3✔
225
                continue;
3✔
226
            }
227

228
            // many
229
            $data[$key][$relationName] = [];
×
230
            foreach ($attributeValue as $rel) {
×
231
                if ('links' === $type) {
×
232
                    $rel = ['href' => $this->getRelationIri($rel)];
×
233
                }
234

235
                $data[$key][$relationName][] = $rel;
×
236
            }
237
        }
238

239
        return $data;
12✔
240
    }
241

242
    /**
243
     * Gets the IRI of the given relation.
244
     *
245
     * @throws UnexpectedValueException
246
     */
247
    private function getRelationIri(mixed $rel): string
248
    {
249
        if (!(\is_array($rel) || \is_string($rel))) {
9✔
250
            throw new UnexpectedValueException('Expected relation to be an IRI or array');
×
251
        }
252

253
        return \is_string($rel) ? $rel : $rel['_links']['self']['href'];
9✔
254
    }
255

256
    /**
257
     * Is the max depth reached for the given attribute?
258
     *
259
     * @param AttributeMetadataInterface[] $attributesMetadata
260
     */
261
    private function isMaxDepthReached(array $attributesMetadata, string $class, string $attribute, array &$context): bool
262
    {
263
        if (
264
            !($context[self::ENABLE_MAX_DEPTH] ?? false)
3✔
265
            || !isset($attributesMetadata[$attribute])
3✔
266
            || null === $maxDepth = $attributesMetadata[$attribute]->getMaxDepth()
3✔
267
        ) {
268
            return false;
3✔
269
        }
270

271
        $key = sprintf(self::DEPTH_KEY_PATTERN, $class, $attribute);
3✔
272
        if (!isset($context[$key])) {
3✔
273
            $context[$key] = 1;
3✔
274

275
            return false;
3✔
276
        }
277

278
        if ($context[$key] === $maxDepth) {
3✔
279
            return true;
3✔
280
        }
281

282
        ++$context[$key];
×
283

284
        return false;
×
285
    }
286
}
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