Completed
Pull Request — master (#58)
by Sebastian
03:04
created

ResolveValidator::isValidValueForField()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 21
Code Lines 12

Duplication

Lines 5
Ratio 23.81 %

Code Coverage

Tests 12
CRAP Score 4

Importance

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