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

overblog / GraphQLBundle / 21415119768

27 Jan 2026 09:35PM UTC coverage: 98.346% (+0.06%) from 98.283%
21415119768

Pull #1232

github

web-flow
Merge b724fd32b into 63c710b33
Pull Request #1232: Bump php version to at least 8.1

4520 of 4596 relevant lines covered (98.35%)

38.7 hits per line

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

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

3
declare(strict_types=1);
4

5
namespace Overblog\GraphQLBundle\Validator;
6

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

31
use function in_array;
32

33
final class InputValidator
34
{
35
    private const TYPE_PROPERTY = 'property';
36
    private const TYPE_GETTER = 'getter';
37
    public const CASCADE = 'cascade';
38

39
    private ResolverArgs $resolverArgs;
40
    private ValidatorInterface $defaultValidator;
41
    private MetadataFactory $metadataFactory;
42
    private ResolveInfo $info;
43
    private ConstraintValidatorFactoryInterface $constraintValidatorFactory;
44
    private ?TranslatorInterface $defaultTranslator;
45

46
    /** @var ClassMetadataInterface[] */
47
    private array $cachedMetadata = [];
48

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

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

75
        $classMapping = $this->mergeClassValidation();
19✔
76

77
        $this->buildValidationTree(
19✔
78
            $rootNode,
19✔
79
            $this->info->fieldDefinition->config['args'] ?? [],
19✔
80
            $classMapping,
19✔
81
            $this->resolverArgs->args->getArrayCopy()
19✔
82
        );
19✔
83

84
        $validator = $this->createValidator($this->metadataFactory);
19✔
85

86
        $errors = $validator->validate($rootNode, null, $groups);
19✔
87

88
        if ($throw && $errors->count() > 0) {
19✔
89
            throw new ArgumentsValidationException($errors);
7✔
90
        } else {
91
            return $errors;
12✔
92
        }
93
    }
94

95
    private function mergeClassValidation(): array
96
    {
97
        /** @phpstan-ignore-next-line */
98
        $common = static::normalizeConfig($this->info->parentType->config['validation'] ?? []);
19✔
99
        /** @phpstan-ignore-next-line */
100
        $specific = static::normalizeConfig($this->info->fieldDefinition->config['validation'] ?? []);
19✔
101

102
        return array_filter([
19✔
103
            'link' => $specific['link'] ?? $common['link'] ?? null,
19✔
104
            'constraints' => [
19✔
105
                ...($common['constraints'] ?? []),
19✔
106
                ...($specific['constraints'] ?? []),
19✔
107
            ],
19✔
108
        ]);
19✔
109
    }
110

111
    private function createValidator(MetadataFactory $metadataFactory): ValidatorInterface
112
    {
113
        $builder = Validation::createValidatorBuilder()
19✔
114
            ->setMetadataFactory($metadataFactory)
19✔
115
            ->setConstraintValidatorFactory($this->constraintValidatorFactory);
19✔
116

117
        if (null !== $this->defaultTranslator) {
19✔
118
            // @phpstan-ignore-next-line (only for Symfony 4.4)
119
            $builder
×
120
                ->setTranslator($this->defaultTranslator)
×
121
                ->setTranslationDomain('validators');
×
122
        }
123

124
        return $builder->getValidator();
19✔
125
    }
126

127
    private function getMetadata(ValidationNode $rootObject): ObjectMetadata
128
    {
129
        // Return existing metadata if present
130
        if ($this->metadataFactory->hasMetadataFor($rootObject)) {
19✔
131
            return $this->metadataFactory->getMetadataFor($rootObject);
1✔
132
        }
133

134
        // Create new metadata and add it to the factory
135
        $metadata = new ObjectMetadata($rootObject);
19✔
136
        $this->metadataFactory->addMetadata($metadata);
19✔
137

138
        return $metadata;
19✔
139
    }
140

141
    /**
142
     * Creates a composition of ValidationNode objects from args
143
     * and simultaneously applies to them validation constraints.
144
     */
145
    private function buildValidationTree(ValidationNode $rootObject, iterable $fields, array $classValidation, array $inputData): ValidationNode
