Completed
Push — master ( 3aac06...366db0 )
by Adrien
01:53
created

methodToConfiguration()   C

Complexity

Conditions 7
Paths 64

Size

Total Lines 39
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 7

Importance

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