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

api-platform / core / 17723449516

15 Sep 2025 05:52AM UTC coverage: 0.0% (-22.6%) from 22.578%
17723449516

Pull #7383

github

web-flow
Merge fa5b61e35 into 949c3c975
Pull Request #7383: fix(metadata): compute isWritable during updates

0 of 6 new or added lines in 4 files covered. (0.0%)

11356 existing lines in 371 files now uncovered.

0 of 48868 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/Symfony/Validator/Metadata/Property/ValidatorPropertyMetadataFactory.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\Symfony\Validator\Metadata\Property;
15

16
use ApiPlatform\JsonSchema\Metadata\Property\Factory\SchemaPropertyMetadataFactory;
17
use ApiPlatform\Metadata\ApiProperty;
18
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
19
use ApiPlatform\Symfony\Validator\Metadata\Property\Restriction\PropertySchemaRestrictionMetadataInterface;
20
use Symfony\Component\Validator\Constraint;
21
use Symfony\Component\Validator\Constraints\Bic;
22
use Symfony\Component\Validator\Constraints\CardScheme;
23
use Symfony\Component\Validator\Constraints\Compound;
24
use Symfony\Component\Validator\Constraints\Currency;
25
use Symfony\Component\Validator\Constraints\Date;
26
use Symfony\Component\Validator\Constraints\DateTime;
27
use Symfony\Component\Validator\Constraints\Email;
28
use Symfony\Component\Validator\Constraints\File;
29
use Symfony\Component\Validator\Constraints\GroupSequence;
30
use Symfony\Component\Validator\Constraints\Iban;
31
use Symfony\Component\Validator\Constraints\Image;
32
use Symfony\Component\Validator\Constraints\Isbn;
33
use Symfony\Component\Validator\Constraints\Issn;
34
use Symfony\Component\Validator\Constraints\NotBlank;
35
use Symfony\Component\Validator\Constraints\NotNull;
36
use Symfony\Component\Validator\Constraints\Sequentially;
37
use Symfony\Component\Validator\Constraints\Time;
38
use Symfony\Component\Validator\Constraints\Url;
39
use Symfony\Component\Validator\Constraints\Uuid;
40
use Symfony\Component\Validator\Mapping\ClassMetadataInterface as ValidatorClassMetadataInterface;
41
use Symfony\Component\Validator\Mapping\Factory\MetadataFactoryInterface as ValidatorMetadataFactoryInterface;
42
use Symfony\Component\Validator\Mapping\PropertyMetadataInterface as ValidatorPropertyMetadataInterface;
43

44
/**
45
 * Decorates a metadata loader using the validator.
46
 *
47
 * @author Kévin Dunglas <dunglas@gmail.com>
48
 */
