Types   A
last analyzed

Complexity

Total Complexity 36

Size/Duplication

Total Lines 294
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 36
dl 0
loc 294
rs 9.52
c 1
b 0
f 0
eloc 104
ccs 114
cts 114
cp 1

20 Methods

Rating   Name   Duplication   Size   Complexity  
A getFilter() 0 6 1
A getOutput() 0 6 1
A createFilteredQueryBuilder() 0 3 1
A throwIfNotEntity() 0 4 2
A getJoinOn() 0 6 1
A getPartialInput() 0 6 1
A getFilterGroupJoin() 0 6 1
B loadType() 0 33 7
A getViaFactory() 0 10 2
A registerInstance() 0 3 1
A getOperator() 0 14 3
A initializeInternalTypes() 0 23 1
A has() 0 3 3
A getFilterGroupCondition() 0 6 1
A get() 0 15 4
A getId() 0 6 1
A getInput() 0 6 1
A isEntity() 0 3 2
A getSorting() 0 6 1
A __construct() 0 16 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 121
    public function __construct(
67
        private readonly EntityManager $entityManager,
68
        private readonly ?ContainerInterface $customTypes = null,
69
    ) {
70 121
        $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...
71 121
        $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...
72 121
        $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...
73 121
        $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...
74 121
        $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...
75 121
        $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...
76 121
        $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...
77 121
        $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...
78 121
        $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...
79 121
        $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...
80
81 121
        $this->initializeInternalTypes();
82
    }
83
84 44
    public function has(string $key): bool
85
    {
86 44
        return $this->customTypes && $this->customTypes->has($key) || array_key_exists($key, $this->types);
87
    }
88
89 54
    public function get(string $key): Type&NamedType
90
    {
91 54
        if ($this->customTypes && $this->customTypes->has($key)) {
92
            /** @var NamedType&Type $t */
93 36
            $t = $this->customTypes->get($key);
94 36
            $this->registerInstance($t);
95
96 36
            return $t;
97
        }
98
99 43
        if (array_key_exists($key, $this->types)) {
100 42
            return $this->types[$key];
101
        }
102
103 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 . '`.');
104
    }
105
106
    /**
107
     * Get a type from internal registry, and create it via the factory if needed.
108
     *
109
     * @param class-string $className
110
     */
111 65
    private function getViaFactory(string $className, string $typeName, AbstractTypeFactory $factory): Type&NamedType
112
    {
113 65
        $this->throwIfNotEntity($className);
114
115 64
        if (!isset($this->types[$typeName])) {
116 64
            $instance = $factory->create($className, $typeName);
117 64
            $this->registerInstance($instance);
118
        }
119
120 64
        return $this->types[$typeName];
121
    }
122
123 19
    public function getOutput(string $className): ObjectType
124
    {
125
        /** @var ObjectType $type */
126 19
        $type = $this->getViaFactory($className, Utils::getTypeName($className), $this->objectTypeFactory);
127
128 18
        return $type;
129
    }
130
131 10
    public function getInput(string $className): InputObjectType
132
    {
133
        /** @var InputObjectType $type */
134 10
        $type = $this->getViaFactory($className, Utils::getTypeName($className) . 'Input', $this->inputTypeFactory);
135
136 10
        return $type;
137
    }
138
139 2
    public function getPartialInput(string $className): InputObjectType
140
    {
141
        /** @var InputObjectType $type */
142 2
        $type = $this->getViaFactory($className, Utils::getTypeName($className) . 'PartialInput', $this->partialInputTypeFactory);
143
144 2
        return $type;
145
    }
146
147 28
    public function getFilter(string $className): InputObjectType
148
    {
149
        /** @var InputObjectType $type */
150 28
        $type = $this->getViaFactory($className, Utils::getTypeName($className) . 'Filter', $this->filterTypeFactory);
151
152 28
        return $type;
153
    }
154
155 4
    public function getSorting(string $className): ListOfType
156
    {
157
        /** @var InputObjectType $type */
158 4
        $type = $this->getViaFactory($className, Utils::getTypeName($className) . 'Sorting', $this->sortingTypeFactory);
159
160 4
        return Type::listOf(Type::nonNull($type));
161
    }
162
163
    /**
164
     * Returns a joinOn input type for the given entity.
165
     *
166
     * This is for internal use only.
167
     *
168
     * @param class-string $className the class name of an entity (`Post::class`)
169
     */
170 2
    public function getJoinOn(string $className): InputObjectType
171
    {
172
        /** @var InputObjectType $type */
173 2
        $type = $this->getViaFactory($className, 'JoinOn' . Utils::getTypeName($className), $this->joinOnTypeFactory);
174
175 2
        return $type;
176
    }
177
178
    /**
179
     * Returns a joins input type for the given entity.
180
     *
181
     * This is for internal use only.
182
     *
183
     * @param class-string $className the class name of an entity (`Post::class`)
184
     */
185 24
    public function getFilterGroupJoin(string $className): InputObjectType
186
    {
187
        /** @var InputObjectType $type */
188 24
        $type = $this->getViaFactory($className, Utils::getTypeName($className) . 'FilterGroupJoin', $this->filterGroupJoinTypeFactory);
189
190 24
        return $type;
191
    }
192
193
    /**
194
     * Returns a condition input type for the given entity.
195
     *
196
     * This is for internal use only.
197
     *
198
     * @param class-string $className the class name of an entity (`Post::class`)
199
     */
200 28
    public function getFilterGroupCondition(string $className): InputObjectType
