Passed
Push — master ( 581283...087f55 )
by Adrien
14:24
created

Types::getOutput()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 2
dl 0
loc 6
rs 10
c 0
b 0
f 0
ccs 3
cts 3
cp 1
cc 1
nc 1
nop 1
crap 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace GraphQL\Doctrine;
6
7
use Doctrine\ORM\EntityManager;
8
use Doctrine\ORM\QueryBuilder;
9
use GraphQL\Doctrine\Definition\EntityIDType;
10
use GraphQL\Doctrine\Definition\JoinTypeType;
11
use GraphQL\Doctrine\Definition\LogicalOperatorType;
12
use GraphQL\Doctrine\Definition\Operator\AbstractOperator;
13
use GraphQL\Doctrine\Definition\SortingOrderType;
14
use GraphQL\Doctrine\Factory\FilteredQueryBuilderFactory;
15
use GraphQL\Doctrine\Factory\Type\AbstractTypeFactory;
16
use GraphQL\Doctrine\Factory\Type\EntityIDTypeFactory;
17
use GraphQL\Doctrine\Factory\Type\FilterGroupConditionTypeFactory;
18
use GraphQL\Doctrine\Factory\Type\FilterGroupJoinTypeFactory;
19
use GraphQL\Doctrine\Factory\Type\FilterTypeFactory;
20
use GraphQL\Doctrine\Factory\Type\InputTypeFactory;
21
use GraphQL\Doctrine\Factory\Type\JoinOnTypeFactory;
22
use GraphQL\Doctrine\Factory\Type\ObjectTypeFactory;
23
use GraphQL\Doctrine\Factory\Type\PartialInputTypeFactory;
24
use GraphQL\Doctrine\Factory\Type\SortingTypeFactory;
25
use GraphQL\Type\Definition\InputObjectType;
26
use GraphQL\Type\Definition\LeafType;
27
use GraphQL\Type\Definition\ListOfType;
28
use GraphQL\Type\Definition\NamedType;
29
use GraphQL\Type\Definition\ObjectType;
30
use GraphQL\Type\Definition\Type;
31
use Psr\Container\ContainerInterface;
32
use UnexpectedValueException;
33
34
/**
35
 * Registry of types to manage all GraphQL types.
36
 *
37
 * This is the entry point for the library.
38
 */
