Completed
Push — master ( 9459ba...5c21ca )
by Portey
05:54
created

ResolveValidator::isValidValueForField()   B

Complexity

Conditions 5
Paths 3

Size

Total Lines 19
Code Lines 11

Duplication

Lines 7
Ratio 36.84 %

Code Coverage

Tests 10
CRAP Score 5

Importance

Changes 3
Bugs 0 Features 0
Metric Value
c 3
b 0
f 0
dl 7
loc 19
ccs 10
cts 10
cp 1
rs 8.8571
cc 5
eloc 11
nc 3
nop 2
crap 5
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\Field as AstField;
20
use Youshido\GraphQL\Parser\Ast\Fragment;
21
use Youshido\GraphQL\Parser\Ast\FragmentReference;
22
use Youshido\GraphQL\Parser\Ast\Mutation;
23
use Youshido\GraphQL\Parser\Ast\Query;
24
use Youshido\GraphQL\Type\AbstractType;
25
use Youshido\GraphQL\Type\InputObject\AbstractInputObjectType;
26
use Youshido\GraphQL\Type\InterfaceType\AbstractInterfaceType;
27
use Youshido\GraphQL\Type\ListType\AbstractListType;
28
use Youshido\GraphQL\Type\Object\AbstractObjectType;
29
use Youshido\GraphQL\Type\TypeInterface;
30
use Youshido\GraphQL\Type\TypeMap;
31
use Youshido\GraphQL\Type\TypeService;
32
use Youshido\GraphQL\Type\Union\AbstractUnionType;
33
use Youshido\GraphQL\Validator\Exception\ResolveException;
34
35
class ResolveValidator implements ResolveValidatorInterface
36
{
37
38
    /** @var  ExecutionContextInterface */
39
    protected $executionContext;
40
41 37
    public function __construct(ExecutionContextInterface $executionContext)
42
    {
43 37
        $this->executionContext = $executionContext;
44 37
    }
45
46
    /**
47
     * @param AbstractObjectType      $objectType
48
     * @param Mutation|Query|AstField $field
49
     *
50
     * @return null
51
     */
52 29
    public function objectHasField($objectType, $field)
53
    {
54 29
        if (!(($objectType instanceof AbstractObjectType || $objectType instanceof AbstractInputObjectType)) || !$objectType->hasField($field->getName())) {
55 2
            $this->executionContext->addError(new ResolveException(sprintf('Field "%s" not found in type "%s"', $field->getName(), $objectType->getNamedType()->getName())));
56
57 2
            return false;
58
        }
59
60 29
        return true;
61
    }
62
63
    /**
64
     * @inheritdoc
65
     */
66 29
    public function validateArguments(FieldInterface $field, $query, Request $request)
67
    {
68
        $requiredArguments = array_filter($field->getArguments(), function (InputField $argument) {
69 22
            return $argument->getType()->getKind() == TypeMap::KIND_NON_NULL;
70 29
        });
71
72 29
        $withDefaultArguments = array_filter($field->getArguments(), function (InputField $argument) {
73 22
            return $argument->getConfig()->get('default') !== null;
74 29
        });
75
76 29
        foreach ($query->getArguments() as $argument) {
77 18 View Code Duplication
            if (!$field->hasArgument($argument->getName())) {
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...
78 2
                $this->executionContext->addError(new ResolveException(sprintf('Unknown argument "%s" on field "%s"', $argument->getName(), $field->getName())));
79
80 2
                return false;
81
            }
82
83 18
            $originalArgumentType = $field->getArgument($argument->getName())->getType();
84 18
            $argumentType         = $originalArgumentType->getNullableType()->getNamedType();
85
86 18
            if ($argument->getValue() instanceof Variable) {
87 4
                if (!$this->processVariable($argument, $argumentType, $field, $request)) {
88 2
                    return false;
89
                }
90 17
            } elseif ($argumentType->getKind() == TypeMap::KIND_INPUT_OBJECT) {
91 2
                if ($originalArgumentType->getNullableType()->getKind() == TypeMap::KIND_LIST) {
92 1
                    if (!$argument->getValue() instanceof InputList) {
93
                        return false;
94
                    }
95
96 1
                    foreach ($argument->getValue()->getValue() as $item) {
97 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...
98
                            return false;
99
                        }
100 1
                    }
101 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...
102
                    return false;
103
                }
104 2
            }
105
106 17
            $values = $argument->getValue()->getValue();
107 17
            if (!$originalArgumentType instanceof AbstractListType) {
108 16
                $values = [$values];
109 16
            }
110 17
            foreach ($values as $value) {
111 17
                if (!$argumentType->isValidValue($value)) {
112 4
                    $this->executionContext->addError(new ResolveException(sprintf('Not valid type for argument "%s" in query "%s"', $argument->getName(), $field->getName())));
113
114 4
                    return false;
115
                }
116 17
            }
117
118 16
            if (array_key_exists($argument->getName(), $requiredArguments)) {
119 4
                unset($requiredArguments[$argument->getName()]);
120 4
            }
121 16
            if (array_key_exists($argument->getName(), $withDefaultArguments)) {
122 6
                unset($withDefaultArguments[$argument->getName()]);
123 6
            }
124 27
        }
125
126 27
        if (count($requiredArguments)) {
127 2
            $this->executionContext->addError(new ResolveException(sprintf('Require "%s" arguments to query "%s"', implode(', ', array_keys($requiredArguments)), $query->getName())));
128
129 2
            return false;
130
        }
131
132 26
        if (count($withDefaultArguments)) {
133 4
            foreach ($withDefaultArguments as $name => $argument) {
134 4
                $query->addArgument(new Argument($name, new Literal($argument->getConfig()->get('default'))));
135 4
            }
136 4
        }
137
138 26
        return true;
139
    }
