Completed
Push — master ( e13501...8c19ea )
by Alexandr
05:24
created

ResolveValidator   F

Complexity

Total Complexity 59

Size/Duplication

Total Lines 273
Duplicated Lines 2.56 %

Coupling/Cohesion

Components 1
Dependencies 18

Test Coverage

Coverage 92.54%

Importance

Changes 15
Bugs 3 Features 3
Metric Value
wmc 59
lcom 1
cbo 18
dl 7
loc 273
ccs 124
cts 134
cp 0.9254
rs 2.9203
c 15
b 3
f 3

14 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 1
A objectHasField() 0 10 4
D validateArguments() 0 74 19
B processInputObject() 0 18 5
B processVariable() 0 28 5
A assertTypeImplementsInterface() 0 6 2
B assertTypeInUnionTypes() 0 18 5
A assertValidFragmentForField() 0 11 4
A hasArrayAccess() 0 4 2
B isValidValueForField() 7 19 5
A resolveTypeIfAbstract() 0 4 2
A resolveAbstractType() 0 19 3
A getExecutionContext() 0 4 1
A setExecutionContext() 0 4 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like ResolveValidator often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ResolveValidator, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * Date: 01.12.15
4
 *
5
 * @author Portey Vasil <[email protected]>
6
 */
7
8
namespace Youshido\GraphQL\Validator\ResolveValidator;
9
10
use Youshido\GraphQL\Execution\Context\ExecutionContextInterface;
11
use Youshido\GraphQL\Execution\Request;
12
use Youshido\GraphQL\Field\FieldInterface;
13
use Youshido\GraphQL\Field\InputField;
14
use Youshido\GraphQL\Parser\Ast\Argument;
15
use Youshido\GraphQL\Parser\Ast\ArgumentValue\InputList;
16
use Youshido\GraphQL\Parser\Ast\ArgumentValue\InputObject;
17
use Youshido\GraphQL\Parser\Ast\ArgumentValue\Literal;
18
use Youshido\GraphQL\Parser\Ast\ArgumentValue\Variable;
19
use Youshido\GraphQL\Parser\Ast\ArgumentValue\VariableReference;
20
use Youshido\GraphQL\Parser\Ast\Field as AstField;
21
use Youshido\GraphQL\Parser\Ast\Fragment;
22
use Youshido\GraphQL\Parser\Ast\FragmentReference;
23
use Youshido\GraphQL\Parser\Ast\Mutation;
24
use Youshido\GraphQL\Parser\Ast\Query;
25
use Youshido\GraphQL\Type\AbstractType;
26
use Youshido\GraphQL\Type\InputObject\AbstractInputObjectType;
27
use Youshido\GraphQL\Type\InterfaceType\AbstractInterfaceType;
28
use Youshido\GraphQL\Type\ListType\AbstractListType;
29
use Youshido\GraphQL\Type\Object\AbstractObjectType;
30
use Youshido\GraphQL\Type\TypeInterface;
31
use Youshido\GraphQL\Type\TypeMap;
32
use Youshido\GraphQL\Type\TypeService;
33
use Youshido\GraphQL\Type\Union\AbstractUnionType;
34
use Youshido\GraphQL\Validator\Exception\ResolveException;
35
36
class ResolveValidator implements ResolveValidatorInterface
37
{
38
39
    /** @var  ExecutionContextInterface */
40
    protected $executionContext;
41
42 38
    public function __construct(ExecutionContextInterface $executionContext)
43
    {
44 38
        $this->executionContext = $executionContext;
45 38
    }
46
47
    /**
48
     * @param AbstractObjectType      $objectType
49
     * @param Mutation|Query|AstField $field
50
     *
51
     * @return null
52
     */
53 30
    public function objectHasField($objectType, $field)
54
    {
55 30
        if (!(($objectType instanceof AbstractObjectType || $objectType instanceof AbstractInputObjectType)) || !$objectType->hasField($field->getName())) {
56 2
            $this->executionContext->addError(new ResolveException(sprintf('Field "%s" not found in type "%s"', $field->getName(), $objectType->getNamedType()->getName())));
57
58 2
            return false;
59
        }
60
61 30
        return true;
62
    }
63
64
    /**
65
     * @inheritdoc
66
     */
67 30
    public function validateArguments(FieldInterface $field, $query, Request $request)
68
    {
69
        $requiredArguments = array_filter($field->getArguments(), function (InputField $argument) {
70 22
            return $argument->getType()->getKind() == TypeMap::KIND_NON_NULL;
71 30
        });
72
73 30
        $withDefaultArguments = array_filter($field->getArguments(), function (InputField $argument) {
74 22
            return $argument->getConfig()->get('default') !== null;
75 30
        });
76
77 30
        foreach ($query->getArguments() as $argument) {
78 18
            if (!$field->hasArgument($argument->getName())) {
79 2
                $this->executionContext->addError(new ResolveException(sprintf('Unknown argument "%s" on field "%s"', $argument->getName(), $field->getName())));
80
81 2
                return false;
82
            }
83
84 18
            $originalArgumentType = $field->getArgument($argument->getName())->getType();
85 18
            $argumentType         = $originalArgumentType->getNullableType()->getNamedType();
86
87 18
            if ($argument->getValue() instanceof VariableReference) {
88 4
                if (!$this->processVariable($argument, $argumentType, $field, $request)) {
89 2
                    return false;
90
                }
91 17
            } elseif ($argumentType->getKind() == TypeMap::KIND_INPUT_OBJECT) {
92 2
                if ($originalArgumentType->getNullableType()->getKind() == TypeMap::KIND_LIST) {
93 1
                    if (!$argument->getValue() instanceof InputList) {
94
                        return false;
95
                    }
96
97 1
                    foreach ($argument->getValue()->getValue() as $item) {
98 1
                        if (!$this->processInputObject($item, $argumentType, $request)) {
0 ignored issues
show
Compatibility introduced by
$argumentType of type object<Youshido\GraphQL\Type\AbstractType> is not a sub-type of object<Youshido\GraphQL\...bstractInputObjectType>. It seems like you assume a child class of the class Youshido\GraphQL\Type\AbstractType to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
99
                            return false;
100
                        }
101 1
                    }
102 2
                } else if (!$this->processInputObject($argument->getValue(), $argumentType, $request)) {
0 ignored issues
show
Compatibility introduced by
$argumentType of type object<Youshido\GraphQL\Type\AbstractType> is not a sub-type of object<Youshido\GraphQL\...bstractInputObjectType>. It seems like you assume a child class of the class Youshido\GraphQL\Type\AbstractType to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
103
                    return false;
104
                }
105 2
            }
106
107 17
            $values = $argument->getValue()->getValue();
108 17
            if (!$originalArgumentType instanceof AbstractListType) {
109 16
                $values = [$values];
110 16
            }
111 17
            foreach ($values as $value) {
112 17
                if (!$argumentType->isValidValue($value)) {
113 4
                    $this->executionContext->addError(new ResolveException(sprintf('Not valid type for argument "%s" in query "%s"', $argument->getName(), $field->getName())));
114
115 4
                    return false;
116
                }
117 17
            }
118
119 16
            if (array_key_exists($argument->getName(), $requiredArguments)) {
120 4
                unset($requiredArguments[$argument->getName()]);
121 4
            }
122 16
            if (array_key_exists($argument->getName(), $withDefaultArguments)) {
123 6
                unset($withDefaultArguments[$argument->getName()]);
124 6
            }
125 28
        }
126
127 28
        if (count($requiredArguments)) {
128 2
            $this->executionContext->addError(new ResolveException(sprintf('Require "%s" arguments to query "%s"', implode(', ', array_keys($requiredArguments)), $query->getName())));
129
130 2
            return false;
131
        }
132
133 27
        if (count($withDefaultArguments)) {
134 4
            foreach ($withDefaultArguments as $name => $argument) {
135 4
                $query->addArgument(new Argument($name, new Literal($argument->getConfig()->get('default'))));
136 4
            }
137 4
        }
138
139 27
        return true;
140
    }
141
142 2
    private function processInputObject(InputObject $astObject, AbstractInputObjectType $inputObjectType, $request)
143
    {
144 2
        foreach ($astObject->getValue() as $name => $value) {
145 2
            $field = $inputObjectType->getField($name);
146
147 2
            if (!$field) {
148
                return false;
149
            }
150
151 2
            $argumentType = $field->getType()->getNullableType();
152
153 2
            if ($value instanceof VariableReference && !$this->processVariable(new Argument($name, $value), $argumentType, $field, $request)) {
154
                return false;
155
            }
156 2
        }
157
158 2
        return true;
159
    }
160
161 4
    private function processVariable(Argument $argument, TypeInterface $argumentType, FieldInterface $field, Request $request)
162
    {
163
        /** @var VariableReference $variableReference */
164 4
        $variableReference = $argument->getValue();
165
166
        /** @var Variable $variable */
167 4
        $variable = $variableReference->getVariable();
168
169
        //todo: here validate argument
170
171 4
        if ($variable->getTypeName() !== $argumentType->getName()) {
172 2
            $this->executionContext->addError(new ResolveException(sprintf('Invalid variable "%s" type, allowed type is "%s"', $variable->getName(), $argumentType->getName())));
173
174 2
            return false;
175
        }
176
177
        /** @var Variable $requestVariable */
178 3
        $requestVariable = $request->getVariable($variable->getName());
179 3
        if (!$request->hasVariable($variable->getName()) || (null === $requestVariable && $variable->isNullable())) {
180 1
            $this->executionContext->addError(new ResolveException(sprintf('Variable "%s" does not exist for query "%s"', $argument->getName(), $field->getName())));
181
182 1
            return false;
183
        }
184
185 3
        $variableReference->setValue($requestVariable);
186
187 3
        return true;
188
    }
189
190 7
    public function assertTypeImplementsInterface(AbstractType $type, AbstractInterfaceType $interface)
191
    {
192 7
        if (!$interface->isValidValue($type)) {
193 1
            throw new ResolveException('Type ' . $type->getName() . ' does not implement ' . $interface->getName());
194
        }
195 6
    }
196
197 3
    public function assertTypeInUnionTypes(AbstractType $type, AbstractUnionType $unionType)
198
    {
199 3
        $unionTypes = $unionType->getTypes();
200 3
        $valid      = false;
201 3
        if (empty($unionTypes)) return false;
202
203 3
        foreach ($unionTypes as $unionType) {
204 3
            if ($unionType->getName() == $type->getName()) {
205 2
                $valid = true;
206
207 2
                break;
208
            }
209 3
        }
210
211 3
        if (!$valid) {
212 2
            throw new ResolveException('Type ' . $type->getName() . ' not exist in types of ' . $unionType->getName());
213
        }
214 2
    }
215
216
    /**
217
     * @param Fragment          $fragment
218
     * @param FragmentReference $fragmentReference
219
     * @param AbstractType      $queryType
220
     *
221
     * @throws \Exception
222
     */
223 5
    public function assertValidFragmentForField($fragment, FragmentReference $fragmentReference, AbstractType $queryType)
224
    {
225 5
        $innerType = $queryType;
226 5
        while ($innerType->isCompositeType()) {
227 3
            $innerType = $innerType->getTypeOf();
228 3
        }
229
230 5
        if (!$fragment instanceof Fragment || $fragment->getModel() !== $innerType->getName()) {
231 1
            throw new ResolveException(sprintf('Fragment reference "%s" not found on model "%s"', $fragmentReference->getName(), $queryType->getName()));
232
        }
233 4
    }
234
235 10
    public function hasArrayAccess($data)
236
    {
237 10
        return is_array($data) || $data instanceof \Traversable;
238
    }
239
240 26
    public function isValidValueForField(FieldInterface $field, $value)
241
    {
242 26
        $fieldType = $field->getType();
243 26 View Code Duplication
        if ($fieldType->getKind() == TypeMap::KIND_NON_NULL && is_null($value)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
244 2
            $this->executionContext->addError(new ResolveException(sprintf('Cannot return null for non-nullable field %s', $field->getName())));
245
246 2
            return null;
247
        } else {
248 26
            $fieldType = $this->resolveTypeIfAbstract($fieldType->getNullableType(), $value);
249
        }
250
251 26
        if (!is_null($value) && !$fieldType->isValidValue($value)) {
252 2
            $this->executionContext->addError(new ResolveException(sprintf('Not valid value for %s field %s', $fieldType->getNullableType()->getKind(), $field->getName())));
253
254 2
            return null;
255
        }
256
257 26
        return true;
258
    }
259
260 26
    public function resolveTypeIfAbstract(AbstractType $type, $resolvedValue)
261
    {
262 26
        return TypeService::isAbstractType($type) ? $this->resolveAbstractType($type, $resolvedValue) : $type;
263
    }
264
265
    /**
266
     * @param AbstractType $type
267
     * @param              $resolvedValue
268
     *
269
     * @return AbstractObjectType
270
     * @throws ResolveException
271
     */
272 6
    public function resolveAbstractType(AbstractType $type, $resolvedValue)
273
    {
274
        /** @var AbstractInterfaceType $type */
275 6
        $resolvedType = $type->resolveType($resolvedValue);
276
277 6
        if (!$resolvedType) {
278
            $this->executionContext->addError(new \Exception('Cannot resolve type'));
279
280
            return $type;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $type; (Youshido\GraphQL\Type\In...e\AbstractInterfaceType) is incompatible with the return type documented by Youshido\GraphQL\Validat...or::resolveAbstractType of type Youshido\GraphQL\Type\Object\AbstractObjectType.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
281
        }
282 6
        if ($type instanceof AbstractInterfaceType) {
283 5
            $this->assertTypeImplementsInterface($resolvedType, $type);
284 5
        } else {
285
            /** @var AbstractUnionType $type */
286 1
            $this->assertTypeInUnionTypes($resolvedType, $type);
287
        }
288
289 6
        return $resolvedType;
290
    }
291
292
    /**
293
     * @return ExecutionContextInterface
294
     */
295 2
    public function getExecutionContext()
296
    {
297 2
        return $this->executionContext;
298
    }
299
300
    /**
301
     * @param ExecutionContextInterface $executionContext
302
     */
303
    public function setExecutionContext($executionContext)
304
    {
305
        $this->executionContext = $executionContext;
306
    }
307
308
}
309