49
final class ValidatorPropertyMetadataFactory implements PropertyMetadataFactoryInterface
50
{
51
    /**
52
     * @var string[] A list of constraint classes making the entity required
53
     */
54
    public const REQUIRED_CONSTRAINTS = [NotBlank::class, NotNull::class];
55

56
    public const SCHEMA_MAPPED_CONSTRAINTS = [
57
        Url::class => 'https://schema.org/url',
58
        Email::class => 'https://schema.org/email',
59
        Uuid::class => 'https://schema.org/identifier',
60
        CardScheme::class => 'https://schema.org/identifier',
61
        Bic::class => 'https://schema.org/identifier',
62
        Iban::class => 'https://schema.org/identifier',
63
        Date::class => 'https://schema.org/Date',
64
        DateTime::class => 'https://schema.org/DateTime',
65
        Time::class => 'https://schema.org/Time',
66
        Image::class => 'https://schema.org/image',
67
        File::class => 'https://schema.org/MediaObject',
68
        Currency::class => 'https://schema.org/priceCurrency',
69
        Isbn::class => 'https://schema.org/isbn',
70
        Issn::class => 'https://schema.org/issn',
71
    ];
72

73
    /**
74
     * @param PropertySchemaRestrictionMetadataInterface[] $restrictionsMetadata
75
     */
76
    public function __construct(private readonly ValidatorMetadataFactoryInterface $validatorMetadataFactory, private readonly PropertyMetadataFactoryInterface $decorated, private readonly iterable $restrictionsMetadata = [])
77
    {
UNCOV
78
    }
×
79

80
    /**
81
     * {@inheritdoc}
82
     */
83
    public function create(string $resourceClass, string $property, array $options = []): ApiProperty
84
    {
UNCOV
85
        $propertyMetadata = $this->decorated->create($resourceClass, $property, $options);
×
86

UNCOV
87
        $extraProperties = $propertyMetadata->getExtraProperties() ?? [];
×
88
        // see AttributePropertyMetadataFactory
UNCOV
89
        if (true === ($extraProperties[SchemaPropertyMetadataFactory::JSON_SCHEMA_USER_DEFINED] ?? false)) {
×
90
            // schema seems to have been declared by the user: do not override nor complete user value
UNCOV
91
            return $propertyMetadata;
×
92
        }
93

UNCOV
94
        $required = $propertyMetadata->isRequired();
×
UNCOV
95
        $types = $propertyMetadata->getTypes();
×
UNCOV
96
        $schema = $propertyMetadata->getSchema();
×
97

UNCOV
98
        if (null !== $required && $types && $schema) {
×
99
            return $propertyMetadata;
×
100
        }
101

UNCOV
102
        $validatorClassMetadata = $this->validatorMetadataFactory->getMetadataFor($resourceClass);
×
103

UNCOV
104
        if (!$validatorClassMetadata instanceof ValidatorClassMetadataInterface) {
×
105
            throw new \UnexpectedValueException(\sprintf('Validator class metadata expected to be of type "%s".', ValidatorClassMetadataInterface::class));
×
106
        }
107

UNCOV
108
        $validationGroups = $this->getValidationGroups($validatorClassMetadata, $options);
×
UNCOV
109
        $restrictions = [];
×
UNCOV
110
        $types ??= [];
×
111

UNCOV
112
        foreach ($validatorClassMetadata->getPropertyMetadata($property) as $validatorPropertyMetadata) {
×
UNCOV
113
            foreach ($this->getPropertyConstraints($validatorPropertyMetadata, $validationGroups) as $constraint) {
×
UNCOV
114
                if (null === $required && $this->isRequired($constraint)) {
×
UNCOV
115
                    $required = true;
×
116
                }
117

UNCOV
118
                $type = self::SCHEMA_MAPPED_CONSTRAINTS[$constraint::class] ?? null;
×
119

UNCOV
120
                if ($type && !\in_array($type, $types, true)) {
×
UNCOV
121
                    $types[] = $type;
×
122
                }
123

UNCOV
124
                foreach ($this->restrictionsMetadata as $restrictionMetadata) {
×
UNCOV
125
                    if ($restrictionMetadata->supports($constraint, $propertyMetadata)) {
×
UNCOV
126
                        $restrictions[] = $restrictionMetadata->create($constraint, $propertyMetadata);
×
127
                    }
128
                }
129
            }
130
        }
131

UNCOV
132
        if ($types) {
×
UNCOV
133
            $propertyMetadata = $propertyMetadata->withTypes($types);
×
134
        }
135

UNCOV
136
        $propertyMetadata = $propertyMetadata->withRequired($required ?? false);
×
137

UNCOV
138
        if (!empty($restrictions)) {
×
UNCOV
139
            if (null === $schema) {
×
UNCOV
140
                $schema = [];
×
141
            }
142

UNCOV
143
            $schema += array_merge(...$restrictions);
×
UNCOV
144
            $propertyMetadata = $propertyMetadata->withSchema($schema);
×
145
        }
146

UNCOV
147
        return $propertyMetadata;
×
148
    }
149

150
    /**
151
     * Returns the list of validation groups.
152
     */
153
    private function getValidationGroups(ValidatorClassMetadataInterface $classMetadata, array $options): array
154
    {
UNCOV
155
        if (isset($options['validation_groups'])) {
×
UNCOV
156
            if ($options['validation_groups'] instanceof GroupSequence) {
×
157
                return $options['validation_groups']->groups;
×
158
            }
159

UNCOV
160
            if (!\is_callable($options['validation_groups'])) {
×
UNCOV
161
                return $options['validation_groups'];
×
162
            }
163
        }
164

UNCOV
165
        if (!method_exists($classMetadata, 'getDefaultGroup')) {
×
166
            throw new \UnexpectedValueException(\sprintf('Validator class metadata expected to have method "%s".', 'getDefaultGroup'));
×
167
        }
168

UNCOV
169
        return [$classMetadata->getDefaultGroup()];
×
170
    }
171

172
    /**
173
     * Tests if the property is required because of its validation groups.
174
     */
175
    private function getPropertyConstraints(
176
        ValidatorPropertyMetadataInterface $validatorPropertyMetadata,
177
        array $groups,
178
    ): array {
UNCOV
179
        $constraints = [];
×
180

UNCOV
181
        foreach ($groups as $validationGroup) {
×
UNCOV
182
            if (!\is_string($validationGroup)) {
×
UNCOV
183
                continue;
×
184
            }
185

UNCOV
186
            foreach ($validatorPropertyMetadata->findConstraints($validationGroup) as $propertyConstraint) {
×
UNCOV
187
                if ($propertyConstraint instanceof Sequentially || $propertyConstraint instanceof Compound) {
×
UNCOV
188
                    $constraints[] = $propertyConstraint->getNestedConstraints();
×
189
                } else {
UNCOV
190
                    $constraints[] = [$propertyConstraint];
×
191
                }
192
            }
193
        }
194

UNCOV
195
        return array_merge([], ...$constraints);
×
196
    }
197

198
    /**
199
     * Is this constraint making the related property required?
200
     */
201
    private function isRequired(Constraint $constraint): bool
202
    {
UNCOV
203
        if ($constraint instanceof NotBlank && $constraint->allowNull) {
×
UNCOV
204
            return false;
×
205
        }
206

UNCOV
207
        foreach (self::REQUIRED_CONSTRAINTS as $requiredConstraint) {
×
UNCOV
208
            if ($constraint instanceof $requiredConstraint) {
×
UNCOV
209
                return true;
×
210
            }
211
        }
212

UNCOV
213
        return false;
×
214
    }
215
}
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