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

overblog / GraphQLBundle / 19025601906

03 Nov 2025 06:13AM UTC coverage: 98.283% (+0.004%) from 98.279%
19025601906

Pull #1216

github

web-flow
Merge 806fc9dd6 into cd19c3e9f
Pull Request #1216: Refactor InputValidator

50 of 50 new or added lines in 1 file covered. (100.0%)

2 existing lines in 1 file now uncovered.

4351 of 4427 relevant lines covered (98.28%)

39.9 hits per line

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

98.15
/src/Validator/InputValidator.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace Overblog\GraphQLBundle\Validator;
6

7
use GraphQL\Type\Definition\InputObjectType;
8
use GraphQL\Type\Definition\ObjectType;
9
use GraphQL\Type\Definition\ResolveInfo;
10
use GraphQL\Type\Definition\Type;
11
use Overblog\GraphQLBundle\Definition\ResolverArgs;
12
use Overblog\GraphQLBundle\Validator\Exception\ArgumentsValidationException;
13
use Overblog\GraphQLBundle\Validator\Mapping\MetadataFactory;
14
use Overblog\GraphQLBundle\Validator\Mapping\ObjectMetadata;
15
use Symfony\Component\Validator\Constraint;
16
use Symfony\Component\Validator\Constraints\GroupSequence;
17
use Symfony\Component\Validator\Constraints\Valid;
18
use Symfony\Component\Validator\ConstraintValidatorFactoryInterface;
19
use Symfony\Component\Validator\ConstraintViolationListInterface;
20
use Symfony\Component\Validator\Mapping\ClassMetadataInterface;
21
use Symfony\Component\Validator\Mapping\GetterMetadata;
22
use Symfony\Component\Validator\Mapping\PropertyMetadata;
23
use Symfony\Component\Validator\Validation;
24
use Symfony\Component\Validator\Validator\ValidatorInterface;
25
use Symfony\Contracts\Translation\TranslatorInterface;
26

27
use function in_array;
28

