• 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

94.64
/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\Api\ResourceClassResolverInterface;
17
use ApiPlatform\Exception\ResourceClassNotFoundException;
18
use ApiPlatform\Metadata\ApiProperty;
19
use ApiPlatform\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
    {
35
        $this->resourceClassResolver = $resourceClassResolver;
34✔
36
    }
37

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

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

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

52
            if ($denormalizationGroups && !\is_array($denormalizationGroups)) {
11✔
53
                $denormalizationGroups = [$denormalizationGroups];
11✔
54
            }
55
        } catch (ResourceClassNotFoundException) {
×
56
            // TODO: for input/output classes, the serializer groups must be read from the actual resource class
57
            return $propertyMetadata;
×
58
        }
59

60
        $propertyMetadata = $this->transformReadWrite($propertyMetadata, $resourceClass, $property, $normalizationGroups, $denormalizationGroups);
11✔
61

62
        if (!$this->isResourceClass($resourceClass) && ($builtinType = $propertyMetadata->getBuiltinTypes()[0] ?? null) && $builtinType->isCollection()) {
11✔
63
            return $propertyMetadata->withReadableLink(true)->withWritableLink(true);
2✔
64
        }
65

66
        return $this->transformLinkStatus($propertyMetadata, $normalizationGroups, $denormalizationGroups);
11✔
67
    }
68

69
    /**
70
     * Sets readable/writable based on matching normalization/denormalization groups and property's ignorance.
71
     *
72
     * A false value is never reset as it could be unreadable/unwritable for other reasons.
73
     * If normalization/denormalization groups are not specified and the property is not ignored, the property is implicitly readable/writable.
74
     *
75
     * @param string[]|null $normalizationGroups
76
     * @param string[]|null $denormalizationGroups
77
     */
78
    private function transformReadWrite(ApiProperty $propertyMetadata, string $resourceClass, string $propertyName, array $normalizationGroups = null, array $denormalizationGroups = null): ApiProperty
79
    {
80
        $serializerAttributeMetadata = $this->getSerializerAttributeMetadata($resourceClass, $propertyName);
11✔
81
        $groups = $serializerAttributeMetadata ? $serializerAttributeMetadata->getGroups() : [];
11✔
82
        $ignored = $serializerAttributeMetadata && $serializerAttributeMetadata->isIgnored();
11✔
83

84
        if (false !== $propertyMetadata->isReadable()) {
11✔
85
            $propertyMetadata = $propertyMetadata->withReadable(!$ignored && (null === $normalizationGroups || array_intersect($normalizationGroups, $groups)));
11✔
86
        }
87

88
        if (false !== $propertyMetadata->isWritable()) {
11✔
89
            $propertyMetadata = $propertyMetadata->withWritable(!$ignored && (null === $denormalizationGroups || array_intersect($denormalizationGroups, $groups)));
10✔
90
        }
91

92
        return $propertyMetadata;
11✔
93
    }
94

95
    /**
96
     * Sets readableLink/writableLink based on matching normalization/denormalization groups.
97
     *
98
     * If normalization/denormalization groups are not specified,
99
     * set link status to false since embedding of resource must be explicitly enabled
100
     *
101
     * @param string[]|null $normalizationGroups
102
     * @param string[]|null $denormalizationGroups
103
     */
104
    private function transformLinkStatus(ApiProperty $propertyMetadata, array $normalizationGroups = null, array $denormalizationGroups = null): ApiProperty
105
    {
106
        // No need to check link status if property is not readable and not writable
107
        if (false === $propertyMetadata->isReadable() && false === $propertyMetadata->isWritable()) {
11✔
108
            return $propertyMetadata;
5✔
109
        }
110

111
        // TODO: 3.0 support multiple types, default value of types will be [] instead of null
112
        $type = $propertyMetadata->getBuiltinTypes()[0] ?? null;
10✔
113

114
        if (null === $type) {
10✔
115
            return $propertyMetadata;
1✔
116
        }
117

118
        if (
119
            $type->isCollection() &&
10✔
120
            $collectionValueType = $type->getCollectionValueTypes()[0] ?? null
10✔
121
        ) {
122
            $relatedClass = $collectionValueType->getClassName();
3✔
123
        } else {
124
            $relatedClass = $type->getClassName();
10✔
125
        }
126

127
        // if property is not a resource relation, don't set link status (as it would have no meaning)
128
        if (null === $relatedClass || !$this->isResourceClass($relatedClass)) {
10✔
129
            return $propertyMetadata;
10✔
130
        }
131

132
        // find the resource class
133
        // this prevents serializer groups on non-resource child class from incorrectly influencing the decision
134
        if (null !== $this->resourceClassResolver) {
5✔
135
            $relatedClass = $this->resourceClassResolver->getResourceClass(null, $relatedClass);
5✔
136
        }
137

138
        $relatedGroups = $this->getClassSerializerGroups($relatedClass);
5✔
139

140
        if (null === $propertyMetadata->isReadableLink()) {
5✔
141
            $propertyMetadata = $propertyMetadata->withReadableLink(null !== $normalizationGroups && !empty(array_intersect($normalizationGroups, $relatedGroups)));
5✔
142
        }
143

144
        if (null === $propertyMetadata->isWritableLink()) {
5✔
145
            $propertyMetadata = $propertyMetadata->withWritableLink(null !== $denormalizationGroups && !empty(array_intersect($denormalizationGroups, $relatedGroups)));
5✔
146
        }
147

148
        return $propertyMetadata;
5✔
149
    }
150

151
    /**
152
     * Gets the effective serializer groups used in normalization/denormalization.
153
     *
154
     * Groups are extracted in the following order:
155
     *
156
     * - From the "serializer_groups" key of the $options array.
157
     * - From metadata of the given operation ("operation_name" key).
158
     * - From metadata of the current resource.
159
     *
160
     * @return (string[]|null|string)[]
161
     */
162
    private function getEffectiveSerializerGroups(array $options): array
163
    {
164
        if (isset($options['serializer_groups'])) {
11✔
165
            $groups = (array) $options['serializer_groups'];
1✔
166

167
            return [$groups, $groups];
1✔
168
        }
169

170
        if (\array_key_exists('normalization_groups', $options) && \array_key_exists('denormalization_groups', $options)) {
11✔
171
            return [$options['normalization_groups'] ?? null, $options['denormalization_groups'] ?? null];
9✔
172
        }
173

174
        return [null, null];
5✔
175
    }
176

177
    private function getSerializerAttributeMetadata(string $class, string $attribute): ?AttributeMetadataInterface
178
    {
179
        $serializerClassMetadata = $this->serializerClassMetadataFactory->getMetadataFor($class);
11✔
180

181
        foreach ($serializerClassMetadata->getAttributesMetadata() as $serializerAttributeMetadata) {
11✔
182
            if ($attribute === $serializerAttributeMetadata->getName()) {
11✔
183
                return $serializerAttributeMetadata;
11✔
184
            }
185
        }
186

187
        return null;
×
188
    }
189

190
    /**
191
     * Gets all serializer groups used in a class.
192
     *
193
     * @return string[]
194
     */
195
    private function getClassSerializerGroups(string $class): array
196
    {
197
        $serializerClassMetadata = $this->serializerClassMetadataFactory->getMetadataFor($class);
5✔
198

199
        $groups = [];
5✔
200
        foreach ($serializerClassMetadata->getAttributesMetadata() as $serializerAttributeMetadata) {
5✔
201
            $groups[] = $serializerAttributeMetadata->getGroups();
5✔
202
        }
203

204
        return array_unique(array_merge(...$groups));
5✔
205
    }
206
}
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