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

api-platform / core / 16705318661

03 Aug 2025 01:05PM UTC coverage: 0.0% (-21.9%) from 21.944%
16705318661

Pull #7317

github

web-flow
Merge 1ca8642ff into d06b1a0a0
Pull Request #7317: Fix/4372 skip null values in hal

0 of 14 new or added lines in 3 files covered. (0.0%)

11680 existing lines in 376 files now uncovered.

0 of 51817 relevant lines covered (0.0%)

0.0 hits per line

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

0.0
/src/Metadata/Property/Factory/SerializerPropertyMetadataFactory.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\Metadata\Property\Factory;
15

16
use ApiPlatform\Metadata\ApiProperty;
17
use ApiPlatform\Metadata\Exception\ResourceClassNotFoundException;
18
use ApiPlatform\Metadata\ResourceClassResolverInterface;
19
use ApiPlatform\Metadata\Util\ResourceClassInfoTrait;
20
use ApiPlatform\Metadata\Util\TypeHelper;
21
use Symfony\Component\PropertyInfo\PropertyInfoExtractor;
22
use Symfony\Component\Serializer\Mapping\AttributeMetadataInterface;
23
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface as SerializerClassMetadataFactoryInterface;
24
use Symfony\Component\TypeInfo\Type;
25
use Symfony\Component\TypeInfo\Type\CollectionType;
26

27
/**
28
 * Populates read/write and link status using serialization groups.
29
 *
30
 * @author Kévin Dunglas <dunglas@gmail.com>
31
 * @author Teoh Han Hui <teohhanhui@gmail.com>
32
 */