29
final class InputValidator
30
{
31
    private const TYPE_PROPERTY = 'property';
32
    private const TYPE_GETTER = 'getter';
33
    public const CASCADE = 'cascade';
34

35
    private ResolverArgs $resolverArgs;
36
    private ValidatorInterface $defaultValidator;
37
    private MetadataFactory $metadataFactory;
38
    private ResolveInfo $info;
39
    private ConstraintValidatorFactoryInterface $constraintValidatorFactory;
40
    private ?TranslatorInterface $defaultTranslator;
41

42
    /** @var ClassMetadataInterface[] */
43
    private array $cachedMetadata = [];
44

45
    public function __construct(
46
        ResolverArgs $resolverArgs,
47
        ValidatorInterface $validator,
48
        ConstraintValidatorFactoryInterface $constraintValidatorFactory,
49
        ?TranslatorInterface $translator
50
    ) {
51
        $this->resolverArgs = $resolverArgs;
19✔
52
        $this->info = $this->resolverArgs->info;
19✔
53
        $this->defaultValidator = $validator;
19✔
54
        $this->constraintValidatorFactory = $constraintValidatorFactory;
19✔
55
        $this->defaultTranslator = $translator;
19✔
56
        $this->metadataFactory = new MetadataFactory();
19✔
57
    }
19✔
58

59
    /**
60
     * Entry point.
61
     *
62
     * @throws ArgumentsValidationException
63
     */
64
    public function validate(string|array|null $groups = null, bool $throw = true): ?ConstraintViolationListInterface
65
    {
66
        $rootNode = new ValidationNode(
19✔
67
            $this->info->parentType,
19✔
68
            $this->info->fieldName,
19✔
69
            null,
19✔
70
            $this->resolverArgs
19✔
71
        );
72

73
        $this->buildValidationTree(
19✔
74
            $rootNode,
75
            $this->info->fieldDefinition->config['args'] ?? [],
19✔
76
            Utils::getClassLevelConstraints($this->info),
19✔
77
            $this->resolverArgs->args->getArrayCopy()
19✔
78
        );
79

80
        $validator = $this->createValidator($this->metadataFactory);
19✔
81
        $errors = $validator->validate($rootNode, null, $groups);
19✔
82

83
        if ($throw && $errors->count() > 0) {
19✔
84
            throw new ArgumentsValidationException($errors);
7✔
85
        } else {
86
            return $errors;
12✔
87
        }
88
    }
89

90
    /**
91
     * Creates a validator with a custom metadata factory. The metadata factory
92
     * is used to properly map validation constraints to ValidationNode objects.
93
     */
94
    private function createValidator(MetadataFactory $metadataFactory): ValidatorInterface
95
    {
96
        $builder = Validation::createValidatorBuilder()
19✔
97
            ->setMetadataFactory($metadataFactory)
19✔
98
            ->setConstraintValidatorFactory($this->constraintValidatorFactory);
19✔
99

100
        if (null !== $this->defaultTranslator) {
19✔
101
            // @phpstan-ignore-next-line (only for Symfony 4.4)
102
            $builder
UNCOV
103
                ->setTranslator($this->defaultTranslator)
×
UNCOV
104
                ->setTranslationDomain('validators');
×
105
        }
106

107
        return $builder->getValidator();
19✔
108
    }
109

110
    /**
111
     * Either returns an existing metadata object related to
112
     * the ValidationNode object or creates a new one.
113
     */
114
    private function getMetadata(ValidationNode $rootObject): ObjectMetadata
115
    {
116
        // Return existing metadata if present
117
        if ($this->metadataFactory->hasMetadataFor($rootObject)) {
19✔
118
            return $this->metadataFactory->getMetadataFor($rootObject);
8✔
119
        }
120

121
        // Create new metadata and add it to the factory
122
        $metadata = new ObjectMetadata($rootObject);
19✔
123
        $this->metadataFactory->addMetadata($metadata);
19✔
124

125
        return $metadata;
19✔
126
    }
127

128
    /**
129
     * Creates a map of ValidationNode objects from args and
130
     * simultaneously applies validation constraints to them.
131
     */
132
    private function buildValidationTree(ValidationNode $rootObject, iterable $fields, array $classValidation, array $inputData): ValidationNode
133
    {
134
        $metadata = $this->getMetadata($rootObject);
19✔
135

136
        if (!empty($classValidation)) {
19✔
137
            $this->applyClassValidation($metadata, $classValidation);
17✔
138
        }
139

140
        foreach ($fields as $name => $arg) {
19✔
141
            $property = $arg['name'] ?? $name;
19✔
142
            $config = Utils::normalizeConfig($arg['validation'] ?? []);
19✔
143

144
            if ($this->shouldCascade($config, $inputData, $property)) {
19✔
145
                $this->handleCascade($rootObject, $property, $arg, $config, $inputData[$property]);
8✔
146
                continue; // delegated to nested object
8✔
147
            }
148

149
            // assign scalar/null value when not cascading
150
            $rootObject->$property = $inputData[$property] ?? null;
19✔
151

152
            if ($metadata->hasPropertyMetadata($property)) {
19✔
153
                continue;
1✔
154
            }
155

156
            $this->applyPropertyConstraints($metadata, $property, Utils::normalizeConfig($config));
19✔
157
        }
158

159
        return $rootObject;
19✔
160
    }
161

162
    private function shouldCascade(array $config, array $inputData, string|int $property): bool
163
    {
164
        return isset($config['cascade']) && isset($inputData[$property]);
19✔
165
    }
166

167
    /**
168
     * Creates a new nested ValidationNode object or a collection of them
169
     * based on the type of the argument and applies the cascade validation.
170
     */
171
    private function handleCascade(ValidationNode $rootObject, string|int $property, array $arg, array $config, mixed $value): void
172
    {
173
        $argType = Utils::unclosure($arg['type']);
8✔
174
        /** @var ObjectType|InputObjectType $type */
175
        $type = Type::getNamedType($argType);
8✔
176

177
        if (Utils::isListOfType($argType)) {
8✔
178
            $rootObject->$property = $this->createCollectionNode($value, $type, $rootObject);
3✔
179
        } else {
180
            $rootObject->$property = $this->createObjectNode($value, $type, $rootObject);
8✔
181
        }
182

183
        // Mark the property for recursive validation
184
        $this->addValidConstraint($this->getMetadata($rootObject), (string) $property, $config['cascade']);
8✔
185
    }
8✔
186

187
    /**
188
     * Applies the Assert\Valid constraint to enable a recursive validation.
189
     *
190
     * @link https://symfony.com/doc/current/reference/constraints/Valid.html Docs
191
     */
192
    private function addValidConstraint(ObjectMetadata $metadata, string $property, array $groups): void
193
    {
194
        $valid = new Valid();
8✔
195
        if (!empty($groups)) {
8✔
196
            $valid->groups = $groups;
4✔
197
        }
198

199
        $metadata->addPropertyConstraint($property, $valid);
8✔
200
    }
8✔
201

202
    /**
203
     * Adds property constraints to the metadata object related to a ValidationNode object.
204
     */
205
    private function applyPropertyConstraints(ObjectMetadata $metadata, string|int $property, array $config): void
206
    {
207
        foreach ($config as $key => $value) {
19✔
208
            switch ($key) {
209
                case 'link':
19✔
210
                    // Add constraints from the linked class
211
                    $this->addLinkedConstraints((string) $property, $value, $metadata);
2✔
212
                    break;
2✔
213
                case 'constraints':
17✔
214
                    // Add constraints from the yml config directly
215
                    $metadata->addPropertyConstraints((string) $property, $value);
17✔
216
                    break;
17✔
217
                case 'cascade':
7✔
218
                    // Cascade validation was already handled recursively.
219
                    break;
7✔
220
            }
221
        }
222
    }
19✔
223

224
    private function addLinkedConstraints(string $property, array $link, ObjectMetadata $metadata, ): void
225
    {
226
        [$fqcn, $classProperty, $type] = $link;
2✔
227

228
        if (!in_array($fqcn, $this->cachedMetadata)) {
2✔
229
            /** @phpstan-ignore-next-line */
230
            $this->cachedMetadata[$fqcn] = $this->defaultValidator->getMetadataFor($fqcn);
2✔
231
        }
232

233
        // Get metadata from the property and its getters
234
        $propertyMetadata = $this->cachedMetadata[$fqcn]->getPropertyMetadata($classProperty);
2✔
235

236
        foreach ($propertyMetadata as $memberMetadata) {
2✔
237
            // Allow only constraints specified by the "link" matcher
238
            if (self::TYPE_GETTER === $type) {
2✔
239
                if (!$memberMetadata instanceof GetterMetadata) {
2✔
240
                    continue;
2✔
241
                }
242
            } elseif (self::TYPE_PROPERTY === $type) {
2✔
243
                if (!$memberMetadata instanceof PropertyMetadata) {
2✔
244
                    continue;
2✔
245
                }
246
            }
247

248
            $metadata->addPropertyConstraints($property, $memberMetadata->getConstraints());
2✔
249
        }
250
    }
2✔
251

252

253
    private function createCollectionNode(array $values, ObjectType|InputObjectType $type, ValidationNode $parent): array
254
    {
255
        $collection = [];
3✔
256

257
        foreach ($values as $value) {
3✔
258
            $collection[] = $this->createObjectNode($value, $type, $parent);
3✔
259
        }
260

261
        return $collection;
3✔
262
    }
263

264
    private function createObjectNode(array $value, ObjectType|InputObjectType $type, ValidationNode $parent): ValidationNode
265
    {
266
        /** @phpstan-ignore-next-line */
267
        $classValidation = Utils::normalizeConfig($type->config['validation'] ?? []);
8✔
268

269
        return $this->buildValidationTree(
8✔
270
            new ValidationNode($type, null, $parent, $this->resolverArgs),
8✔
271
            Utils::unclosure($type->config['fields']),
8✔
272
            $classValidation,
8✔
273
            $value
8✔
274
        );
275
    }
276

277
    private function applyClassValidation(ObjectMetadata $metadata, array $rules): void
278
    {
279
        $rules = Utils::normalizeConfig($rules);
17✔
280

281
        foreach ($rules as $key => $value) {
17✔
282
            switch ($key) {
283
                case 'link':
17✔
284
                    $linkedMetadata = $this->defaultValidator->getMetadataFor($value);
2✔
285
                    $metadata->addConstraints($linkedMetadata->getConstraints());
2✔
286
                    break;
2✔
287
                case 'constraints':
17✔
288
                    foreach (Utils::unclosure($value) as $constraint) {
17✔
289
                        if ($constraint instanceof Constraint) {
17✔
290
                            $metadata->addConstraint($constraint);
17✔
291
                        } elseif ($constraint instanceof GroupSequence) {
7✔
292
                            $metadata->setGroupSequence($constraint);
7✔
293
                        }
294
                    }
295
                    break;
17✔
296
            }
297
        }
298
    }
17✔
299

300
    /**
301
     * @throws ArgumentsValidationException
302
     */
303
    public function __invoke(array|string|null $groups = null, bool $throw = true): ?ConstraintViolationListInterface
304
    {
305
        return $this->validate($groups, $throw);
11✔
306
    }
307
}
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