isIdentityField()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 3
rs 10
c 0
b 0
f 0
ccs 2
cts 2
cp 1
cc 1
eloc 1
nc 1
nop 1
crap 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace GraphQL\Doctrine\Factory;
6
7
use Doctrine\Common\Collections\Collection;
8
use Doctrine\ORM\Mapping\ClassMetadata;
9
use GraphQL\Doctrine\Attribute\AbstractAttribute;
10
use GraphQL\Doctrine\Attribute\Exclude;
11
use GraphQL\Doctrine\Exception;
12
use GraphQL\Type\Definition\InputType;
13
use GraphQL\Type\Definition\NonNull;
14
use GraphQL\Type\Definition\Type;
15
use GraphQL\Type\Definition\WrappingType;
16
use ReflectionClass;
17
use ReflectionMethod;
18
use ReflectionNamedType;
19
use ReflectionParameter;
20
21
/**
22
 * A factory to create a configuration for all fields of an entity.
23
 */
24
abstract class AbstractFieldsConfigurationFactory extends AbstractFactory
25
{
26
    /**
27
     * Doctrine metadata for the entity.
28
     */
29
    private ClassMetadata $metadata;
30
31
    /**
32
     * The identity field name, eg: "id".
33
     */
34
    private string $identityField;
35
36
    /**
37
     * Returns the regexp pattern to filter method names.
38
     */
39
    abstract protected function getMethodPattern(): string;
40
41
    /**
42
     * Get the entire configuration for a method.
43
     */
44
    abstract protected function methodToConfiguration(ReflectionMethod $method): ?array;
45
46
    /**
47
     * Create a configuration for all fields of Doctrine entity.
48
     *
49
     * @param class-string $className
50
     */
51 24
    public function create(string $className): array
52
    {
53 24
        $this->findIdentityField($className);
54
55 24
        $class = $this->metadata->getReflectionClass();
56 24
        $methods = $class->getMethods(ReflectionMethod::IS_PUBLIC);
57 24
        $fieldConfigurations = [];
58 24
        foreach ($methods as $method) {
59
            // Skip non-callable or non-instance
60 24
            if ($method->isAbstract() || $method->isStatic()) {
61 1
                continue;
62
            }
63
64
            // Skip non-getter methods
65 24
            $name = $method->getName();
66 24
            if (!preg_match($this->getMethodPattern(), $name)) {
67 18
                continue;
68
            }
69
70
            // Skip exclusion specified by user
71 24
            if ($this->isExcluded($method)) {
72 3
                continue;
73
            }
74
75 24
            $configuration = $this->methodToConfiguration($method);
76 18
            if ($configuration) {
77 18
                $fieldConfigurations[] = $configuration;
78
            }
79
        }
80
81 18
        return $fieldConfigurations;
82
    }
83
84
    /**
85
     * Returns whether the getter is excluded.
86
     */
87 24
    private function isExcluded(ReflectionMethod $method): bool
88
    {
89 24
        $exclude = $this->reader->getAttribute($method, Exclude::class);
90
91 24
        return $exclude !== null;
92
    }
93
94
    /**
95
     * Get a GraphQL type instance from PHP type hinted type, possibly looking up the content of collections.
96
     */
97 11
    final protected function getTypeFromReturnTypeHint(ReflectionMethod $method, string $fieldName): ?Type
98
    {
99 11
        $returnType = $method->getReturnType();
100 11
        if (!$returnType instanceof ReflectionNamedType) {
101 1
            return null;
102
        }
103
104 10
        $returnTypeName = $returnType->getName();
105 10
        if (is_a($returnTypeName, Collection::class, true) || $returnTypeName === 'array') {
106 3
            $targetEntity = $this->getTargetEntity($fieldName);
107 3
            if (!$targetEntity) {
108 1
                throw new Exception('The method ' . $this->getMethodFullName($method) . ' is type hinted with a return type of `' . $returnTypeName . '`, but the entity contained in that collection could not be automatically detected. Either fix the type hint, fix the doctrine mapping, or specify the type with `#[API\Field]` attribute.');
109
            }
110
111 2
            $type = Type::listOf(Type::nonNull($this->getTypeFromRegistry($targetEntity, false)));
112 2
            if (!$returnType->allowsNull()) {
113 2
                $type = Type::nonNull($type);
114
            }
115
116 2
            return $type;
117
        }
118
119 9
        return $this->reflectionTypeToType($returnType);
120
    }
121
122
    /**
123
     * Convert a reflected type to GraphQL Type.
124
     */
125 14
    final protected function reflectionTypeToType(ReflectionNamedType $reflectionType, bool $isEntityId = false): Type
126
    {
127 14
        $name = $reflectionType->getName();
128 14
        if ($name === 'self') {
129 5
            $name = $this->metadata->name;
130
        }
131
132 14
        $type = $this->getTypeFromRegistry($name, $isEntityId);
133 14
        if (!$reflectionType->allowsNull()) {
134 13
            $type = Type::nonNull($type);
135
        }
136
137
        // @phpstan-ignore-next-line
138 14
        return $type;
139
    }
140
141
    /**
142
     * Look up which field is the ID.
143
     *
144
     * @param class-string $className
145
     */
146 24
    private function findIdentityField(string $className): void
147
    {
148 24
        $this->metadata = $this->entityManager->getClassMetadata($className);
149 24
        foreach ($this->metadata->fieldMappings as $meta) {
150 24
            if ($meta->id ?? false) {
151 24
                $this->identityField = $meta->fieldName;
152
            }
153
        }
154
    }
155
156
    /**
157
     * Returns the fully qualified method name.
158
     */
159 6
    final protected function getMethodFullName(ReflectionMethod $method): string
160
    {
161 6
        return '`' . $method->getDeclaringClass()->getName() . '::' . $method->getName() . '()`';
162
    }
163
164
    /**
165
     * Throws exception if type is an array.
166
     */
167 16
    final protected function throwIfArray(ReflectionParameter $param, ?string $type): void
168
    {
169 16
        if ($type === 'array') {
170 1
            throw new Exception('The parameter `$' . $param->getName() . '` on method ' . $this->getMethodFullName($param->getDeclaringFunction()) . ' is type hinted as `array` and is not overridden via `#[API\Argument]` attribute. Either change the type hint or specify the type with `#[API\Argument]` attribute.');
171
        }
172
    }
173
174
    /**
175
     * Returns whether the given field name is the identity for the entity.
176
     */
177 13
    final protected function isIdentityField(string $fieldName): bool
178
    {
179 13
        return $this->identityField === $fieldName;
180
    }
181
182
    /**
183
     * Finds the target entity in the given association.
184
     */
185 3
    private function getTargetEntity(string $fieldName): ?string
186
    {
187 3
        return $this->metadata->associationMappings[$fieldName]->targetEntity ?? null;
188
    }
189
190
    /**
191
     * Return the default value, if any, of the property for the current entity.
192
     *
193
     * It does take into account that the property might be defined on a parent class
194
     * of entity. And it will find it if that is the case.
195
     */
196 9
    final protected function getPropertyDefaultValue(string $fieldName): mixed
197
    {
198 9
        $property = $this->metadata->getReflectionProperties()[$fieldName] ?? null;
199 9
        if (!$property) {
200 6
            return null;
201
        }
202
203 8
        return $property->getDeclaringClass()->getDefaultProperties()[$fieldName] ?? null;
204
    }
205
206
    /**
207
     * Input with default values cannot be non-null.
208
     */
209 17
    final protected function nonNullIfHasDefault(AbstractAttribute $attribute): void
210
    {
211 17
        $type = $attribute->getTypeInstance();
212 17
        if ($type instanceof NonNull && $attribute->hasDefaultValue()) {
213 10
            $attribute->setTypeInstance($type->getWrappedType());
214
        }
215
    }
216
217
    /**
218
     * Throws exception if argument type is invalid.
219
     */
220 17
    final protected function throwIfNotInputType(ReflectionParameter $param, AbstractAttribute $attribute): void
221
    {
222 17
        $type = $attribute->getTypeInstance();
223 17
        $class = new ReflectionClass($attribute);
224 17
        $attributeName = $class->getShortName();
225
226 17
        if (!$type) {
227 2
            throw new Exception('Could not find type for parameter `$' . $param->name . '` for method ' . $this->getMethodFullName($param->getDeclaringFunction()) . '. Either type hint the parameter, or specify the type with `#[API\\' . $attributeName . ']` attribute.');
228
        }
229
230 15
        if ($type instanceof WrappingType) {
231 14
            $type = $type->getInnermostType();
232
        }
233
234 15
        if (!($type instanceof InputType)) {
235 1
            throw new Exception('Type for parameter `$' . $param->name . '` for method ' . $this->getMethodFullName($param->getDeclaringFunction()) . ' must be an instance of `' . InputType::class . '`, but was `' . $type::class . '`. Use `#[API\\' . $attributeName . ']` attribute to specify a custom InputType.');
236
        }
237
    }
238
}
239