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
22:02
created

InputValidator::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 1

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 1
eloc 6
c 2
b 0
f 0
nc 1
nop 4
dl 0
loc 12
rs 10
ccs 5
cts 5
cp 1
crap 1
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
156
                if ($arg['type'] instanceof Closure) {
157
                    $arg['type'] = $arg['type']();
158 2
                }
159
160 2
                /** @var ObjectType|InputObjectType $type */
161
                $type = Type::getNamedType($arg['type']);
162 2
163 2
                if (static::isListOfType($arg['type'])) {
164 2
                    $rootObject->$property = $this->createCollectionNode($inputData[$property], $type, $rootObject);
165
                } else {
166 2
                    $rootObject->$property = $this->createObjectNode($inputData[$property], $type, $rootObject);
167 2
                }
168 2
169
                $valid = new Valid();
170
171
                if (!empty($groups)) {
172 2
                    $valid->groups = $groups;
173
                }
174
175 2
                $metadata->addPropertyConstraint($property, $valid);
176 14
            } else {
177 14
                $rootObject->$property = $inputData[$property] ?? null;
178 14
            }
179 6
180 6
            $validation = static::normalizeConfig($validation);
181
182
            foreach ($validation as $key => $value) {
183
                switch ($key) {
184
                    case 'link':
185 16
                        [$fqcn, $property, $type] = $value;
186
187 16
                        if (!in_array($fqcn, $this->cachedMetadata)) {
188
                            $this->cachedMetadata[$fqcn] = $this->defaultValidator->getMetadataFor($fqcn);
189
                        }
190
191
                        // Get metadata from the property and it's getters
192
                        $propertyMetadata = $this->cachedMetadata[$fqcn]->getPropertyMetadata($property);
193 2
194
                        foreach ($propertyMetadata as $memberMetadata) {
195 2
                            // Allow only constraints specified by the "link" matcher
196
                            if (self::TYPE_GETTER === $type) {
197 2
                                if (!$memberMetadata instanceof GetterMetadata) {
198 2
                                    continue;
199
                                }
200
                            } elseif (self::TYPE_PROPERTY === $type) {
201 2
                                if (!$memberMetadata instanceof PropertyMetadata) {
202
                                    continue;
203
                                }
204
                            }
205
206
                            $metadata->addPropertyConstraints($property, $memberMetadata->getConstraints());
207 6
                        }
208
209 6
                        break;
210 6
                    case 'constraints':
211
                        $metadata->addPropertyConstraints($property, $value);
212 6
                        break;
213 6
                    case 'cascade':
214
                        break;
215
                }
216 6
            }
217 6
        }
218
219
        $this->metadataFactory->addMetadata($metadata);
220
221
        return $rootObject;
222
    }
223
224 8
    /**
225
     * @param GeneratedTypeInterface|ListOfType|NonNull $type
226 8
     */
227
    private static function isListOfType($type): bool
228 8
    {
229
        if ($type instanceof ListOfType || ($type instanceof NonNull && $type->getOfType() instanceof ListOfType)) {
230 8
            return true;
231 2
        }
232 2
233 2
        return false;
234 8
    }
235 8
236 8
    /**
237 2
     * @param ObjectType|InputObjectType $type
238 6
     */
239 6
    private function createCollectionNode(array $values, $type, ValidationNode $parent): array
240
    {
241
        $collection = [];
242 8
243
        foreach ($values as $value) {
244
            $collection[] = $this->createObjectNode($value, $type, $parent);
245 8
        }
246
247 16
        return $collection;
248
    }
249 16
250 14
    /**
251
     * @param ObjectType|InputObjectType $type
252 16
     */
253
    private function createObjectNode(array $value, $type, ValidationNode $parent): ValidationNode
254
    {
255
        $classValidation = static::normalizeConfig($type->config['validation'] ?? []);
256
257
        return $this->buildValidationTree(
258
            new ValidationNode($type, null, $parent, $this->resolverArgs),
259 10
            $type->config['fields'](),
260
            $classValidation,
261 10
            $value
262
        );
263
    }
264
265
    private function applyClassValidation(ObjectMetadata $metadata, array $rules): void
266
    {
267
        $rules = static::normalizeConfig($rules);
268
269
        foreach ($rules as $key => $value) {
270
            switch ($key) {
271
                case 'link':
272
                    $linkedMetadata = $this->defaultValidator->getMetadataFor($value);
273
                    $metadata->addConstraints($linkedMetadata->getConstraints());
274
                    break;
275
                case 'constraints':
276
                    if ($value instanceof Closure) {
277
                        $value = $value();
278
                    }
279
                    foreach ($value as $constraint) {
280
                        if ($constraint instanceof Constraint) {
281
                            $metadata->addConstraint($constraint);
282
                        } elseif ($constraint instanceof GroupSequence) {
283
                            $metadata->setGroupSequence($constraint);
284
                        }
285
                    }
286
                    break;
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) {
300
            return ['constraints' => $config()];
301
        }
302
303
        if (self::CASCADE === $config) {
304
            return ['cascade' => []];
305
        }
306
307
        if (isset($config['constraints']) && $config['constraints'] instanceof Closure) {
308
            $config['constraints'] = $config['constraints']();
309
        }
310
311
        return $config;
312
    }
313
314
    /**
315
     * @param string|array|null $groups
316
     *
317
     * @throws ArgumentsValidationException
318
     */
319
    public function __invoke($groups = null, bool $throw = true): ?ConstraintViolationListInterface
320
    {
321
        return $this->validate($groups, $throw);
322
    }
323
}
324