140
141 2
    private function processInputObject(InputObject $astObject, AbstractInputObjectType $inputObjectType, $request)
142
    {
143 2
        foreach ($astObject->getValue() as $name => $value) {
144 2
            $field = $inputObjectType->getField($name);
145
146 2
            if (!$field) {
147
                return false;
148
            }
149
150 2
            $argumentType = $field->getType()->getNullableType();
151
152 2
            if ($value instanceof Variable && !$this->processVariable(new Argument($name, $value), $argumentType, $field, $request)) {
153
                return false;
154
            }
155 2
        }
156
157 2
        return true;
158
    }
159
160 4
    private function processVariable(Argument $argument, TypeInterface $argumentType, FieldInterface $field, Request $request)
161
    {
162
        /** @var Variable $variable */
163 4
        $variable = $argument->getValue();
164
165
        //todo: here validate argument
166
167 4
        if ($variable->getTypeName() !== $argumentType->getName()) {
168 2
            $this->executionContext->addError(new ResolveException(sprintf('Invalid variable "%s" type, allowed type is "%s"', $variable->getName(), $argumentType->getName())));
169
170 2
            return false;
171
        }
172
173
        /** @var Variable $requestVariable */
174 3
        $requestVariable = $request->getVariable($variable->getName());
175 3 View Code Duplication
        if (null === $requestVariable) {
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...
176 1
            $this->executionContext->addError(new ResolveException(sprintf('Variable "%s" does not exist for query "%s"', $argument->getName(), $field->getName())));
177
178 1
            return false;
179
        }
180
181 3
        $variable->setValue($requestVariable);
182
183 3
        return true;
184
    }
185
186 7
    public function assertTypeImplementsInterface(AbstractType $type, AbstractInterfaceType $interface)
187
    {
188 7
        if (!$interface->isValidValue($type)) {
189 1
            throw new ResolveException('Type ' . $type->getName() . ' does not implement ' . $interface->getName());
190
        }
191 6
    }