146
    {
147
        $metadata = $this->getMetadata($rootObject);
19✔
148

149
        if (!empty($classValidation)) {
19✔
150
            $this->applyClassValidation($metadata, $classValidation);
17✔
151
        }
152

153
        foreach ($fields as $name => $arg) {
19✔
154
            $property = $arg['name'] ?? $name;
19✔
155
            $config = static::normalizeConfig($arg['validation'] ?? []);
19✔
156

157
            if (isset($config['cascade']) && isset($inputData[$property])) {
19✔
158
                $argType = $this->unclosure($arg['type']);
8✔
159

160
                /** @var ObjectType|InputObjectType $type */
161
                $type = Type::getNamedType($argType);
8✔
162

163
                if (static::isListOfType($argType)) {
8✔
164
                    $rootObject->$property = $this->createCollectionNode($inputData[$property], $type, $rootObject);
3✔
165
                } else {
166
                    $rootObject->$property = $this->createObjectNode($inputData[$property], $type, $rootObject);
8✔
167
                }
168

169
                $valid = new Valid();
8✔
170
                $groups = $config['cascade'];
8✔
171

172
                if (!empty($groups)) {
8✔
173
                    $valid->groups = $groups;
4✔
174
                }
175

176
                // Apply the Assert/Valid constraint for a recursive validation.
177
                // For more details see https://symfony.com/doc/current/reference/constraints/Valid.html
178
                $metadata->addPropertyConstraint($property, $valid);
8✔
179

180
                // Skip the rest as the validation was delegated to the nested object.
181
                continue;
8✔
182
            } else {
183
                $rootObject->$property = $inputData[$property] ?? null;
19✔
184
            }
185

186
            if ($metadata->hasPropertyMetadata($property)) {
19✔
187
                continue;
1✔
188
            }
189

190
            $config = static::normalizeConfig($config);
19✔
191

192
            // Apply validation constraints for the property
193
            foreach ($config as $key => $value) {
19✔
194
                switch ($key) {
195
                    case 'link':
19✔
196
                        [$fqcn, $classProperty, $type] = $value;
2✔
197

198
                        if (!in_array($fqcn, $this->cachedMetadata)) {
2✔
199
                            /** @phpstan-ignore-next-line */
200
                            $this->cachedMetadata[$fqcn] = $this->defaultValidator->getMetadataFor($fqcn);
2✔
201
                        }
202

203
                        // Get metadata from the property and it's getters
204
                        $propertyMetadata = $this->cachedMetadata[$fqcn]->getPropertyMetadata($classProperty);
2✔
205

206
                        foreach ($propertyMetadata as $memberMetadata) {
2✔
207
                            // Allow only constraints specified by the "link" matcher
208
                            if (self::TYPE_GETTER === $type) {
2✔
209
                                if (!$memberMetadata instanceof GetterMetadata) {
2✔
210
                                    continue;
2✔
211
                                }
212
                            } elseif (self::TYPE_PROPERTY === $type) {
2✔
213
                                if (!$memberMetadata instanceof PropertyMetadata) {
2✔
214
                                    continue;
2✔
215
                                }
216
                            }
217

218
                            $metadata->addPropertyConstraints($property, $memberMetadata->getConstraints());
2✔
219
                        }
220

221
                        break;
2✔
222
                    case 'constraints': // Add constraint from the yml config
17✔
223
                        $metadata->addPropertyConstraints($property, $value);
17✔
224
                        break;
17✔
225
                    case 'cascade':
7✔
226
                        // Cascade validation was already handled recursively.
227
                        break;
7✔
228
                }
229
            }
230
        }
231

232
        return $rootObject;
19✔
233
    }
234

235
    private static function isListOfType(GeneratedTypeInterface|ListOfType|NonNull $type): bool
236
    {
237
        if ($type instanceof ListOfType || ($type instanceof NonNull && $type->getWrappedType() instanceof ListOfType)) {
8✔
238
            return true;
3✔
239
        }
240

241
        return false;
8✔
242
    }
243

244
    private function createCollectionNode(array $values, ObjectType|InputObjectType $type, ValidationNode $parent): array
245
    {
246
        $collection = [];
3✔
247

248
        foreach ($values as $value) {
3✔
249
            $collection[] = $this->createObjectNode($value, $type, $parent);
3✔
250
        }
251

252
        return $collection;
3✔
253
    }
254

255
    private function createObjectNode(array $value, ObjectType|InputObjectType $type, ValidationNode $parent): ValidationNode
256
    {
257
        /** @phpstan-ignore-next-line */
258
        $classValidation = static::normalizeConfig($type->config['validation'] ?? []);
8✔
259

260
        return $this->buildValidationTree(
8✔
261
            new ValidationNode($type, null, $parent, $this->resolverArgs),
8✔
262
            self::unclosure($type->config['fields']),
8✔
263
            $classValidation,
8✔
264
            $value
8✔
265
        );
8✔
266
    }
267

268
    private function applyClassValidation(ObjectMetadata $metadata, array $rules): void
269
    {
270
        $rules = static::normalizeConfig($rules);
17✔
271

272
        foreach ($rules as $key => $value) {
17✔
273
            switch ($key) {
274
                case 'link':
17✔
275
                    $linkedMetadata = $this->defaultValidator->getMetadataFor($value);
2✔
276
                    $metadata->addConstraints($linkedMetadata->getConstraints());
2✔
277
                    break;
2✔
278
                case 'constraints':
17✔
279
                    foreach ($this->unclosure($value) as $constraint) {
17✔
280
                        if ($constraint instanceof Constraint) {
17✔
281
                            $metadata->addConstraint($constraint);
17✔
282
                        } elseif ($constraint instanceof GroupSequence) {
7✔
283
                            $metadata->setGroupSequence($constraint);
7✔
284
                        }
285
                    }
286
                    break;
17✔
287
            }
288
        }
289
    }
290

291
    /**
292
     * Restructures short forms into the full form array and
293
     * unwraps constraints in closures.
294
     *
295
     * @param mixed $config
296
     */
297
    public static function normalizeConfig($config): array
298
    {
299
        if ($config instanceof Closure) {
19✔
300
            return ['constraints' => $config()];
19✔
301
        }
302

303
        if (self::CASCADE === $config) {
19✔
304
            return ['cascade' => []];
8✔
305
        }
306

307
        if (isset($config['constraints']) && $config['constraints'] instanceof Closure) {
19✔
308
            $config['constraints'] = $config['constraints']();
2✔
309
        }
310

311
        return $config;
19✔
312
    }
313

314
    /**
315
     * @param mixed $value
316
     *
317
     * @return mixed
318
     */
319
    private function unclosure($value)
320
    {
321
        if ($value instanceof Closure) {
18✔
322
            return $value();
8✔
323
        }
324

325
        return $value;
17✔
326
    }
327

328
    /**
329
     * @param string|array|null $groups
330
     *
331
     * @throws ArgumentsValidationException
332
     */
333
    public function __invoke($groups = null, bool $throw = true): ?ConstraintViolationListInterface
334
    {
335
        return $this->validate($groups, $throw);
11✔
336
    }
337
}
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