Completed
Pull Request — master (#60)
by David
01:49
created

RecursiveTypeMapper::getClassTree()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

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