Completed
Pull Request — master (#2)
by Adrien
02:16
created

getFieldFromAnnotation()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 14
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
dl 0
loc 14
ccs 0
cts 9
cp 0
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 9
nc 2
nop 1
crap 6
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
    protected function getMethodPattern(): string
21
    {
22
        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
    protected function methodToConfiguration(ReflectionMethod $method): ?array
32
    {
33
        // First get user specified values
34
        $field = $this->getFieldFromAnnotation($method);
35
36
        $fieldName = lcfirst(preg_replace('~^get~', '', $method->getName()));
37
        if (!$field->name) {
38
            $field->name = $fieldName;
39
        }
40
41
        $docBlock = new DocBlockReader($method);
42
        if (!$field->description) {
43
            $field->description = $docBlock->getMethodDescription();
44
        }
45
46
        if ($this->isIdentityField($fieldName)) {
47
            $field->type = Type::nonNull(Type::id());
48
        }
49
50
        // If still no type, look for docblock
51
        if (!$field->type) {
52
            $field->type = $this->getTypeFromDocBock($method, $docBlock);
53
        }
54
55
        // If still no type, look for type hint
56
        if (!$field->type) {
57
            $field->type = $this->getTypeFromReturnTypeHint($method, $fieldName);
58
        }
59
60
        // If still no args, look for type hint
61
        $field->args = $this->getArgumentsFromTypeHint($method, $field->args, $docBlock);
62
63
        // If still no type, cannot continue
64
        if (!$field->type) {
65
            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
        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
    private function getFieldFromAnnotation(ReflectionMethod $method): Field
79
    {
80
        $field = $this->getAnnotationReader()->getMethodAnnotation($method, Field::class) ?? new Field();
81
82
        $field->type = $this->getTypeFromPhpDeclaration($method, $field->type);
83
        $args = [];
84
        foreach ($field->args as $arg) {
85
            $arg->type = $this->getTypeFromPhpDeclaration($method, $arg->type);
86
            $args[$arg->name] = $arg;
87
        }
88
        $field->args = $args;
89
90
        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
    private function getArgumentsFromTypeHint(ReflectionMethod $method, array $argsFromAnnotations, DocBlockReader $docBlock): array
101
    {
102
        $args = [];
103
        foreach ($method->getParameters() as $param) {
104
            //Either get existing, or create new argument
105
            $arg = $argsFromAnnotations[$param->getName()] ?? new Argument();
106
            $args[$param->getName()] = $arg;
107
108
            $this->completeArgumentFromTypeHint($method, $param, $arg, $docBlock);
109
        }
110
111
        $extraAnnotations = array_diff(array_keys($argsFromAnnotations), array_keys($args));
112
        if ($extraAnnotations) {
113
            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
        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
    private function completeArgumentFromTypeHint(ReflectionMethod $method, ReflectionParameter $param, Argument $arg, DocBlockReader $docBlock)
126
    {
127
        if (!$arg->name) {
128
            $arg->name = $param->getName();
129
        }
130
131
        if (!$arg->description) {
132
            $arg->description = $docBlock->getParameterDescription($param);
133
        }
134
135
        if (!isset($arg->defaultValue) && $param->isDefaultValueAvailable()) {
136
            $arg->defaultValue = $param->getDefaultValue();
137
        }
138
139
        if (!$arg->type) {
140
            $typeDeclaration = $docBlock->getParameterType($param);
141
            $this->throwIfArray($param, $typeDeclaration);
142
            $arg->type = $this->getTypeFromPhpDeclaration($method, $typeDeclaration, true);
143
        }
144
145
        $type = $param->getType();
146
        if (!$arg->type && $type) {
147
            $this->throwIfArray($param, (string) $type);
148
            $arg->type = $this->refelectionTypeToType($type, true);
149
        }
150
151
        $arg->type = $this->nonNullIfHasDefault($param, $arg->type);
152
153
        $this->throwIfNotInputType($param, $arg->type, 'Argument');
154
    }
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
    private function getTypeFromDocBock(ReflectionMethod $method, DocBlockReader $docBlock): ?Type
163
    {
164
        $typeDeclaration = $docBlock->getReturnType();
165
        $blacklist = [
166
            'Collection',
167
            'array',
168
        ];
169
170
        if ($typeDeclaration && !in_array($typeDeclaration, $blacklist, true)) {
171
            return $this->getTypeFromPhpDeclaration($method, $typeDeclaration);
172
        }
173
174
        return null;
175
    }
176
}
177