192
193 3
    public function assertTypeInUnionTypes(AbstractType $type, AbstractUnionType $unionType)
194
    {
195 3
        $unionTypes = $unionType->getTypes();
196 3
        $valid      = false;
197 3
        if (empty($unionTypes)) return false;
198
199 3
        foreach ($unionTypes as $unionType) {
200 3
            if ($unionType->getName() == $type->getName()) {
201 2
                $valid = true;
202
203 2
                break;
204
            }
205 3
        }
206
207 3
        if (!$valid) {
208 2
            throw new ResolveException('Type ' . $type->getName() . ' not exist in types of ' . $unionType->getName());
209
        }
210 2
    }
211
212
    /**
213
     * @param Fragment          $fragment
214
     * @param FragmentReference $fragmentReference
215
     * @param AbstractType      $queryType
216
     *
217
     * @throws \Exception
218
     */
219 5
    public function assertValidFragmentForField($fragment, FragmentReference $fragmentReference, AbstractType $queryType)
220
    {
221 5
        $innerType = $queryType;
222 5
        while ($innerType->isCompositeType()) {
223 3
            $innerType = $innerType->getTypeOf();
224 3
        }
225
226 5
        if (!$fragment instanceof Fragment || $fragment->getModel() !== $innerType->getName()) {
227 1
            throw new ResolveException(sprintf('Fragment reference "%s" not found on model "%s"', $fragmentReference->getName(), $queryType->getName()));
228
        }
229 4
    }
230
231 9
    public function hasArrayAccess($data)
232
    {
233 9
        return is_array($data) || $data instanceof \Traversable;
234
    }
235
236 25
    public function isValidValueForField(FieldInterface $field, $value)
237
    {
238 25
        $fieldType = $field->getType();
239 25 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...
240 2
            $this->executionContext->addError(new ResolveException(sprintf('Cannot return null for non-nullable field %s', $field->getName())));
241
242 2
            return null;
243
        } else {
244 25
            $fieldType = $this->resolveTypeIfAbstract($fieldType->getNullableType(), $value);
245
        }
246
247 25
        if (!is_null($value) && !$fieldType->isValidValue($value)) {
248 2
            $this->executionContext->addError(new ResolveException(sprintf('Not valid value for %s field %s', $fieldType->getNullableType()->getKind(), $field->getName())));
249
250 2
            return null;
251
        }
252
253 25
        return true;
254
    }
255
256 25
    public function resolveTypeIfAbstract(AbstractType $type, $resolvedValue)
257
    {
258 25
        return TypeService::isAbstractType($type) ? $this->resolveAbstractType($type, $resolvedValue) : $type;
259
    }
260
261
    /**
262
     * @param AbstractType $type
263
     * @param              $resolvedValue
264
     *
265
     * @return AbstractObjectType
266
     * @throws ResolveException
267
     */
268 6
    public function resolveAbstractType(AbstractType $type, $resolvedValue)
269
    {
270
        /** @var AbstractInterfaceType $type */
271 6
        $resolvedType = $type->resolveType($resolvedValue);
272
273 6
        if (!$resolvedType) {
274
            $this->executionContext->addError(new \Exception('Cannot resolve type'));
275
276
            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...
277
        }
278 6
        if ($type instanceof AbstractInterfaceType) {
279 5
            $this->assertTypeImplementsInterface($resolvedType, $type);
280 5
        } else {
281
            /** @var AbstractUnionType $type */
282 1
            $this->assertTypeInUnionTypes($resolvedType, $type);
283
        }
284
285 6
        return $resolvedType;
286
    }
287
288
    /**
289
     * @return ExecutionContextInterface
290
     */
291 2
    public function getExecutionContext()
292
    {
293 2
        return $this->executionContext;
294
    }
295
296
    /**
297
     * @param ExecutionContextInterface $executionContext
298
     */
299
    public function setExecutionContext($executionContext)
300
    {
301
        $this->executionContext = $executionContext;
302
    }
303
304
}
305