201
    {
202
        /** @var InputObjectType $type */
203 28
        $type = $this->getViaFactory($className, Utils::getTypeName($className) . 'FilterGroupCondition', $this->filterGroupConditionTypeFactory);
204
205 28
        return $type;
206
    }
207
208 8
    public function getId(string $className): EntityIDType
209
    {
210
        /** @var EntityIDType $type */
211 8
        $type = $this->getViaFactory($className, Utils::getTypeName($className) . 'ID', $this->entityIDTypeFactory);
212
213 8
        return $type;
214
    }
215
216
    /**
217
     * Returns an operator input type.
218
     *
219
     * This is for internal use only.
220
     *
221
     * @param class-string<AbstractOperator> $className the class name of an operator (`EqualOperatorType::class`)
222
     */
223 20
    public function getOperator(string $className, LeafType $type): AbstractOperator
224
    {
225 20
        if (!is_a($className, AbstractOperator::class, true)) {
226 1
            throw new Exception('Expects a FQCN implementing `' . AbstractOperator::class . '`, but instead got: ' . $className);
227
        }
228
229 19
        $key = Utils::getOperatorTypeName($className, $type);
230
231 19
        if (!isset($this->types[$key])) {
232 19
            $instance = new $className($this, $type);
233 19
            $this->registerInstance($instance);
234
        }
235
236 19
        return $this->types[$key];
237
    }
238
239
    /**
240
     * Register the given type in our internal registry with its name.
241
     *
242
     * This is for internal use only. You should declare custom types via the constructor, not this method.
243
     */
244 121
    public function registerInstance(Type&NamedType $instance): void
245
    {
246 121
        $this->types[$instance->name()] = $instance;
247
    }
248
249
    /**
250
     * Checks if a className is a valid doctrine entity.
251
     */
252 67
    public function isEntity(string $className): bool
253
    {
254 67
        return class_exists($className) && !$this->entityManager->getMetadataFactory()->isTransient($className);
255
    }
256
257
    /**
258
     * Initialize internal types for common needs.
259
     */
260 121
    private function initializeInternalTypes(): void
261
    {
262 121
        $phpToGraphQLMapping = [
263
            // PHP types
264 121
            'id' => Type::id(),
265 121
            'bool' => Type::boolean(),
266 121
            'int' => Type::int(),
267 121
            'float' => Type::float(),
268 121
            'string' => Type::string(),
269
270
            // Doctrine types
271 121
            'boolean' => Type::boolean(),
272 121
            'integer' => Type::int(),
273 121
            'smallint' => Type::int(),
274 121
            'bigint' => Type::int(),
275 121
            'decimal' => Type::string(),
276 121
            'text' => Type::string(),
277 121
        ];
278
279 121
        $this->types = $phpToGraphQLMapping;
280 121
        $this->registerInstance(new LogicalOperatorType());
281 121
        $this->registerInstance(new JoinTypeType());
282 121
        $this->registerInstance(new SortingOrderType());
283
    }
284
285
    /**
286
     * Throw an exception if the class name is not Doctrine entity.
287
     */
288 65
    private function throwIfNotEntity(string $className): void
289
    {
290 65
        if (!$this->isEntity($className)) {
291 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.');
292
        }
293
    }
294
295 22
    public function createFilteredQueryBuilder(string $className, array $filter, array $sorting): QueryBuilder
296
    {
297 22
        return $this->filteredQueryBuilderFactory->create($className, $filter, $sorting);
298
    }
299
300 12
    public function loadType(string $typeName, string $namespace): ?Type
301
    {
302 12
        if ($this->has($typeName)) {
303 1
            return $this->get($typeName);
304
        }
305
306 11
        if (preg_match('~^(?<shortName>.*)(?<kind>PartialInput)$~', $typeName, $m)
307 11
            || preg_match('~^(?<shortName>.*)(?<kind>Input|PartialInput|Filter|Sorting|FilterGroupJoin|FilterGroupCondition|ID)$~', $typeName, $m)
308 11
            || preg_match('~^(?<kind>JoinOn)(?<shortName>.*)$~', $typeName, $m)
309 11
            || preg_match('~^(?<shortName>.*)$~', $typeName, $m)) {
310 11
            $shortName = $m['shortName'];
311 11
            $kind = $m['kind'] ?? '';
312
313
            /** @var class-string $className */
314 11
            $className = $namespace . '\\' . $shortName;
315
316 11
            if ($this->isEntity($className)) {
317 9
                return match ($kind) {
318 1
                    'Input' => $this->getViaFactory($className, $typeName, $this->inputTypeFactory),
319 1
                    'PartialInput' => $this->getViaFactory($className, $typeName, $this->partialInputTypeFactory),
320 1
                    'Filter' => $this->getViaFactory($className, $typeName, $this->filterTypeFactory),
321 1
                    'Sorting' => $this->getViaFactory($className, $typeName, $this->sortingTypeFactory),
322 1
                    'JoinOn' => $this->getViaFactory($className, $typeName, $this->joinOnTypeFactory),
323 1
                    'FilterGroupJoin' => $this->getViaFactory($className, $typeName, $this->filterGroupJoinTypeFactory),
324 1
                    'FilterGroupCondition' => $this->getViaFactory($className, $typeName, $this->filterGroupConditionTypeFactory),
325 1
                    'ID' => $this->getViaFactory($className, $typeName, $this->entityIDTypeFactory),
326 1
                    '' => $this->getViaFactory($className, $typeName, $this->objectTypeFactory),
327 9
                    default => throw new Exception("Unsupported kind of type `$kind` when trying to load type `$typeName`"),
328 9
                };
329
            }
330
        }
331
332 2
        return null;
333
    }
334
}
335