Scrutinizer GitHub App not installed

We could not synchronize checks via GitHub's checks API since Scrutinizer's GitHub App is not installed for this repository.

Install GitHub App

Passed
Pull Request — master (#815)
by Timur
23:43
created

InputValidator::applyClassConstraints()   B

Complexity

Conditions 7
Paths 7

Size

Total Lines 19
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 7

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 14
c 1
b 0
f 0
dl 0
loc 19
ccs 14
cts 14
cp 1
rs 8.8333
cc 7
nc 7
nop 2
crap 7
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\ArgumentInterface;
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
use function in_array;
31
32
class InputValidator
33
{
34
    private const TYPE_PROPERTY = 'property';
35
    private const TYPE_GETTER = 'getter';
36
    public const CASCADE = 'cascade';
37
38
    private array $resolverArgs;
39
    private ValidatorInterface $defaultValidator;
40
    private MetadataFactory $metadataFactory;
41
    private ResolveInfo $info;
42
    private ConstraintValidatorFactoryInterface $constraintValidatorFactory;
43
    private ?TranslatorInterface $defaultTranslator;
44 17
45
    /** @var ClassMetadataInterface[] */
46
    private array $cachedMetadata = [];
47
48
    public function __construct(
49
        array $resolverArgs,
50
        ValidatorInterface $validator,
51 17
        ConstraintValidatorFactoryInterface $constraintValidatorFactory,
52 1
        ?TranslatorInterface $translator
53 1
    ) {
54
        $this->resolverArgs = $this->mapResolverArgs(...$resolverArgs);
55
        $this->info = $this->resolverArgs['info'];
56
        $this->defaultValidator = $validator;
57
        $this->constraintValidatorFactory = $constraintValidatorFactory;
58 16
        $this->defaultTranslator = $translator;
59 16
        $this->metadataFactory = new MetadataFactory();
60 16
    }
61 16
62 16
    /**
63 16
     * Converts a numeric array of resolver args to an associative one.
64 16
     *
65 16
     * @param mixed $value
66
     * @param mixed $context
67
     */
68
    private function mapResolverArgs($value, ArgumentInterface $args, $context, ResolveInfo $info): array
69
    {
70
        return [
71
            'value' => $value,
72
            'args' => $args,
73 16
            'context' => $context,
74
            'info' => $info,
75
        ];
76 16
    }
77 16
78 16
    /**
79 16
     * @param string|array|null $groups
80
     *
81
     * @throws ArgumentsValidationException
82
     */
83
    public function validate($groups = null, bool $throw = true): ?ConstraintViolationListInterface
84
    {
85
        $rootObject = new ValidationNode($this->info->parentType, $this->info->fieldName, null, $this->resolverArgs);
86
87
        $classMapping = $this->mergeClassValidation();
88 16
89
        $this->buildValidationTree(
90 16
            $rootObject,
91
            $this->info->fieldDefinition->config['args'],
92 16
            $classMapping,
93 16
            $this->resolverArgs['args']->getArrayCopy()
94 16
        );
95 16
96 16
        $validator = $this->createValidator($this->metadataFactory);
97
98
        $errors = $validator->validate($rootObject, null, $groups);
99 16
100
        if ($throw && $errors->count() > 0) {
101 16
            throw new ArgumentsValidationException($errors);
102
        } else {
103 16
            return $errors;
104 6
        }
105
    }
106 10
107
    private function mergeClassValidation(): array
108
    {
109
        $common = static::normalizeConfig($this->info->parentType->config['validation'] ?? []);
110
        $specific = static::normalizeConfig($this->info->fieldDefinition->config['validation'] ?? []);
111
112
        return array_filter([
113
            'link' => $specific['link'] ?? $common['link'] ?? null,
114 16
            'constraints' => [
115
                ...($common['constraints'] ?? []),
116 16
                ...($specific['constraints'] ?? []),
117
            ],
118 16
        ]);
119 8
    }
120
121
    private function createValidator(MetadataFactory $metadataFactory): ValidatorInterface
122 16
    {
123 16
        $builder = Validation::createValidatorBuilder()
124 6
            ->setMetadataFactory($metadataFactory)
125
            ->setConstraintValidatorFactory($this->constraintValidatorFactory);
126
127 6
        if (null !== $this->defaultTranslator) {
128
            // @phpstan-ignore-next-line (only for Symfony 4.4)
129 6
            $builder
130 2
                ->setTranslator($this->defaultTranslator)
131
                ->setTranslationDomain('validators');
132 6
        }
133
134
        return $builder->getValidator();
135 6
    }
136
137 6
    /**
138 4
     * Creates a composition of ValidationNode objects from args
139
     * and simultaneously applies to them validation constraints.
140
     */
141 6
    protected function buildValidationTree(ValidationNode $rootObject, array $fields, array $classValidation, array $inputData): ValidationNode
142
    {
143 16
        $metadata = new ObjectMetadata($rootObject);
144
145
        if (!empty($classValidation)) {
146 16
            $this->applyClassValidation($metadata, $classValidation);
147
        }
148 16
149
        foreach ($fields as $name => $arg) {
150 16
            $property = $arg['name'] ?? $name;
151 2
            $validation = static::normalizeConfig($arg['validation'] ?? []);
152
153 2
            if (isset($validation['cascade']) && isset($inputData[$property])) {
154 2
                $groups = $validation['cascade'];
155
                $argType = $this->unclosure($arg['type']);
156
157
                /** @var ObjectType|InputObjectType $type */
158 2
                $type = Type::getNamedType($argType);
159
160 2
                if (static::isListOfType($argType)) {
161
                    $rootObject->$property = $this->createCollectionNode($inputData[$property], $type, $rootObject);
162 2
                } else {
163 2
                    $rootObject->$property = $this->createObjectNode($inputData[$property], $type, $rootObject);
164 2
                }
165
166 2
                $valid = new Valid();
167 2
168 2
                if (!empty($groups)) {
169
                    $valid->groups = $groups;
170
                }
171
172 2
                $metadata->addPropertyConstraint($property, $valid);
173
            } else {
174
                $rootObject->$property = $inputData[$property] ?? null;
175 2
            }
176 14
177 14
            $validation = static::normalizeConfig($validation);
178 14
179 6
            foreach ($validation as $key => $value) {
180 6
                switch ($key) {
181
                    case 'link':
182
                        [$fqcn, $property, $type] = $value;
183
184
                        if (!in_array($fqcn, $this->cachedMetadata)) {
185 16
                            $this->cachedMetadata[$fqcn] = $this->defaultValidator->getMetadataFor($fqcn);
186
                        }
187 16
188
                        // Get metadata from the property and it's getters
189
                        $propertyMetadata = $this->cachedMetadata[$fqcn]->getPropertyMetadata($property);
190
191
                        foreach ($propertyMetadata as $memberMetadata) {
192
                            // Allow only constraints specified by the "link" matcher
193 2
                            if (self::TYPE_GETTER === $type) {
194
                                if (!$memberMetadata instanceof GetterMetadata) {
195 2
                                    continue;
196
                                }
197 2
                            } elseif (self::TYPE_PROPERTY === $type) {
198 2
                                if (!$memberMetadata instanceof PropertyMetadata) {
199
                                    continue;
200
                                }
201 2
                            }
202
203
                            $metadata->addPropertyConstraints($property, $memberMetadata->getConstraints());
204
                        }
205
206
                        break;
207 6
                    case 'constraints':
208
                        $metadata->addPropertyConstraints($property, $value);
209 6
                        break;
210 6
                    case 'cascade':
211
                        break;
212 6
                }
213 6
            }
214
        }
215
216 6
        $this->metadataFactory->addMetadata($metadata);
217 6
218
        return $rootObject;
219
    }
220
221
    /**
222
     * @param GeneratedTypeInterface|ListOfType|NonNull $type
223
     */
224 8
    private static function isListOfType($type): bool
225
    {
226 8
        if ($type instanceof ListOfType || ($type instanceof NonNull && $type->getOfType() instanceof ListOfType)) {
227
            return true;
228 8
        }
229
230 8
        return false;
231 2
    }
232 2
233 2
    /**
234 8
     * @param ObjectType|InputObjectType $type
235 8
     */
236 8
    private function createCollectionNode(array $values, $type, ValidationNode $parent): array
237 2
    {
238 6
        $collection = [];
239 6
240
        foreach ($values as $value) {
241
            $collection[] = $this->createObjectNode($value, $type, $parent);
242 8
        }
243
244
        return $collection;
245 8
    }
246
247 16
    /**
248
     * @param ObjectType|InputObjectType $type
249 16
     */
250 14
    private function createObjectNode(array $value, $type, ValidationNode $parent): ValidationNode
251
    {
252 16
        $classValidation = static::normalizeConfig($type->config['validation'] ?? []);
253
254
        return $this->buildValidationTree(
255
            new ValidationNode($type, null, $parent, $this->resolverArgs),
256
            $type->config['fields'](),
257
            $classValidation,
258
            $value
259 10
        );
260
    }
261 10
262
    private function applyClassValidation(ObjectMetadata $metadata, array $rules): void
263
    {
264
        $rules = static::normalizeConfig($rules);
265
266
        foreach ($rules as $key => $value) {
267
            switch ($key) {
268
                case 'link':
269
                    $linkedMetadata = $this->defaultValidator->getMetadataFor($value);
270
                    $metadata->addConstraints($linkedMetadata->getConstraints());
271
                    break;
272
                case 'constraints':
273
                    foreach ($this->unclosure($value) as $constraint) {
274
                        if ($constraint instanceof Constraint) {
275
                            $metadata->addConstraint($constraint);
276
                        } elseif ($constraint instanceof GroupSequence) {
277
                            $metadata->setGroupSequence($constraint);
278
                        }
279
                    }
280
                    break;
281
            }
282
        }
283
    }
284
285
    /**
286
     * Restructures short forms into the full form array and
287
     * unwraps constraints in closures.
288
     *
289
     * @param mixed $config
290
     */
291
    public static function normalizeConfig($config): array
292
    {
293
        if ($config instanceof Closure) {
294
            return ['constraints' => $config()];
295
        }
296
297
        if (self::CASCADE === $config) {
298
            return ['cascade' => []];
299
        }
300
301
        if (isset($config['constraints']) && $config['constraints'] instanceof Closure) {
302
            $config['constraints'] = $config['constraints']();
303
        }
304
305
        return $config;
306
    }
307
308
    /**
309
     * @param mixed $value
310
     *
311
     * @return mixed
312
     */
313
    private function unclosure($value)
314
    {
315
        if ($value instanceof Closure) {
316
            return $value();
317
        }
318
319
        return $value;
320
    }
321
322
    /**
323
     * @param string|array|null $groups
324
     *
325
     * @throws ArgumentsValidationException
326
     */
327
    public function __invoke($groups = null, bool $throw = true): ?ConstraintViolationListInterface
328
    {
329
        return $this->validate($groups, $throw);
330
    }
331
}
332