Completed
Push — master ( fbefd9...bc1382 )
by Adrien
04:14
created

OutputFieldsConfigurationFactory::completeField()   C

Complexity

Conditions 7
Paths 64

Size

Total Lines 34
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 7

Importance

Changes 0
Metric Value
dl 0
loc 34
ccs 17
cts 17
cp 1
rs 6.7272
c 0
b 0
f 0
cc 7
eloc 16
nc 64
nop 2
crap 7
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 10
    protected function getMethodPattern(): string
21
    {
22 10
        return '~^(get|is|has)[A-Z]~';
23
    }
24
25
    /**
26
     * Get the entire configuration for a method
27
     * @param ReflectionMethod $method
28
     * @return array
29
     */
30 10
    protected function methodToConfiguration(ReflectionMethod $method): ?array
31
    {
32
        // Get a field from annotation, or an empty one
33 10
        $field = $this->getAnnotationReader()->getMethodAnnotation($method, Field::class) ?? new Field();
34
35 10
        if (!$field->type instanceof Type) {
36 10
            $this->convertTypeDeclarationsToInstances($method, $field);
37 10
            $this->completeField($method, $field);
38
        }
39
40 4
        return $field->toArray();
41
    }
42
43
    /**
44
     * All its types will be converted from string to real instance of Type
45
     *
46
     * @param ReflectionMethod $method
47
     * @param Field $field
48
     */
49 10
    private function convertTypeDeclarationsToInstances(ReflectionMethod $method, Field $field): void
50
    {
51 10
        $field->type = $this->getTypeFromPhpDeclaration($method, $field->type);
52 10
        $args = [];
53 10
        foreach ($field->args as $arg) {
54 4
            $arg->type = $this->getTypeFromPhpDeclaration($method, $arg->type);
55 4
            $args[$arg->name] = $arg;
56
        }
57 10
        $field->args = $args;
58 10
    }
59
60
    /**
61
     * Complete field with info from doc blocks and type hints
62
     * @param ReflectionMethod $method
63
     * @param Field $field
64
     * @throws Exception
65
     */
66 10
    private function completeField(ReflectionMethod $method, Field $field): void
67
    {
68 10
        $fieldName = lcfirst(preg_replace('~^get~', '', $method->getName()));
69 10
        if (!$field->name) {
70 10
            $field->name = $fieldName;
71
        }
72
73 10
        $docBlock = new DocBlockReader($method);
74 10
        if (!$field->description) {
75 10
            $field->description = $docBlock->getMethodDescription();
76
        }
77
78 10
        if ($this->isIdentityField($fieldName)) {
79 4
            $field->type = Type::nonNull(Type::id());
80
        }
81
82
        // If still no type, look for docblock
83 10
        if (!$field->type) {
84 9
            $field->type = $this->getTypeFromDocBock($method, $docBlock);
85
        }
86
87
        // If still no type, look for type hint
88 10
        if (!$field->type) {
89 6
            $field->type = $this->getTypeFromReturnTypeHint($method, $fieldName);
90
        }
91
92
        // If still no args, look for type hint
93 9
        $field->args = $this->getArgumentsFromTypeHint($method, $field->args, $docBlock);
94
95
        // If still no type, cannot continue
96 5
        if (!$field->type) {
97 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.');
98
        }
99 4
    }
100
101
    /**
102
     * Complete arguments configuration from existing type hints
103
     * @param ReflectionMethod $method
104
     * @param Argument[] $argsFromAnnotations
105
     * @throws Exception
106
     * @return array
107
     */
108 9
    private function getArgumentsFromTypeHint(ReflectionMethod $method, array $argsFromAnnotations, DocBlockReader $docBlock): array
109
    {
110 9
        $args = [];
111 9
        foreach ($method->getParameters() as $param) {
112
            //Either get existing, or create new argument
113 7
            $arg = $argsFromAnnotations[$param->getName()] ?? new Argument();
114 7
            $args[$param->getName()] = $arg;
115
116 7
            $this->completeArgumentFromTypeHint($method, $param, $arg, $docBlock);
117
        }
118
119 6
        $extraAnnotations = array_diff(array_keys($argsFromAnnotations), array_keys($args));
120 6
        if ($extraAnnotations) {
121 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));
122
        }
123
124 5
        return $args;
125
    }
126
127
    /**
128
     * Complete a single argument from its type hint
129
     * @param ReflectionMethod $method
130
     * @param ReflectionParameter $param
131
     * @param Argument $arg
132
     */
133 7
    private function completeArgumentFromTypeHint(ReflectionMethod $method, ReflectionParameter $param, Argument $arg, DocBlockReader $docBlock): void
134
    {
135 7
        if (!$arg->name) {
136 6
            $arg->name = $param->getName();
137
        }
138
139 7
        if (!$arg->description) {
140 7
            $arg->description = $docBlock->getParameterDescription($param);
141
        }
142
143 7
        if (!isset($arg->defaultValue) && $param->isDefaultValueAvailable()) {
144 3
            $arg->defaultValue = $param->getDefaultValue();
145
        }
146
147 7
        if (!$arg->type) {
148 6
            $typeDeclaration = $docBlock->getParameterType($param);
149 6
            $this->throwIfArray($param, $typeDeclaration);
150 5
            $arg->type = $this->getTypeFromPhpDeclaration($method, $typeDeclaration, true);
151
        }
152
153 6
        $type = $param->getType();
154 6
        if (!$arg->type && $type) {
155 2
            $this->throwIfArray($param, (string) $type);
156 2
            $arg->type = $this->refelectionTypeToType($type, true);
157
        }
158
159 6
        $arg->type = $this->nonNullIfHasDefault($param, $arg->type);
0 ignored issues
show
Bug introduced by
It seems like $arg->type can also be of type null or string; however, GraphQL\Doctrine\Factory...::nonNullIfHasDefault() does only seem to accept object<GraphQL\Type\Definition\Type>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
160
161 6
        $this->throwIfNotInputType($param, $arg->type, 'Argument');
162 4
    }
163
164
    /**
165
     * Get a GraphQL type instance from dock block return type
166
     * @param ReflectionMethod $method
167
     * @param \GraphQL\Doctrine\DocBlockReader $docBlock
168
     * @return null|Type
169
     */
170 9
    private function getTypeFromDocBock(ReflectionMethod $method, DocBlockReader $docBlock): ?Type
171
    {
172 9
        $typeDeclaration = $docBlock->getReturnType();
173
        $blacklist = [
174 9
            'Collection',
175
            'array',
176
        ];
177
178 9
        if ($typeDeclaration && !in_array($typeDeclaration, $blacklist, true)) {
179 5
            return $this->getTypeFromPhpDeclaration($method, $typeDeclaration);
180
        }
181
182 6
        return null;
183
    }
184
}
185