33
final class SerializerPropertyMetadataFactory implements PropertyMetadataFactoryInterface
34
{
35
    use ResourceClassInfoTrait;
36

37
    public function __construct(private readonly SerializerClassMetadataFactoryInterface $serializerClassMetadataFactory, private readonly PropertyMetadataFactoryInterface $decorated, ?ResourceClassResolverInterface $resourceClassResolver = null)
38
    {
UNCOV
39
        $this->resourceClassResolver = $resourceClassResolver;
×
40
    }
41

42
    /**
43
     * {@inheritdoc}
44
     */
45
    public function create(string $resourceClass, string $property, array $options = []): ApiProperty
46
    {
UNCOV
47
        $propertyMetadata = $this->decorated->create($resourceClass, $property, $options);
×
48

49
        try {
UNCOV
50
            [$normalizationGroups, $denormalizationGroups] = $this->getEffectiveSerializerGroups($options);
×
51

UNCOV
52
            if ($normalizationGroups && !\is_array($normalizationGroups)) {
×
53
                $normalizationGroups = [$normalizationGroups];
×
54
            }
55

UNCOV
56
            if ($denormalizationGroups && !\is_array($denormalizationGroups)) {
×
57
                $denormalizationGroups = [$denormalizationGroups];
×
58
            }
59

UNCOV
60
            $ignoredAttributes = $options['ignored_attributes'] ?? [];
×
61
        } catch (ResourceClassNotFoundException) {
×
62
            // TODO: for input/output classes, the serializer groups must be read from the actual resource class
63
            return $propertyMetadata;
×
64
        }
65

UNCOV
66
        $propertyMetadata = $this->transformReadWrite($propertyMetadata, $resourceClass, $property, $normalizationGroups, $denormalizationGroups, $ignoredAttributes);
×
67

68
        // TODO: remove in 5.x
UNCOV
69
        if (!method_exists(PropertyInfoExtractor::class, 'getType')) {
×
70
            $types = $propertyMetadata->getBuiltinTypes() ?? [];
×
71

72
            if (!$this->isResourceClass($resourceClass) && $types) {
×
73
                foreach ($types as $builtinType) {
×
74
                    if ($builtinType->isCollection()) {
×
75
                        return $propertyMetadata->withReadableLink(true)->withWritableLink(true);
×
76
                    }
77
                }
78
            }
79

80
            return $this->transformLinkStatusLegacy($propertyMetadata, $normalizationGroups, $denormalizationGroups, $types);
×
81
        }
UNCOV
82
        $type = $propertyMetadata->getNativeType();
×
UNCOV
83
        if (null !== $type && !$this->isResourceClass($resourceClass) && $type->isSatisfiedBy(static fn (Type $t): bool => $t instanceof CollectionType)) {
×
UNCOV
84
            return $propertyMetadata->withReadableLink(true)->withWritableLink(true);
×
85
        }
86

UNCOV
87
        return $this->transformLinkStatus($propertyMetadata, $normalizationGroups, $denormalizationGroups, $type);
×
88
    }
89

90
    /**
91
     * Sets readable/writable based on matching normalization/denormalization groups and property's ignorance.
92
     *
93
     * A false value is never reset as it could be unreadable/unwritable for other reasons.
94
     * If normalization/denormalization groups are not specified and the property is not ignored, the property is implicitly readable/writable.
95
     *
96
     * @param string[]|null $normalizationGroups
97
     * @param string[]|null $denormalizationGroups
98
     */
99
    private function transformReadWrite(ApiProperty $propertyMetadata, string $resourceClass, string $propertyName, ?array $normalizationGroups = null, ?array $denormalizationGroups = null, array $ignoredAttributes = []): ApiProperty
100
    {
UNCOV
101
        if (\in_array($propertyName, $ignoredAttributes, true)) {
×
UNCOV
102
            return $propertyMetadata->withWritable(false)->withReadable(false);
×
103
        }
104

UNCOV
105
        $serializerAttributeMetadata = $this->getSerializerAttributeMetadata($resourceClass, $propertyName);
×
UNCOV
106
        $groups = $serializerAttributeMetadata?->getGroups() ?? [];
×
UNCOV
107
        $ignored = $serializerAttributeMetadata?->isIgnored() ?? false;
×
108

UNCOV
109
        if (false !== $propertyMetadata->isReadable()) {
×
UNCOV
110
            $propertyMetadata = $propertyMetadata->withReadable(!$ignored && (null === $normalizationGroups || array_intersect($normalizationGroups, $groups)));
×
111
        }
112

UNCOV
113
        if (false !== $propertyMetadata->isWritable()) {
×
UNCOV
114
            $propertyMetadata = $propertyMetadata->withWritable(!$ignored && (null === $denormalizationGroups || array_intersect($denormalizationGroups, $groups)));
×
115
        }
116

UNCOV
117
        return $propertyMetadata;
×
118
    }
119

120
    /**
121
     * Sets readableLink/writableLink based on matching normalization/denormalization groups.
122
     *
123
     * If normalization/denormalization groups are not specified,
124
     * set link status to false since embedding of resource must be explicitly enabled
125
     *
126
     * @param string[]|null $normalizationGroups
127
     * @param string[]|null $denormalizationGroups
128
     */
129
    private function transformLinkStatusLegacy(ApiProperty $propertyMetadata, ?array $normalizationGroups = null, ?array $denormalizationGroups = null, ?array $types = null): ApiProperty
130
    {
131
        // No need to check link status if property is not readable and not writable
132
        if (false === $propertyMetadata->isReadable() && false === $propertyMetadata->isWritable()) {
×
133
            return $propertyMetadata;
×
134
        }
135

136
        foreach ($types as $type) {
×
137
            if (
138
                $type->isCollection()
×
139
                && $collectionValueType = $type->getCollectionValueTypes()[0] ?? null
×
140
            ) {
141
                $relatedClass = $collectionValueType->getClassName();
×
142
            } else {
143
                $relatedClass = $type->getClassName();
×
144
            }
145

146
            // if property is not a resource relation, don't set link status (as it would have no meaning)
147
            if (null === $relatedClass || !$this->isResourceClass($relatedClass)) {
×
148
                continue;
×
149
            }
150

151
            // find the resource class
152
            // this prevents serializer groups on non-resource child class from incorrectly influencing the decision
153
            if (null !== $this->resourceClassResolver) {
×
154
                $relatedClass = $this->resourceClassResolver->getResourceClass(null, $relatedClass);
×
155
            }
156

157
            $relatedGroups = $this->getClassSerializerGroups($relatedClass);
×
158

159
            if (null === $propertyMetadata->isReadableLink()) {
×
160
                $propertyMetadata = $propertyMetadata->withReadableLink(null !== $normalizationGroups && !empty(array_intersect($normalizationGroups, $relatedGroups)));
×
161
            }
162

163
            if (null === $propertyMetadata->isWritableLink()) {
×
164
                $propertyMetadata = $propertyMetadata->withWritableLink(null !== $denormalizationGroups && !empty(array_intersect($denormalizationGroups, $relatedGroups)));
×
165
            }
166

167
            return $propertyMetadata;
×
168
        }
169

170
        return $propertyMetadata;
×
171
    }
172

173
    /**
174
     * Sets readableLink/writableLink based on matching normalization/denormalization groups.
175
     *
176
     * If normalization/denormalization groups are not specified,
177
     * set link status to false since embedding of resource must be explicitly enabled
178
     *
179
     * @param string[]|null $normalizationGroups
180
     * @param string[]|null $denormalizationGroups
181
     */
182
    private function transformLinkStatus(ApiProperty $propertyMetadata, ?array $normalizationGroups = null, ?array $denormalizationGroups = null, ?Type $type = null): ApiProperty
183
    {
184
        // No need to check link status if property is not readable and not writable
UNCOV
185
        if (false === $propertyMetadata->isReadable() && false === $propertyMetadata->isWritable()) {
×
UNCOV
186
            return $propertyMetadata;
×
187
        }
188

UNCOV
189
        if (!$type) {
×
UNCOV
190
            return $propertyMetadata;
×
191
        }
192

UNCOV
193
        $collectionValueType = TypeHelper::getCollectionValueType($type);
×
UNCOV
194
        $className = $collectionValueType ? TypeHelper::getClassName($collectionValueType) : TypeHelper::getClassName($type);
×
195

196
        // if property is not a resource relation, don't set link status (as it would have no meaning)
UNCOV
197
        if (!$className || !$this->isResourceClass($className)) {
×
UNCOV
198
            return $propertyMetadata;
×
199
        }
200

201
        // find the resource class
202
        // this prevents serializer groups on non-resource child class from incorrectly influencing the decision
UNCOV
203
        if (null !== $this->resourceClassResolver) {
×
UNCOV
204
            $className = $this->resourceClassResolver->getResourceClass(null, $className);
×
205
        }
206

UNCOV
207
        $relatedGroups = $this->getClassSerializerGroups($className);
×
208

UNCOV
209
        if (null === $propertyMetadata->isReadableLink()) {
×
UNCOV
210
            $propertyMetadata = $propertyMetadata->withReadableLink(null !== $normalizationGroups && !empty(array_intersect($normalizationGroups, $relatedGroups)));
×
211
        }
212

UNCOV
213
        if (null === $propertyMetadata->isWritableLink()) {
×
UNCOV
214
            $propertyMetadata = $propertyMetadata->withWritableLink(null !== $denormalizationGroups && !empty(array_intersect($denormalizationGroups, $relatedGroups)));
×
215
        }
216

UNCOV
217
        return $propertyMetadata;
×
218
    }
219

220
    /**
221
     * Gets the effective serializer groups used in normalization/denormalization.
222
     *
223
     * Groups are extracted in the following order:
224
     *
225
     * - From the "serializer_groups" key of the $options array.
226
     * - From metadata of the given operation ("operation_name" key).
227
     * - From metadata of the current resource.
228
     *
229
     * @return (string[]|string|null)[]
230
     */
231
    private function getEffectiveSerializerGroups(array $options): array
232
    {
UNCOV
233
        if (isset($options['serializer_groups'])) {
×
UNCOV
234
            $groups = (array) $options['serializer_groups'];
×
235

UNCOV
236
            return [$groups, $groups];
×
237
        }
238

UNCOV
239
        if (\array_key_exists('normalization_groups', $options) && \array_key_exists('denormalization_groups', $options)) {
×
UNCOV
240
            return [$options['normalization_groups'] ?? null, $options['denormalization_groups'] ?? null];
×
241
        }
242

UNCOV
243
        return [null, null];
×
244
    }
245

246
    private function getSerializerAttributeMetadata(string $class, string $attribute): ?AttributeMetadataInterface
247
    {
UNCOV
248
        $serializerClassMetadata = $this->serializerClassMetadataFactory->getMetadataFor($class);
×
249

UNCOV
250
        foreach ($serializerClassMetadata->getAttributesMetadata() as $serializerAttributeMetadata) {
×
UNCOV
251
            if ($attribute === $serializerAttributeMetadata->getName()) {
×
UNCOV
252
                return $serializerAttributeMetadata;
×
253
            }
254
        }
255

UNCOV
256
        return null;
×
257
    }
258

259
    /**
260
     * Gets all serializer groups used in a class.
261
     *
262
     * @return string[]
263
     */
264
    private function getClassSerializerGroups(string $class): array
265
    {
UNCOV
266
        $serializerClassMetadata = $this->serializerClassMetadataFactory->getMetadataFor($class);
×
267

UNCOV
268
        $groups = [];
×
UNCOV
269
        foreach ($serializerClassMetadata->getAttributesMetadata() as $serializerAttributeMetadata) {
×
UNCOV
270
            $groups[] = $serializerAttributeMetadata->getGroups();
×
271
        }
272

UNCOV
273
        return array_unique(array_merge(...$groups));
×
274
    }
275
}
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