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
25:10
created

InputValidator::mergeClassValidation()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 10
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 7
c 0
b 0
f 0
nc 1
nop 0
dl 0
loc 10
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\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
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 ResolverArgs $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
        ResolverArgs $resolverArgs,
50
        ValidatorInterface $validator,
51 17
        ConstraintValidatorFactoryInterface $constraintValidatorFactory,
52 1
        ?TranslatorInterface $translator
53 1
    ) {
54
        $this->resolverArgs = $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
     * @param string|array|null $groups
64 16
     *
65 16
     * @throws ArgumentsValidationException
66
     */
67
    public function validate($groups = null, bool $throw = true): ?ConstraintViolationListInterface
68
    {
69
        $rootNode = new ValidationNode(
70
            $this->info->parentType,
71
            $this->info->fieldName,
72
            null,
73 16
            $this->resolverArgs
74
        );
75
76 16
        $classMapping = $this->mergeClassValidation();
77 16
78 16
        $this->buildValidationTree(
79 16
            $rootNode,
80
            $this->info->fieldDefinition->config['args'],
81
            $classMapping,
82
            $this->resolverArgs->args->getArrayCopy()
83
        );
84
85
        $validator = $this->createValidator($this->metadataFactory);
86
87
        $errors = $validator->validate($rootNode, null, $groups);
88 16
89
        if ($throw && $errors->count() > 0) {
90 16
            throw new ArgumentsValidationException($errors);
91
        } else {
92 16
            return $errors;
93 16
        }
94 16
    }
95 16
96 16
    private function mergeClassValidation(): array
97
    {
98
        $common = static::normalizeConfig($this->info->parentType->config['validation'] ?? []);
99 16
        $specific = static::normalizeConfig($this->info->fieldDefinition->config['validation'] ?? []);
100
101 16
        return array_filter([
102
            'link' => $specific['link'] ?? $common['link'] ?? null,
103 16
            'constraints' => [
104 6
                ...($common['constraints'] ?? []),
105
                ...($specific['constraints'] ?? []),
106 10
            ],
107
        ]);
108
    }
109
110
    private function createValidator(MetadataFactory $metadataFactory): ValidatorInterface
111
    {
112
        $builder = Validation::createValidatorBuilder()
113
            ->setMetadataFactory($metadataFactory)
114 16
            ->setConstraintValidatorFactory($this->constraintValidatorFactory);
115
116 16
        if (null !== $this->defaultTranslator) {
117
            // @phpstan-ignore-next-line (only for Symfony 4.4)
118 16
            $builder
119 8
                ->setTranslator($this->defaultTranslator)
120
                ->setTranslationDomain('validators');
121
        }
122 16
123 16
        return $builder->getValidator();
124 6
    }
125
126
    /**
127 6
     * Creates a composition of ValidationNode objects from args
128
     * and simultaneously applies to them validation constraints.
129 6
     */
130 2
    protected function buildValidationTree(ValidationNode $rootObject, array $fields, array $classValidation, array $inputData): ValidationNode
131
    {
132 6
        $metadata = new ObjectMetadata($rootObject);
133
134
        if (!empty($classValidation)) {
135 6
            $this->applyClassValidation($metadata, $classValidation);
136
        }
137 6
138 4
        foreach ($fields as $name => $arg) {
139
            $property = $arg['name'] ?? $name;
140
            $config = static::normalizeConfig($arg['validation'] ?? []);
141 6
142
            if (isset($config['cascade']) && isset($inputData[$property])) {
143 16
                $groups = $config['cascade'];
144
                $argType = $this->unclosure($arg['type']);
145
146 16
                /** @var ObjectType|InputObjectType $type */
147
                $type = Type::getNamedType($argType);
148 16
149
                if (static::isListOfType($argType)) {
150 16
                    $rootObject->$property = $this->createCollectionNode($inputData[$property], $type, $rootObject);
151 2
                } else {
152
                    $rootObject->$property = $this->createObjectNode($inputData[$property], $type, $rootObject);
153 2
                }
154 2
155
                $valid = new Valid();
156
157
                if (!empty($groups)) {
158 2
                    $valid->groups = $groups;
159
                }
160 2
161
                $metadata->addPropertyConstraint($property, $valid);
162 2
            } else {
163 2
                $rootObject->$property = $inputData[$property] ?? null;
164 2
            }
165
166 2
            $config = static::normalizeConfig($config);
167 2
168 2
            foreach ($config as $key => $value) {
169
                switch ($key) {
170
                    case 'link':
171
                        [$fqcn, $property, $type] = $value;
172 2
173
                        if (!in_array($fqcn, $this->cachedMetadata)) {
174
                            $this->cachedMetadata[$fqcn] = $this->defaultValidator->getMetadataFor($fqcn);
175 2
                        }
176 14
177 14
                        // Get metadata from the property and it's getters
178 14
                        $propertyMetadata = $this->cachedMetadata[$fqcn]->getPropertyMetadata($property);
179 6
180 6
                        foreach ($propertyMetadata as $memberMetadata) {
181
                            // Allow only constraints specified by the "link" matcher
182
                            if (self::TYPE_GETTER === $type) {
183
                                if (!$memberMetadata instanceof GetterMetadata) {
184
                                    continue;
185 16
                                }
186
                            } elseif (self::TYPE_PROPERTY === $type) {
187 16
                                if (!$memberMetadata instanceof PropertyMetadata) {
188
                                    continue;
189
                                }
190
                            }
191
192
                            $metadata->addPropertyConstraints($property, $memberMetadata->getConstraints());
193 2
                        }
194
195 2
                        break;
196
                    case 'constraints':
197 2
                        $metadata->addPropertyConstraints($property, $value);
198 2
                        break;
199
                    case 'cascade':
200
                        break;
201 2
                }
202
            }
203
        }
204
205
        $this->metadataFactory->addMetadata($metadata);
206
207 6
        return $rootObject;
208
    }
209 6
210 6
    /**
211
     * @param GeneratedTypeInterface|ListOfType|NonNull $type
212 6
     */
213 6
    private static function isListOfType($type): bool
214
    {
215
        if ($type instanceof ListOfType || ($type instanceof NonNull && $type->getOfType() instanceof ListOfType)) {
216 6
            return true;
217 6
        }
218
219
        return false;
220
    }
221
222
    /**
223
     * @param ObjectType|InputObjectType $type
224 8
     */
225
    private function createCollectionNode(array $values, $type, ValidationNode $parent): array
226 8
    {
227
        $collection = [];
228 8
229
        foreach ($values as $value) {
230 8
            $collection[] = $this->createObjectNode($value, $type, $parent);
231 2
        }
232 2
233 2
        return $collection;
234 8
    }
235 8
236 8
    /**
237 2
     * @param ObjectType|InputObjectType $type
238 6
     */
239 6
    private function createObjectNode(array $value, $type, ValidationNode $parent): ValidationNode
240
    {
241
        $classValidation = static::normalizeConfig($type->config['validation'] ?? []);
242 8
243
        return $this->buildValidationTree(
244
            new ValidationNode($type, null, $parent, $this->resolverArgs),
245 8
            self::unclosure($type->config['fields']),
0 ignored issues
show
Bug Best Practice introduced by
The method Overblog\GraphQLBundle\V...tValidator::unclosure() is not static, but was called statically. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

245
            self::/** @scrutinizer ignore-call */ 
246
                  unclosure($type->config['fields']),
Loading history...
246
            $classValidation,
247 16
            $value
248
        );
249 16
    }
250 14
251
    private function applyClassValidation(ObjectMetadata $metadata, array $rules): void
252 16
    {
253
        $rules = static::normalizeConfig($rules);
254
255
        foreach ($rules as $key => $value) {
256
            switch ($key) {
257
                case 'link':
258
                    $linkedMetadata = $this->defaultValidator->getMetadataFor($value);
259 10
                    $metadata->addConstraints($linkedMetadata->getConstraints());
260
                    break;
261 10
                case 'constraints':
262
                    foreach ($this->unclosure($value) as $constraint) {
263
                        if ($constraint instanceof Constraint) {
264
                            $metadata->addConstraint($constraint);
265
                        } elseif ($constraint instanceof GroupSequence) {
266
                            $metadata->setGroupSequence($constraint);
267
                        }
268
                    }
269
                    break;
270
            }
271
        }
272
    }
273
274
    /**
275
     * Restructures short forms into the full form array and
276
     * unwraps constraints in closures.
277
     *
278
     * @param mixed $config
279
     */
280
    public static function normalizeConfig($config): array
281
    {
282
        if ($config instanceof Closure) {
283
            return ['constraints' => $config()];
284
        }
285
286
        if (self::CASCADE === $config) {
287
            return ['cascade' => []];
288
        }
289
290
        if (isset($config['constraints']) && $config['constraints'] instanceof Closure) {
291
            $config['constraints'] = $config['constraints']();
292
        }
293
294
        return $config;
295
    }
296
297
    /**
298
     * @param mixed $value
299
     *
300
     * @return mixed
301
     */
302
    private function unclosure($value)
303
    {
304
        if ($value instanceof Closure) {
305
            return $value();
306
        }
307
308
        return $value;
309
    }
310
311
    /**
312
     * @param string|array|null $groups
313
     *
314
     * @throws ArgumentsValidationException
315
     */
316
    public function __invoke($groups = null, bool $throw = true): ?ConstraintViolationListInterface
317
    {
318
        return $this->validate($groups, $throw);
319
    }
320
}
321