RecursiveTypeMapper::getInterfaceToClassNameMap()   A
last analyzed

Complexity

Conditions 3
Paths 3

Size

Total Lines 13
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

402
                $this->typeRegistry->registerType(/** @scrutinizer ignore-type */ $type);
Loading history...
403
            }
404
            if ($type instanceof MutableObjectType) {
405
                if ($this->typeMapper->canExtendTypeForName($typeName, $type, $this)) {
406
                    $this->typeMapper->extendTypeForName($typeName, $type, $this);
407
                }
408
                $type->freeze();
409
            }
410
            return $type;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $type could return the type GraphQL\Type\Definition\NamedType which is incompatible with the type-hinted return GraphQL\Type\Definition\Type. Consider adding an additional type-check to rule them out.
Loading history...
411
        }
412
413
        // Maybe the type is an interface?
414
        $interfaceToClassNameMap = $this->getInterfaceToClassNameMap();
415
        if (isset($interfaceToClassNameMap[$typeName])) {
416
            $className = $interfaceToClassNameMap[$typeName];
417
            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...
418
        }
419
420
        throw CannotMapTypeException::createForName($typeName);
421
    }
422
}
423