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