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
Push — 0.13 ( 2f8699...07e04a )
by Jérémiah
03:05
created

InputValidator   A

Complexity

Total Complexity 36

Size/Duplication

Total Lines 264
Duplicated Lines 0 %

Test Coverage

Coverage 97.8%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 36
eloc 99
dl 0
loc 264
ccs 89
cts 91
cp 0.978
rs 9.52
c 1
b 0
f 0

8 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 15 2
A createCollectionNode() 0 9 2
A mapResolverArgs() 0 7 1
A validate() 0 14 3
A createObjectNode() 0 11 2
A __invoke() 0 3 1
B applyClassConstraints() 0 21 8
C buildValidationTree() 0 72 17
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\Validator\Exception\ArgumentsValidationException;
12
use Overblog\GraphQLBundle\Validator\Mapping\MetadataFactory;
13
use Overblog\GraphQLBundle\Validator\Mapping\ObjectMetadata;
14
use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;
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\ConstraintViolationListInterface;
19
use Symfony\Component\Validator\Mapping\ClassMetadataInterface;
20
use Symfony\Component\Validator\Mapping\GetterMetadata;
21
use Symfony\Component\Validator\Mapping\PropertyMetadata;
22
use Symfony\Component\Validator\Validator\ValidatorInterface;
23
24
class InputValidator
25
{
26
    private const TYPE_PROPERTY = 'property';
27
    private const TYPE_GETTER = 'getter';
28
29
    /**
30
     * @var array
31
     */
32
    private $resolverArgs;
33
34
    /**
35
     * @var array
36
     */
37
    private $constraintMapping;
38
39
    /**
40
     * @var ValidatorInterface
41
     */
42
    private $validator;
43
44
    /**
45
     * @var MetadataFactory
46
     */
47
    private $metadataFactory;
48
49
    /**
50
     * @var ResolveInfo
51
     */
52
    private $info;
53
54
    /**
55
     * @var ValidatorFactory
56
     */
57
    private $validatorFactory;
58
59
    /**
60
     * @var ClassMetadataInterface[]
61
     */
62
    private $cachedMetadata = [];
63
64
    /**
65
     * InputValidator constructor.
66
     *
67
     * @param array                   $resolverArgs
68
     * @param ValidatorInterface|null $validator
69
     * @param ValidatorFactory        $factory
70
     * @param array                   $mapping
71
     */
72 18
    public function __construct(array $resolverArgs, ?ValidatorInterface $validator, ValidatorFactory $factory, array $mapping)
73
    {
74 18
        if (null === $validator) {
75 1
            throw new ServiceNotFoundException(
76 1
                "The 'validator' service is not found. To use the 'InputValidator' you need to install the 
77
                Symfony Validator Component first. See: 'https://symfony.com/doc/current/validation.html'"
78
            );
79
        }
80
81 17
        $this->resolverArgs = $this->mapResolverArgs($resolverArgs);
82 17
        $this->info = $this->resolverArgs['info'];
83 17
        $this->constraintMapping = $mapping;
84 17
        $this->validator = $validator;
85 17
        $this->validatorFactory = $factory;
86 17
        $this->metadataFactory = new MetadataFactory();
87 17
    }
88
89
    /**
90
     * Converts a numeric array of resolver args to an associative one.
91
     *
92
     * @param array $rawReolverArgs
93
     *
94
     * @return array
95
     */
96 17
    private function mapResolverArgs(array $rawReolverArgs)
97
    {
98
        return [
99 17
            'value' => $rawReolverArgs[0],
100 17
            'args' => $rawReolverArgs[1],
101 17
            'context' => $rawReolverArgs[2],
102 17
            'info' => $rawReolverArgs[3],
103
        ];
104
    }
105
106
    /**
107
     * @param string|array|null $groups
108
     * @param bool              $throw
109
     *
110
     * @return ConstraintViolationListInterface|null
111
     *
112
     * @throws ArgumentsValidationException
113
     */
114 17
    public function validate($groups = null, bool $throw = true): ?ConstraintViolationListInterface
115
    {
116 17
        $rootObject = new ValidationNode($this->info->parentType, $this->info->fieldName, null, $this->resolverArgs);
117
118 17
        $this->buildValidationTree($rootObject, $this->constraintMapping, $this->resolverArgs['args']->getArrayCopy());
119
120 17
        $validator = $this->validatorFactory->createValidator($this->metadataFactory);
121
122 17
        $errors = $validator->validate($rootObject, null, $groups);
123
124 17
        if ($throw && $errors->count() > 0) {
125 6
            throw new ArgumentsValidationException($errors);
126
        } else {
127 11
            return $errors;
128
        }
129
    }
130
131
    /**
132
     * Creates a composition of ValidationNode objects from args
133
     * and simultaneously applies to them validation constraints.
134
     *
135
     * @param ValidationNode $rootObject
136
     * @param array          $constraintMapping
137
     * @param array          $args
138
     *
139
     * @return ValidationNode
140
     */
141 17
    protected function buildValidationTree(ValidationNode $rootObject, array $constraintMapping, array $args): ValidationNode
142
    {
143 17
        $metadata = new ObjectMetadata($rootObject);
144
145 17
        $this->applyClassConstraints($metadata, $constraintMapping['class']);
146
147 17
        foreach ($constraintMapping['properties'] as $property => $params) {
148 17
            if (!empty($params['cascade']) && isset($args[$property])) {
149 6
                $options = $params['cascade'];
150 6
                $type = $this->info->schema->getType($options['referenceType']);
151
152 6
                if ($options['isCollection']) {
153 2
                    $rootObject->$property = $this->createCollectionNode($args[$property], $type, $rootObject);
154
                } else {
155 6
                    $rootObject->$property = $this->createObjectNode($args[$property], $type, $rootObject);
156
                }
157
158 6
                $valid = new Valid();
159
160 6
                if (!empty($options['groups'])) {
161 4
                    $valid->groups = $options['groups'];
162
                }
163
164 6
                $metadata->addPropertyConstraint($property, $valid);
165
            } else {
166 17
                $rootObject->$property = $args[$property] ?? null;
167
            }
168
169 17
            foreach ($params ?? [] as $key => $value) {
170 17
                if (null === $value) {
171 17
                    continue;
172
                }
173
174
                switch ($key) {
175 16
                    case 'link':
176 2
                        [$fqcn, $property, $type] = $value;
177
178 2
                        if (!\in_array($fqcn, $this->cachedMetadata)) {
179 2
                            $this->cachedMetadata[$fqcn] = $this->validator->getMetadataFor($fqcn);
180
                        }
181
182
                        // Get metadata from the property and it's getters
183 2
                        $propertyMetadata = $this->cachedMetadata[$fqcn]->getPropertyMetadata($property);
184
185 2
                        foreach ($propertyMetadata as $memberMetadata) {
186
                            // Allow only constraints specified by the "link" matcher
187 2
                            if (self::TYPE_GETTER === $type) {
188 2
                                if (!$memberMetadata instanceof GetterMetadata) {
189 2
                                    continue;
190
                                }
191 2
                            } elseif (self::TYPE_PROPERTY === $type) {
192 2
                                if (!$memberMetadata instanceof PropertyMetadata) {
193 2
                                    continue;
194
                                }
195
                            }
196
197 2
                            $metadata->addPropertyConstraints($property, $memberMetadata->getConstraints());
198
                        }
199
200 2
                        break;
201 14
                    case 'constraints':
202 14
                        $metadata->addPropertyConstraints($property, $value);
203 14
                        break;
204 6
                    case 'cascade':
205 6
                        break;
206
                }
207
            }
208
        }
209
210 17
        $this->metadataFactory->addMetadata($metadata);
211
212 17
        return $rootObject;
213
    }
214
215
    /**
216
     * @param array                      $values
217
     * @param ObjectType|InputObjectType $type
218
     * @param ValidationNode             $parent
219
     *
220
     * @return array
221
     */
222 2
    private function createCollectionNode(array $values, Type $type, ValidationNode $parent): array
223
    {
224 2
        $collection = [];
225
226 2
        foreach ($values as $value) {
227 2
            $collection[] = $this->createObjectNode($value, $type, $parent);
228
        }
229
230 2
        return $collection;
231
    }
232
233
    /**
234
     * @param array                      $value
235
     * @param ObjectType|InputObjectType $type
236
     * @param $parent
237
     *
238
     * @return ValidationNode
239
     */
240 6
    private function createObjectNode(array $value, Type $type, ValidationNode $parent): ValidationNode
241
    {
242
        $mapping = [
243 6
            'class' => $type->config['validation'] ?? null,
244
        ];
245
246 6
        foreach ($type->getFields() as $fieldName => $inputField) {
0 ignored issues
show
Bug introduced by
The method getFields() does not exist on GraphQL\Type\Definition\Type. It seems like you code against a sub-type of GraphQL\Type\Definition\Type such as GraphQL\Type\Definition\InterfaceType or GraphQL\Type\Definition\ObjectType or GraphQL\Type\Definition\InputObjectType. ( Ignorable by Annotation )

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

246
        foreach ($type->/** @scrutinizer ignore-call */ getFields() as $fieldName => $inputField) {
Loading history...
247 6
            $mapping['properties'][$fieldName] = $inputField->config['validation'] ?? [];
248
        }
249
250 6
        return $this->buildValidationTree(new ValidationNode($type, null, $parent, $this->resolverArgs), $mapping, $value);
251
    }
252
253
    /**
254
     * @param ObjectMetadata $metadata
255
     * @param array          $constraints
256
     */
257 17
    private function applyClassConstraints(ObjectMetadata $metadata, ?array $constraints): void
258
    {
259 17
        foreach ($constraints ?? [] as $key => $value) {
260 8
            if (null === $value) {
261 8
                continue;
262
            }
263
264
            switch ($key) {
265 8
                case 'link':
266 2
                    $linkedMetadata = $this->validator->getMetadataFor($value);
267 2
                    $metadata->addConstraints($linkedMetadata->getConstraints());
268 2
                    break;
269 8
                case 'constraints':
270 8
                    foreach ($value as $constraint) {
271 8
                        if ($constraint instanceof Constraint) {
272 2
                            $metadata->addConstraint($constraint);
273 6
                        } elseif ($constraint instanceof GroupSequence) {
274 6
                            $metadata->setGroupSequence($constraint);
275
                        }
276
                    }
277 8
                    break;
278
            }
279
        }
280 17
    }
281
282
    /**
283
     * @throws ArgumentsValidationException
284
     */
285
    public function __invoke($groups = null, bool $throw = true): ?ConstraintViolationListInterface
286
    {
287
        return $this->validate($groups, $throw);
288
    }
289
}
290