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

api-platform / core / 15133993414

20 May 2025 09:30AM UTC coverage: 26.313% (-1.2%) from 27.493%
15133993414

Pull #7161

github

web-flow
Merge e2c03d45f into 5459ba375
Pull Request #7161: fix(metadata): infer parameter string type from schema

0 of 2 new or added lines in 1 file covered. (0.0%)

11019 existing lines in 363 files now uncovered.

12898 of 49018 relevant lines covered (26.31%)

34.33 hits per line

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

93.44
/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 Symfony\Component\Serializer\Mapping\AttributeMetadataInterface;
21
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface as SerializerClassMetadataFactoryInterface;
22

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

33
    public function __construct(private readonly SerializerClassMetadataFactoryInterface $serializerClassMetadataFactory, private readonly PropertyMetadataFactoryInterface $decorated, ?ResourceClassResolverInterface $resourceClassResolver = null)
34
    {
UNCOV
35
        $this->resourceClassResolver = $resourceClassResolver;
979✔
36
    }
37

38
    /**
39
     * {@inheritdoc}
40
     */
41
    public function create(string $resourceClass, string $property, array $options = []): ApiProperty
42
    {
UNCOV
43
        $propertyMetadata = $this->decorated->create($resourceClass, $property, $options);
240✔
44

45
        try {
UNCOV
46
            [$normalizationGroups, $denormalizationGroups] = $this->getEffectiveSerializerGroups($options);
240✔
47

UNCOV
48
            if ($normalizationGroups && !\is_array($normalizationGroups)) {
240✔
49
                $normalizationGroups = [$normalizationGroups];
×
50
            }
51

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

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

UNCOV
62
        $propertyMetadata = $this->transformReadWrite($propertyMetadata, $resourceClass, $property, $normalizationGroups, $denormalizationGroups, $ignoredAttributes);
240✔
UNCOV
63
        $types = $propertyMetadata->getBuiltinTypes() ?? [];
240✔
64

UNCOV
65
        if (!$this->isResourceClass($resourceClass) && $types) {
240✔
UNCOV
66
            foreach ($types as $builtinType) {
43✔
UNCOV
67
                if ($builtinType->isCollection()) {
43✔
UNCOV
68
                    return $propertyMetadata->withReadableLink(true)->withWritableLink(true);
14✔
69
                }
70
            }
71
        }
72

UNCOV
73
        return $this->transformLinkStatus($propertyMetadata, $normalizationGroups, $denormalizationGroups, $types);
240✔
74
    }
75

76
    /**
77
     * Sets readable/writable based on matching normalization/denormalization groups and property's ignorance.
78
     *
79
     * A false value is never reset as it could be unreadable/unwritable for other reasons.
80
     * If normalization/denormalization groups are not specified and the property is not ignored, the property is implicitly readable/writable.
81
     *
82
     * @param string[]|null $normalizationGroups
83
     * @param string[]|null $denormalizationGroups
84
     */
85
    private function transformReadWrite(ApiProperty $propertyMetadata, string $resourceClass, string $propertyName, ?array $normalizationGroups = null, ?array $denormalizationGroups = null, array $ignoredAttributes = []): ApiProperty
86
    {
UNCOV
87
        if (\in_array($propertyName, $ignoredAttributes, true)) {
240✔
UNCOV
88
            return $propertyMetadata->withWritable(false)->withReadable(false);
3✔
89
        }
90

UNCOV
91
        $serializerAttributeMetadata = $this->getSerializerAttributeMetadata($resourceClass, $propertyName);
240✔
UNCOV
92
        $groups = $serializerAttributeMetadata ? $serializerAttributeMetadata->getGroups() : [];
240✔
UNCOV
93
        $ignored = $serializerAttributeMetadata && $serializerAttributeMetadata->isIgnored();
240✔
94

UNCOV
95
        if (false !== $propertyMetadata->isReadable()) {
240✔
UNCOV
96
            $propertyMetadata = $propertyMetadata->withReadable(!$ignored && (null === $normalizationGroups || array_intersect($normalizationGroups, $groups)));
235✔
97
        }
98

UNCOV
99
        if (false !== $propertyMetadata->isWritable()) {
240✔
UNCOV
100
            $propertyMetadata = $propertyMetadata->withWritable(!$ignored && (null === $denormalizationGroups || array_intersect($denormalizationGroups, $groups)));
216✔
101
        }
102

UNCOV
103
        return $propertyMetadata;
240✔
104
    }
105

106
    /**
107
     * Sets readableLink/writableLink based on matching normalization/denormalization groups.
108
     *
109
     * If normalization/denormalization groups are not specified,
110
     * set link status to false since embedding of resource must be explicitly enabled
111
     *
112
     * @param string[]|null $normalizationGroups
113
     * @param string[]|null $denormalizationGroups
114
     */
115
    private function transformLinkStatus(ApiProperty $propertyMetadata, ?array $normalizationGroups = null, ?array $denormalizationGroups = null, ?array $types = null): ApiProperty
116
    {
117
        // No need to check link status if property is not readable and not writable
UNCOV
118
        if (false === $propertyMetadata->isReadable() && false === $propertyMetadata->isWritable()) {
240✔
UNCOV
119
            return $propertyMetadata;
54✔
120
        }
121

UNCOV
122
        foreach ($types as $type) {
229✔
123
            if (
UNCOV
124
                $type->isCollection()
221✔
UNCOV
125
                && $collectionValueType = $type->getCollectionValueTypes()[0] ?? null
221✔
126
            ) {
UNCOV
127
                $relatedClass = $collectionValueType->getClassName();
56✔
128
            } else {
UNCOV
129
                $relatedClass = $type->getClassName();
221✔
130
            }
131

132
            // if property is not a resource relation, don't set link status (as it would have no meaning)
UNCOV
133
            if (null === $relatedClass || !$this->isResourceClass($relatedClass)) {
221✔
UNCOV
134
                continue;
217✔
135
            }
136

137
            // find the resource class
138
            // this prevents serializer groups on non-resource child class from incorrectly influencing the decision
UNCOV
139
            if (null !== $this->resourceClassResolver) {
82✔
UNCOV
140
                $relatedClass = $this->resourceClassResolver->getResourceClass(null, $relatedClass);
82✔
141
            }
142

UNCOV
143
            $relatedGroups = $this->getClassSerializerGroups($relatedClass);
82✔
144

UNCOV
145
            if (null === $propertyMetadata->isReadableLink()) {
82✔
UNCOV
146
                $propertyMetadata = $propertyMetadata->withReadableLink(null !== $normalizationGroups && !empty(array_intersect($normalizationGroups, $relatedGroups)));
82✔
147
            }
148

UNCOV
149
            if (null === $propertyMetadata->isWritableLink()) {
82✔
UNCOV
150
                $propertyMetadata = $propertyMetadata->withWritableLink(null !== $denormalizationGroups && !empty(array_intersect($denormalizationGroups, $relatedGroups)));
82✔
151
            }
152

UNCOV
153
            return $propertyMetadata;
82✔
154
        }
155

UNCOV
156
        return $propertyMetadata;
225✔
157
    }
158

159
    /**
160
     * Gets the effective serializer groups used in normalization/denormalization.
161
     *
162
     * Groups are extracted in the following order:
163
     *
164
     * - From the "serializer_groups" key of the $options array.
165
     * - From metadata of the given operation ("operation_name" key).
166
     * - From metadata of the current resource.
167
     *
168
     * @return (string[]|string|null)[]
169
     */
170
    private function getEffectiveSerializerGroups(array $options): array
171
    {
UNCOV
172
        if (isset($options['serializer_groups'])) {
240✔
UNCOV
173
            $groups = (array) $options['serializer_groups'];
92✔
174

UNCOV
175
            return [$groups, $groups];
92✔
176
        }
177

UNCOV
178
        if (\array_key_exists('normalization_groups', $options) && \array_key_exists('denormalization_groups', $options)) {
167✔
UNCOV
179
            return [$options['normalization_groups'] ?? null, $options['denormalization_groups'] ?? null];
120✔
180
        }
181

UNCOV
182
        return [null, null];
84✔
183
    }
184

185
    private function getSerializerAttributeMetadata(string $class, string $attribute): ?AttributeMetadataInterface
186
    {
UNCOV
187
        $serializerClassMetadata = $this->serializerClassMetadataFactory->getMetadataFor($class);
240✔
188

UNCOV
189
        foreach ($serializerClassMetadata->getAttributesMetadata() as $serializerAttributeMetadata) {
240✔
UNCOV
190
            if ($attribute === $serializerAttributeMetadata->getName()) {
240✔
UNCOV
191
                return $serializerAttributeMetadata;
235✔
192
            }
193
        }
194

UNCOV
195
        return null;
15✔
196
    }
197

198
    /**
199
     * Gets all serializer groups used in a class.
200
     *
201
     * @return string[]
202
     */
203
    private function getClassSerializerGroups(string $class): array
204
    {
UNCOV
205
        $serializerClassMetadata = $this->serializerClassMetadataFactory->getMetadataFor($class);
82✔
206

UNCOV
207
        $groups = [];
82✔
UNCOV
208
        foreach ($serializerClassMetadata->getAttributesMetadata() as $serializerAttributeMetadata) {
82✔
UNCOV
209
            $groups[] = $serializerAttributeMetadata->getGroups();
82✔
210
        }
211

UNCOV
212
        return array_unique(array_merge(...$groups));
82✔
213
    }
214
}
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