Passed
Pull Request — master (#78)
by David
02:09
created

RecursiveTypeMapper::mapNameToType()   B

Complexity

Conditions 7
Paths 7

Size

Total Lines 26
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 15
dl 0
loc 26
rs 8.8333
c 0
b 0
f 0
cc 7
nc 7
nop 1
1
<?php
2
3
4
namespace TheCodingMachine\GraphQL\Controllers\Mappers;
5
6
7
use function array_flip;
8
use function array_reverse;
9
use function get_parent_class;
10
use GraphQL\Type\Definition\InputObjectType;
11
use GraphQL\Type\Definition\InputType;
12
use GraphQL\Type\Definition\InterfaceType;
13
use GraphQL\Type\Definition\ObjectType;
14
use GraphQL\Type\Definition\OutputType;
15
use GraphQL\Type\Definition\Type;
16
use Psr\SimpleCache\CacheInterface;
17
use TheCodingMachine\GraphQL\Controllers\NamingStrategyInterface;
18
use TheCodingMachine\GraphQL\Controllers\TypeRegistry;
19
use TheCodingMachine\GraphQL\Controllers\Types\InterfaceFromObjectType;
20
21
/**
22
 * This class wraps a TypeMapperInterface into a RecursiveTypeMapperInterface.
23
 * While the wrapped class does only tests one given class, the recursive type mapper
24
 * tests the class and all its parents.
25
 */
26
class RecursiveTypeMapper implements RecursiveTypeMapperInterface
27
{
28
    /**
29
     * @var TypeMapperInterface
30
     */
31
    private $typeMapper;
32
33
    /**
34
     * An array mapping a class name to the MappedClass instance (useful to know if the class has children)
35
     *
36
     * @var array<string,MappedClass>|null
37
     */
38
    private $mappedClasses;
39
40
    /**
41
     * An array of interfaces OR object types if no interface matching.
42
     *
43
     * @var array<string,OutputType&Type>
44
     */
45
    private $interfaces = [];
46
47
    /**
48
     * @var NamingStrategyInterface
49
     */
50
    private $namingStrategy;
51
52
    /**
53
     * @var CacheInterface
54
     */
55
    private $cache;
56
57
    /**
58
     * @var int|null
59
     */
60
    private $ttl;
61
62
    /**
63
     * @var array<string, string> An array mapping a GraphQL interface name to the PHP class name that triggered its generation.
64
     */
65
    private $interfaceToClassNameMap;
66
67
    /**
68
     * @var TypeRegistry
69
     */
70
    private $typeRegistry;
71
72
73
    public function __construct(TypeMapperInterface $typeMapper, NamingStrategyInterface $namingStrategy, CacheInterface $cache, TypeRegistry $typeRegistry, ?int $ttl = null)
74
    {
75
        $this->typeMapper = $typeMapper;
76
        $this->namingStrategy = $namingStrategy;
77
        $this->cache = $cache;
78
        $this->ttl = $ttl;
79
        $this->typeRegistry = $typeRegistry;
80
    }
81
82
    /**
83
     * Returns true if this type mapper can map the $className FQCN to a GraphQL type.
84
     *
85
     * @param string $className The class name to look for (this function looks into parent classes if the class does not match a type).
86
     * @return bool
87
     */
88
    public function canMapClassToType(string $className): bool
89
    {
90
        return $this->findClosestMatchingParent($className) !== null;
91
    }
92
93
    /**
94
     * Maps a PHP fully qualified class name to a GraphQL type.
95
     *
96
     * @param string $className The class name to look for (this function looks into parent classes if the class does not match a type)
97
     * @param (OutputType&ObjectType)|(OutputType&InterfaceType)|null $subType An optional sub-type if the main class is an iterator that needs to be typed.
98
     * @return ObjectType
99
     * @throws CannotMapTypeExceptionInterface
100
     */
101
    public function mapClassToType(string $className, ?OutputType $subType): ObjectType
102
    {
103
        $closestClassName = $this->findClosestMatchingParent($className);
104
        if ($closestClassName === null) {
105
            throw CannotMapTypeException::createForType($className);
106
        }
107
        $type = $this->typeMapper->mapClassToType($closestClassName, $subType, $this);
108
109
        // In the event this type was already part of cache, let's not extend it.
110
        if ($this->typeRegistry->hasType($type->name)) {
111
            return $type;
112
        }
113
114
        $type = $this->extendType($className, $type);
115
        $this->typeRegistry->registerType($type);
116
117
        return $type;
118
    }
119
120
    /**
121
     * Returns the closest parent that can be mapped, or null if nothing can be matched.
122
     *
123
     * @param string $className
124
     * @return string|null
125
     */
126
    private function findClosestMatchingParent(string $className): ?string
127
    {
128
        do {
129
            if ($this->typeMapper->canMapClassToType($className)) {
130
                return $className;
131
            }
132
        } while ($className = get_parent_class($className));
133
        return null;
134
    }
135
136
    /**
137
     * Extends a type using available type extenders.
138
     *
139
     * @param string $className
140
     * @param ObjectType $type
141
     * @return ObjectType
142
     * @throws CannotMapTypeExceptionInterface
143
     */
144
    private function extendType(string $className, ObjectType $type): ObjectType
145
    {
146
        $classes = [];
147
        do {
148
            if ($this->typeMapper->canExtendTypeForClass($className, $type)) {
149
                $classes[] = $className;
150
            }
151
        } while ($className = get_parent_class($className));
152
153
        // Let's apply extenders from the most basic type.
154
        $classes = array_reverse($classes);
155
        foreach ($classes as $class) {
156
            $type = $this->typeMapper->extendTypeForClass($class, $type, $this);
157
        }
158
159
        return $type;
160
    }
161
162
    /**
163
     * Maps a PHP fully qualified class name to a GraphQL type. Returns an interface if possible (if the class
164
     * has children) or returns an output type otherwise.
165
     *
166
     * @param string $className The exact class name to look for (this function does not look into parent classes).
167
     * @param (OutputType&ObjectType)|(OutputType&InterfaceType)|null $subType A subtype (if the main className is an iterator)
168
     * @return OutputType&Type
169
     * @throws CannotMapTypeExceptionInterface
170
     */
171
    public function mapClassToInterfaceOrType(string $className, ?OutputType $subType): OutputType
172
    {
173
        $closestClassName = $this->findClosestMatchingParent($className);
174
        if ($closestClassName === null) {
175
            throw CannotMapTypeException::createForType($className);
176
        }
177
        if (!isset($this->interfaces[$closestClassName])) {
178
            $objectType = $this->mapClassToType($className, $subType);
179
180
            $supportedClasses = $this->getClassTree();
181
            if (isset($supportedClasses[$closestClassName]) && !empty($supportedClasses[$closestClassName]->getChildren())) {
182
                // Cast as an interface
183
                $this->interfaces[$closestClassName] = new InterfaceFromObjectType($this->namingStrategy->getInterfaceNameFromConcreteName($objectType->name), $objectType, $subType, $this);
184
                $this->typeRegistry->registerType($this->interfaces[$closestClassName]);
185
            } else {
186
                $this->interfaces[$closestClassName] = $objectType;
187
            }
188
        }
189
        return $this->interfaces[$closestClassName];
190
    }
191
192
    /**
193
     * Build a map mapping GraphQL interface names to the PHP class name of the object creating this interface.
194
     *
195
     * @return array<string, string>
196
     */
197
    private function buildInterfaceToClassNameMap(): array
198
    {
199
        $map = [];
200
        $supportedClasses = $this->getClassTree();
201
        foreach ($supportedClasses as $className => $mappedClass) {
202
            if (!empty($mappedClass->getChildren())) {
203
                $objectType = $this->mapClassToType($className, null);
204
                $interfaceName = $this->namingStrategy->getInterfaceNameFromConcreteName($objectType->name);
205
                $map[$interfaceName] = $className;
206
            }
207
        }
208
        return $map;
209
    }
210
211
    /**
212
     * Returns a map mapping GraphQL interface names to the PHP class name of the object creating this interface.
213
     * The map may come from the cache.
214
     *
215
     * @return array<string, string>
216
     */
217
    private function getInterfaceToClassNameMap(): array
218
    {
219
        if ($this->interfaceToClassNameMap === null) {
220
            $key = 'recursiveTypeMapper_interfaceToClassNameMap';
221
            $this->interfaceToClassNameMap = $this->cache->get($key);
222
            if ($this->interfaceToClassNameMap === null) {
223
                $this->interfaceToClassNameMap = $this->buildInterfaceToClassNameMap();
224
                // This is a very short lived cache. Useful to avoid overloading a server in case of heavy load.
225
                // Defaults to 2 seconds.
226
                $this->cache->set($key, $this->interfaceToClassNameMap, $this->ttl);
227
            }
228
        }
229
        return $this->interfaceToClassNameMap;
230
    }
231
232
    /**
233
     * Finds the list of interfaces returned by $className.
234
     *
235
     * @param string $className
236
     * @return InterfaceType[]
237
     */
238
    public function findInterfaces(string $className): array
239
    {
240
        $interfaces = [];
241
        while ($className = $this->findClosestMatchingParent($className)) {
242
            $type = $this->mapClassToInterfaceOrType($className, null);
243
            if ($type instanceof InterfaceType) {
244
                $interfaces[] = $type;
245
            }
246
            $className = get_parent_class($className);
247
            if ($className === false) {
248
                break;
249
            }
250
        }
251
        return $interfaces;
252
    }
253
254
    /**
255
     * @return array<string,MappedClass>
256
     */
257
    private function getClassTree(): array
258
    {
259
        if ($this->mappedClasses === null) {
260
            $supportedClasses = array_flip($this->typeMapper->getSupportedClasses());
261
            foreach ($supportedClasses as $supportedClass => $foo) {
262
                $this->getMappedClass($supportedClass, $supportedClasses);
263
            }
264
        }
265
        return $this->mappedClasses;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->mappedClasses could return the type null which is incompatible with the type-hinted return array. Consider adding an additional type-check to rule them out.
Loading history...
266
    }
267
268
    /**
269
     * @param string $className
270
     * @param array<string,int> $supportedClasses
271
     * @return MappedClass
272
     */
273
    private function getMappedClass(string $className, array $supportedClasses): MappedClass
274
    {
275
        if (!isset($this->mappedClasses[$className])) {
276
            $mappedClass = new MappedClass(/*$className*/);
277
            $this->mappedClasses[$className] = $mappedClass;
278
            $parentClassName = $className;
279
            while ($parentClassName = get_parent_class($parentClassName)) {
280
                if (isset($supportedClasses[$parentClassName])) {
281
                    $parentMappedClass = $this->getMappedClass($parentClassName, $supportedClasses);
282
                    //$mappedClass->setParent($parentMappedClass);
283
                    $parentMappedClass->addChild($mappedClass);
284
                    break;
285
                }
286
            }
287
        }
288
        return $this->mappedClasses[$className];
289
    }
290
291
    /**
292
     * Returns true if this type mapper can map the $className FQCN to a GraphQL input type.
293
     *
294
     * @param string $className
295
     * @return bool
296
     */
297
    public function canMapClassToInputType(string $className): bool
298
    {
299
        return $this->typeMapper->canMapClassToInputType($className);
300
    }
301
302
    /**
303
     * Maps a PHP fully qualified class name to a GraphQL input type.
304
     *
305
     * @param string $className
306
     * @return InputObjectType
307
     * @throws CannotMapTypeExceptionInterface
308
     */
309
    public function mapClassToInputType(string $className): InputObjectType
310
    {
311
        return $this->typeMapper->mapClassToInputType($className, $this);
312
    }
313
314
    /**
315
     * Returns an array containing all OutputTypes.
316
     * Needed for introspection because of interfaces.
317
     *
318
     * @return array<string, OutputType>
319
     */
320
    public function getOutputTypes(): array
321
    {
322
        $types = [];
323
        foreach ($this->typeMapper->getSupportedClasses() as $supportedClass) {
324
            $types[$supportedClass] = $this->mapClassToType($supportedClass, null);
325
        }
326
        return $types;
327
    }
328
329
    /**
330
     * Returns true if this type mapper can map the $typeName GraphQL name to a GraphQL type.
331
     *
332
     * @param string $typeName The name of the GraphQL type
333
     * @return bool
334
     */
335
    public function canMapNameToType(string $typeName): bool
336
    {
337
        $result = $this->typeMapper->canMapNameToType($typeName);
338
        if ($result === true) {
339
            return true;
340
        }
341
342
        // Maybe the type is an interface?
343
        $interfaceToClassNameMap = $this->getInterfaceToClassNameMap();
344
        if (isset($interfaceToClassNameMap[$typeName])) {
345
            return true;
346
        }
347
348
        return false;
349
    }
350
351
    /**
352
     * Returns a GraphQL type by name (can be either an input or output type)
353
     *
354
     * @param string $typeName The name of the GraphQL type
355
     * @return Type&(InputType|OutputType)
356
     */
357
    public function mapNameToType(string $typeName): Type
358
    {
359
        if ($this->typeRegistry->hasType($typeName)) {
360
            return $this->typeRegistry->getType($typeName);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->typeRegistry->getType($typeName) returns the type GraphQL\Type\Definition\NamedType which is incompatible with the type-hinted return GraphQL\Type\Definition\Type.
Loading history...
361
        }
362
        if ($this->typeMapper->canMapNameToType($typeName)) {
363
            $type = $this->typeMapper->mapNameToType($typeName, $this);
364
            if (!$this->typeRegistry->hasType($typeName)) {
365
                if ($type instanceof ObjectType) {
366
                    if ($this->typeMapper->canExtendTypeForName($typeName, $type)) {
367
                        $type = $this->typeMapper->extendTypeForName($typeName, $type, $this);
368
                    }
369
                    $this->typeRegistry->registerType($type);
370
                }
371
            }
372
            return $type;
373
        }
374
375
        // Maybe the type is an interface?
376
        $interfaceToClassNameMap = $this->getInterfaceToClassNameMap();
377
        if (isset($interfaceToClassNameMap[$typeName])) {
378
            $className = $interfaceToClassNameMap[$typeName];
379
            return $this->mapClassToInterfaceOrType($className, null);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->mapClassTo...rType($className, null) returns the type GraphQL\Type\Definition\OutputType which is incompatible with the type-hinted return GraphQL\Type\Definition\Type.
Loading history...
380
        }
381
382
        throw CannotMapTypeException::createForName($typeName);
383
    }
384
}
385