39
final class Types implements TypesInterface
40
{
41
    /**
42
     * @var array<string, NamedType&Type> mapping of type name to type instances
43
     */
44
    private array $types = [];
45
46
    private readonly ObjectTypeFactory $objectTypeFactory;
47
48
    private readonly InputTypeFactory $inputTypeFactory;
49
50
    private readonly PartialInputTypeFactory $partialInputTypeFactory;
51
52
    private readonly FilterTypeFactory $filterTypeFactory;
53
54
    private readonly FilteredQueryBuilderFactory $filteredQueryBuilderFactory;
55
56
    private readonly SortingTypeFactory $sortingTypeFactory;
57
58
    private readonly EntityIDTypeFactory $entityIDTypeFactory;
59
60
    private readonly JoinOnTypeFactory $joinOnTypeFactory;
61
62
    private readonly FilterGroupJoinTypeFactory $filterGroupJoinTypeFactory;
63
64
    private readonly FilterGroupConditionTypeFactory $filterGroupConditionTypeFactory;
65
66 117
    public function __construct(private readonly EntityManager $entityManager, private readonly ?ContainerInterface $customTypes = null)
67
    {
68 117
        $this->objectTypeFactory = new ObjectTypeFactory($this, $entityManager);
0 ignored issues
show
Bug introduced by
The property objectTypeFactory is declared read-only in GraphQL\Doctrine\Types.
Loading history...
69 117
        $this->inputTypeFactory = new InputTypeFactory($this, $entityManager);
0 ignored issues
show
Bug introduced by
The property inputTypeFactory is declared read-only in GraphQL\Doctrine\Types.
Loading history...
70 117
        $this->partialInputTypeFactory = new PartialInputTypeFactory($this, $entityManager);
0 ignored issues
show
Bug introduced by
The property partialInputTypeFactory is declared read-only in GraphQL\Doctrine\Types.
Loading history...
71 117
        $this->sortingTypeFactory = new SortingTypeFactory($this, $entityManager);
0 ignored issues
show
Bug introduced by
The property sortingTypeFactory is declared read-only in GraphQL\Doctrine\Types.
Loading history...
72 117
        $this->entityIDTypeFactory = new EntityIDTypeFactory($this, $entityManager);
0 ignored issues
show
Bug introduced by
The property entityIDTypeFactory is declared read-only in GraphQL\Doctrine\Types.
Loading history...
73 117
        $this->filterGroupJoinTypeFactory = new FilterGroupJoinTypeFactory($this, $entityManager);
0 ignored issues
show
Bug introduced by
The property filterGroupJoinTypeFactory is declared read-only in GraphQL\Doctrine\Types.
Loading history...
74 117
        $this->filterGroupConditionTypeFactory = new FilterGroupConditionTypeFactory($this, $entityManager);
0 ignored issues
show
Bug introduced by
The property filterGroupConditionTypeFactory is declared read-only in GraphQL\Doctrine\Types.
Loading history...
75 117
        $this->filteredQueryBuilderFactory = new FilteredQueryBuilderFactory($this, $entityManager, $this->sortingTypeFactory);
0 ignored issues
show
Bug introduced by
The property filteredQueryBuilderFactory is declared read-only in GraphQL\Doctrine\Types.
Loading history...
76 117
        $this->filterTypeFactory = new FilterTypeFactory($this, $entityManager, $this->filterGroupJoinTypeFactory, $this->filterGroupConditionTypeFactory);
0 ignored issues
show
Bug introduced by
The property filterTypeFactory is declared read-only in GraphQL\Doctrine\Types.
Loading history...
77 117
        $this->joinOnTypeFactory = new JoinOnTypeFactory($this, $entityManager, $this->filterGroupJoinTypeFactory, $this->filterGroupConditionTypeFactory);
0 ignored issues
show
Bug introduced by
The property joinOnTypeFactory is declared read-only in GraphQL\Doctrine\Types.
Loading history...
78
79 117
        $this->initializeInternalTypes();
80
    }
81
82 43
    public function has(string $key): bool
83
    {
84 43
        return $this->customTypes && $this->customTypes->has($key) || array_key_exists($key, $this->types);
85
    }
86
87 50
    public function get(string $key): Type&NamedType
88
    {
89 50
        if ($this->customTypes && $this->customTypes->has($key)) {
90
            /** @var NamedType&Type $t */
91 32
            $t = $this->customTypes->get($key);
92 32
            $this->registerInstance($t);
93
94 32
            return $t;
95
        }
96
97 41
        if (array_key_exists($key, $this->types)) {
98 40
            return $this->types[$key];
99
        }
100
101 1
        throw new Exception('No type registered with key `' . $key . '`. Either correct the usage, or register it in your custom types container when instantiating `' . self::class . '`.');
102
    }
103
104
    /**
105
     * Get a type from internal registry, and create it via the factory if needed.
106
     *
107
     * @param class-string $className
108
     */
109 61
    private function getViaFactory(string $className, string $typeName, AbstractTypeFactory $factory): Type&NamedType
110
    {
111 61
        $this->throwIfNotEntity($className);
112
113 60
        if (!isset($this->types[$typeName])) {
114 60
            $instance = $factory->create($className, $typeName);
115 60
            $this->registerInstance($instance);
116
        }
117
118 60
        return $this->types[$typeName];
119
    }
120
121 18
    public function getOutput(string $className): ObjectType
122
    {
123
        /** @var ObjectType $type */
124 18
        $type = $this->getViaFactory($className, Utils::getTypeName($className), $this->objectTypeFactory);
125
126 17
        return $type;
127
    }
128
129 8
    public function getInput(string $className): InputObjectType
130
    {
131
        /** @var InputObjectType $type */
132 8
        $type = $this->getViaFactory($className, Utils::getTypeName($className) . 'Input', $this->inputTypeFactory);
133
134 8
        return $type;
135
    }
136
137 2
    public function getPartialInput(string $className): InputObjectType
138
    {
139
        /** @var InputObjectType $type */
140 2
        $type = $this->getViaFactory($className, Utils::getTypeName($className) . 'PartialInput', $this->partialInputTypeFactory);
141
142 2
        return $type;
143
    }
144
145 27
    public function getFilter(string $className): InputObjectType
146
    {
147
        /** @var InputObjectType $type */
148 27
        $type = $this->getViaFactory($className, Utils::getTypeName($className) . 'Filter', $this->filterTypeFactory);
149
150 27
        return $type;
151
    }
152
153 4
    public function getSorting(string $className): ListOfType
154
    {
155
        /** @var InputObjectType $type */
156 4
        $type = $this->getViaFactory($className, Utils::getTypeName($className) . 'Sorting', $this->sortingTypeFactory);
157
158 4
        return Type::listOf(Type::nonNull($type));
159
    }
160
161
    /**
162
     * Returns a joinOn input type for the given entity.
163
     *
164
     * This is for internal use only.
165
     *
166
     * @param class-string $className the class name of an entity (`Post::class`)
167
     */
168 2
    public function getJoinOn(string $className): InputObjectType
169
    {
170
        /** @var InputObjectType $type */
171 2
        $type = $this->getViaFactory($className, 'JoinOn' . Utils::getTypeName($className), $this->joinOnTypeFactory);
172
173 2
        return $type;
174
    }
175
176
    /**
177
     * Returns a joins input type for the given entity.
178
     *
179
     * This is for internal use only.
180
     *
181
     * @param class-string $className the class name of an entity (`Post::class`)
182
     */
183 24
    public function getFilterGroupJoin(string $className): InputObjectType
184
    {
185
        /** @var InputObjectType $type */
186 24
        $type = $this->getViaFactory($className, Utils::getTypeName($className) . 'FilterGroupJoin', $this->filterGroupJoinTypeFactory);
187
188 24
        return $type;
189
    }
190
191
    /**
192
     * Returns a condition input type for the given entity.
193
     *
194
     * This is for internal use only.
195
     *
196
     * @param class-string $className the class name of an entity (`Post::class`)
197
     */
198 27
    public function getFilterGroupCondition(string $className): InputObjectType
199
    {
200
        /** @var InputObjectType $type */
201 27
        $type = $this->getViaFactory($className, Utils::getTypeName($className) . 'FilterGroupCondition', $this->filterGroupConditionTypeFactory);
202
203 27
        return $type;
204
    }
205
206 8
    public function getId(string $className): EntityIDType
207
    {
208
        /** @var EntityIDType $type */
209 8
        $type = $this->getViaFactory($className, Utils::getTypeName($className) . 'ID', $this->entityIDTypeFactory);
210
211 8
        return $type;
212
    }
213
214
    /**
215
     * Returns an operator input type.
216
     *
217
     * This is for internal use only.
218
     *
219
     * @param class-string<AbstractOperator> $className the class name of an operator (`EqualOperatorType::class`)
220
     */
221 19
    public function getOperator(string $className, LeafType $type): AbstractOperator
222
    {
223 19
        if (!is_a($className, AbstractOperator::class, true)) {
224 1
            throw new Exception('Expects a FQCN implementing `' . AbstractOperator::class . '`, but instead got: ' . $className);
225
        }
226
227 18
        $key = Utils::getOperatorTypeName($className, $type);
228
229 18
        if (!isset($this->types[$key])) {
230 18
            $instance = new $className($this, $type);
231 18
            $this->registerInstance($instance);
232
        }
233
234 18
        return $this->types[$key];
235
    }
236
237
    /**
238
     * Register the given type in our internal registry with its name.
239
     *
240
     * This is for internal use only. You should declare custom types via the constructor, not this method.
241
     */
242 117
    public function registerInstance(Type&NamedType $instance): void
243
    {
244 117
        $this->types[$instance->name()] = $instance;
245
    }
246
247
    /**
248
     * Checks if a className is a valid doctrine entity.
249
     */
250 63
    public function isEntity(string $className): bool
251
    {
252 63
        return class_exists($className) && !$this->entityManager->getMetadataFactory()->isTransient($className);
253
    }
254
255
    /**
256
     * Initialize internal types for common needs.
257
     */
258 117
    private function initializeInternalTypes(): void
259
    {
260 117
        $phpToGraphQLMapping = [
261
            // PHP types
262 117
            'id' => Type::id(),
263 117
            'bool' => Type::boolean(),
264 117
            'int' => Type::int(),
265 117
            'float' => Type::float(),
266 117
            'string' => Type::string(),
267
268
            // Doctrine types
269 117
            'boolean' => Type::boolean(),
270 117
            'integer' => Type::int(),
271 117
            'smallint' => Type::int(),
272 117
            'bigint' => Type::int(),
273 117
            'decimal' => Type::string(),
274 117
            'text' => Type::string(),
275 117
        ];
276
277 117
        $this->types = $phpToGraphQLMapping;
278 117
        $this->registerInstance(new LogicalOperatorType());
279 117
        $this->registerInstance(new JoinTypeType());
280 117
        $this->registerInstance(new SortingOrderType());
281
    }
282
283
    /**
284
     * Throw an exception if the class name is not Doctrine entity.
285
     */
286 61
    private function throwIfNotEntity(string $className): void
287
    {
288 61
        if (!$this->isEntity($className)) {
289 1
            throw new UnexpectedValueException('Given class name `' . $className . '` is not a Doctrine entity. Either register a custom GraphQL type for `' . $className . '` when instantiating `' . self::class . '`, or change the usage of that class to something else.');
290
        }
291
    }
292
293 22
    public function createFilteredQueryBuilder(string $className, array $filter, array $sorting): QueryBuilder
294
    {
295 22
        return $this->filteredQueryBuilderFactory->create($className, $filter, $sorting);
296
    }
297
298 12
    public function loadType(string $typeName, string $namespace): ?Type
299
    {
300 12
        if ($this->has($typeName)) {
301 1
            return $this->get($typeName);
302
        }
303
304 11
        if (preg_match('~^(?<shortName>.*)(?<kind>PartialInput)$~', $typeName, $m)
305 11
            || preg_match('~^(?<shortName>.*)(?<kind>Input|PartialInput|Filter|Sorting|FilterGroupJoin|FilterGroupCondition|ID)$~', $typeName, $m)
306 11
            || preg_match('~^(?<kind>JoinOn)(?<shortName>.*)$~', $typeName, $m)
307 11
            || preg_match('~^(?<shortName>.*)$~', $typeName, $m)) {
308 11
            $shortName = $m['shortName'];
309 11
            $kind = $m['kind'] ?? '';
310
311
            /** @var class-string $className */
312 11
            $className = $namespace . '\\' . $shortName;
313
314 11
            if ($this->isEntity($className)) {
315 9
                return match ($kind) {
316 9
                    'Input' => $this->getViaFactory($className, $typeName, $this->inputTypeFactory),
317 9
                    'PartialInput' => $this->getViaFactory($className, $typeName, $this->partialInputTypeFactory),
318 9
                    'Filter' => $this->getViaFactory($className, $typeName, $this->filterTypeFactory),
319 9
                    'Sorting' => $this->getViaFactory($className, $typeName, $this->sortingTypeFactory),
320 9
                    'JoinOn' => $this->getViaFactory($className, $typeName, $this->joinOnTypeFactory),
321 9
                    'FilterGroupJoin' => $this->getViaFactory($className, $typeName, $this->filterGroupJoinTypeFactory),
322 9
                    'FilterGroupCondition' => $this->getViaFactory($className, $typeName, $this->filterGroupConditionTypeFactory),
323 9
                    'ID' => $this->getViaFactory($className, $typeName, $this->entityIDTypeFactory),
324 9
                    '' => $this->getViaFactory($className, $typeName, $this->objectTypeFactory),
325 9
                    default => throw new Exception("Unsupported kind of type `$kind` when trying to load type `$typeName`"),
326 9
                };
327
            }
328
        }
329
330 2
        return null;
331
    }
332
}
333