Completed
Push — master ( 5d7827...c80c5c )
by Adrien
01:49
created

OutputFieldsConfigurationFactory::completeField()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 14
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 3

Importance

Changes 0
Metric Value
dl 0
loc 14
ccs 9
cts 9
cp 1
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 8
nc 4
nop 2
crap 3
1
<?php
2
3
declare(strict_types=1);
4
5
namespace GraphQL\Doctrine\Factory;
6
7
use GraphQL\Doctrine\Annotation\Argument;
8
use GraphQL\Doctrine\Annotation\Field;
9
use GraphQL\Doctrine\DocBlockReader;
10
use GraphQL\Doctrine\Exception;
11
use GraphQL\Type\Definition\Type;
12
use ReflectionMethod;
13
use ReflectionParameter;
14
15
/**
16
 * A factory to create a configuration for all getters of an entity
17
 */
18
class OutputFieldsConfigurationFactory extends AbstractFieldsConfigurationFactory
19
{
20 12
    protected function getMethodPattern(): string
21
    {
22 12
        return '~^(get|is|has)[A-Z]~';
23
    }
24
25
    /**
26
     * Get the entire configuration for a method
27
     *
28
     * @param ReflectionMethod $method
29
     *
30
     * @return null|array
31
     */
32 11
    protected function methodToConfiguration(ReflectionMethod $method): ?array
33
    {
34
        // Get a field from annotation, or an empty one
35 11
        $field = $this->getAnnotationReader()->getMethodAnnotation($method, Field::class) ?? new Field();
36
37 11
        if (!$field->type instanceof Type) {
0 ignored issues
show
introduced by
$field->type is never a sub-type of GraphQL\Type\Definition\Type.
Loading history...
38 11
            $this->convertTypeDeclarationsToInstances($method, $field);
39 11
            $this->completeField($field, $method);
40
        }
41
42 5
        return $field->toArray();
43
    }
44
45
    /**
46
     * All its types will be converted from string to real instance of Type
47
     *
48
     * @param ReflectionMethod $method
49
     * @param Field $field
50
     */
51 11
    private function convertTypeDeclarationsToInstances(ReflectionMethod $method, Field $field): void
52
    {
53 11
        $field->type = $this->getTypeFromPhpDeclaration($method, $field->type);
54 11
        $args = [];
55 11
        foreach ($field->args as $arg) {
56 4
            $arg->type = $this->getTypeFromPhpDeclaration($method, $arg->type);
57 4
            $args[$arg->name] = $arg;
58
        }
59 11
        $field->args = $args;
60 11
    }
61
62
    /**
63
     * Complete field with info from doc blocks and type hints
64
     *
65
     * @param Field $field
66
     * @param ReflectionMethod $method
67
     *
68
     * @throws Exception
69
     */
70 11
    private function completeField(Field $field, ReflectionMethod $method): void
71
    {
72 11
        $fieldName = lcfirst(preg_replace('~^get~', '', $method->getName()));
73 11
        if (!$field->name) {
74 11
            $field->name = $fieldName;
75
        }
76
77 11
        $docBlock = new DocBlockReader($method);
78 11
        if (!$field->description) {
79 11
            $field->description = $docBlock->getMethodDescription();
80
        }
81
82 11
        $this->completeFieldArguments($field, $method, $docBlock);
83 7
        $this->completeFieldType($field, $method, $fieldName, $docBlock);
84 5
    }
85
86
    /**
87
     * Complete arguments configuration from existing type hints
88
     *
89
     * @param Field $field
90
     * @param ReflectionMethod $method
91
     * @param DocBlockReader $docBlock
92
     */
93 11
    private function completeFieldArguments(Field $field, ReflectionMethod $method, DocBlockReader $docBlock): void
94
    {
95 11
        $argsFromAnnotations = $field->args;
96 11
        $args = [];
97 11
        foreach ($method->getParameters() as $param) {
98
            // Either get existing, or create new argument
99 8
            $arg = $argsFromAnnotations[$param->getName()] ?? new Argument();
100 8
            $args[$param->getName()] = $arg;
101
102 8
            $this->completeArgumentFromTypeHint($method, $param, $arg, $docBlock);
103
        }
104
105 8
        $extraAnnotations = array_diff(array_keys($argsFromAnnotations), array_keys($args));
106 8
        if ($extraAnnotations) {
107 1
            throw new Exception('The following arguments were declared via `@API\Argument` annotation but do not match actual parameter names on method ' . $this->getMethodFullName($method) . '. Either rename or remove the annotations: ' . implode(', ', $extraAnnotations));
108
        }
109
110 7
        $field->args = $args;
111 7
    }
112
113
    /**
114
     * Complete a single argument from its type hint
115
     *
116
     * @param ReflectionMethod $method
117
     * @param ReflectionParameter $param
118
     * @param Argument $arg
119
     * @param DocBlockReader $docBlock
120
     */
121 8
    private function completeArgumentFromTypeHint(ReflectionMethod $method, ReflectionParameter $param, Argument $arg, DocBlockReader $docBlock): void
122
    {
123 8
        if (!$arg->name) {
124 7
            $arg->name = $param->getName();
125
        }
126
127 8
        if (!$arg->description) {
128 8
            $arg->description = $docBlock->getParameterDescription($param);
129
        }
130
131 8
        if (!isset($arg->defaultValue) && $param->isDefaultValueAvailable()) {
132 4
            $arg->defaultValue = $param->getDefaultValue();
133
        }
134
135 8
        if (!$arg->type) {
136 7
            $typeDeclaration = $docBlock->getParameterType($param);
137 7
            $this->throwIfArray($param, $typeDeclaration);
138 6
            $arg->type = $this->getTypeFromPhpDeclaration($method, $typeDeclaration, true);
139
        }
140
141 7
        $type = $param->getType();
142 7
        if (!$arg->type && $type) {
143 3
            $this->throwIfArray($param, (string) $type);
144 3
            $arg->type = $this->reflectionTypeToType($type, true);
145
        }
146
147 7
        $arg->type = $this->nonNullIfHasDefault($arg->type, $arg->defaultValue);
148
149 7
        $this->throwIfNotInputType($param, $arg->type, 'Argument');
150 5
    }
151
152
    /**
153
     * Get a GraphQL type instance from dock block return type
154
     *
155
     * @param ReflectionMethod $method
156
     * @param \GraphQL\Doctrine\DocBlockReader $docBlock
157
     *
158
     * @return null|Type
159
     */
160 6
    private function getTypeFromDocBock(ReflectionMethod $method, DocBlockReader $docBlock): ?Type
161
    {
162 6
        $typeDeclaration = $docBlock->getReturnType();
163
        $blacklist = [
164 6
            'Collection',
165
            'array',
166
        ];
167
168 6
        if ($typeDeclaration && !in_array($typeDeclaration, $blacklist, true)) {
169 2
            return $this->getTypeFromPhpDeclaration($method, $typeDeclaration);
170
        }
171
172 6
        return null;
173
    }
174
175
    /**
176
     * Complete field type from doc blocks and type hints
177
     *
178
     * @param Field $field
179
     * @param ReflectionMethod $method
180
     * @param string $fieldName
181
     * @param DocBlockReader $docBlock
182
     */
183 7
    private function completeFieldType(Field $field, ReflectionMethod $method, string $fieldName, DocBlockReader $docBlock): void
184
    {
185 7
        if ($this->isIdentityField($fieldName)) {
186 5
            $field->type = Type::nonNull(Type::id());
187
        }
188
189
        // If still no type, look for docblock
190 7
        if (!$field->type) {
191 6
            $field->type = $this->getTypeFromDocBock($method, $docBlock);
192
        }
193
194
        // If still no type, look for type hint
195 7
        if (!$field->type) {
196 6
            $field->type = $this->getTypeFromReturnTypeHint($method, $fieldName);
197
        }
198
199
        // If still no type, cannot continue
200 6
        if (!$field->type) {
201 1
            throw new Exception('Could not find type for method ' . $this->getMethodFullName($method) . '. Either type hint the return value, or specify the type with `@API\Field` annotation.');
202
        }
203 5
    }
204
}
205