Passed
Pull Request — master (#63)
by David
03:26
created

getTypeFromCacheByGraphQLTypeName()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 23
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 12
dl 0
loc 23
rs 9.8666
c 0
b 0
f 0
cc 4
nc 4
nop 1
1
<?php
2
3
4
namespace TheCodingMachine\GraphQL\Controllers\Mappers;
5
6
use function array_keys;
7
use function filemtime;
8
use GraphQL\Type\Definition\InputObjectType;
9
use GraphQL\Type\Definition\InputType;
10
use GraphQL\Type\Definition\ObjectType;
11
use GraphQL\Type\Definition\OutputType;
12
use Mouf\Composer\ClassNameMapper;
13
use Psr\Container\ContainerInterface;
14
use Psr\SimpleCache\CacheInterface;
15
use ReflectionMethod;
16
use TheCodingMachine\ClassExplorer\Glob\GlobClassExplorer;
17
use TheCodingMachine\GraphQL\Controllers\AnnotationReader;
18
use TheCodingMachine\GraphQL\Controllers\Annotations\Factory;
19
use TheCodingMachine\GraphQL\Controllers\Annotations\Type;
20
use TheCodingMachine\GraphQL\Controllers\InputTypeGenerator;
21
use TheCodingMachine\GraphQL\Controllers\NamingStrategy;
22
use TheCodingMachine\GraphQL\Controllers\TypeGenerator;
23
24
/**
25
 * Scans all the classes in a given namespace of the main project (not the vendor directory).
26
 * Analyzes all classes and uses the @Type annotation to find the types automatically.
27
 *
28
 * Assumes that the container contains a class whose identifier is the same as the class name.
29
 */
30
final class GlobTypeMapper implements TypeMapperInterface
31
{
32
    /**
33
     * @var string
34
     */
35
    private $namespace;
36
    /**
37
     * @var AnnotationReader
38
     */
39
    private $annotationReader;
40
    /**
41
     * @var CacheInterface
42
     */
43
    private $cache;
44
    /**
45
     * @var int|null
46
     */
47
    private $globTtl;
48
    /**
49
     * @var array<string,string> Maps a domain class to the GraphQL type annotated class
50
     */
51
    private $mapClassToTypeArray = [];
52
    /**
53
     * @var array<string,string> Maps a GraphQL type name to the GraphQL type annotated class
54
     */
55
    private $mapNameToType = [];
56
    /**
57
     * @var array<string,string[]> Maps a domain class to the factory method that creates the input type in the form [classname, methodname]
58
     */
59
    private $mapClassToFactory = [];
60
    /**
61
     * @var array<string,string[]> Maps a GraphQL input type name to the factory method that creates the input type in the form [classname, methodname]
62
     */
63
    private $mapInputNameToFactory = [];
64
    /**
65
     * @var ContainerInterface
66
     */
67
    private $container;
68
    /**
69
     * @var TypeGenerator
70
     */
71
    private $typeGenerator;
72
    /**
73
     * @var int|null
74
     */
75
    private $mapTtl;
76
    /**
77
     * @var bool
78
     */
79
    private $fullMapComputed = false;
80
    /**
81
     * @var NamingStrategy
82
     */
83
    private $namingStrategy;
84
    /**
85
     * @var InputTypeGenerator
86
     */
87
    private $inputTypeGenerator;
88
89
    /**
90
     * @param string $namespace The namespace that contains the GraphQL types (they must have a `@Type` annotation)
91
     */
92
    public function __construct(string $namespace, TypeGenerator $typeGenerator, InputTypeGenerator $inputTypeGenerator, ContainerInterface $container, AnnotationReader $annotationReader, NamingStrategy $namingStrategy, CacheInterface $cache, ?int $globTtl = 2, ?int $mapTtl = null)
93
    {
94
        $this->namespace = $namespace;
95
        $this->typeGenerator = $typeGenerator;
96
        $this->container = $container;
97
        $this->annotationReader = $annotationReader;
98
        $this->namingStrategy = $namingStrategy;
99
        $this->cache = $cache;
100
        $this->globTtl = $globTtl;
101
        $this->mapTtl = $mapTtl;
102
        $this->inputTypeGenerator = $inputTypeGenerator;
103
    }
104
105
    /**
106
     * Returns an array of fully qualified class names.
107
     *
108
     * @return array<string,string>
109
     */
110
    private function getMap(): array
111
    {
112
        if ($this->fullMapComputed === false) {
113
            $namespace = str_replace('\\', '_', $this->namespace);
114
            $keyClassCache = 'globTypeMapper_'.$namespace;
115
            $keyNameCache = 'globTypeMapper_names_'.$namespace;
116
            $keyInputClassCache = 'globInputTypeMapper_'.$namespace;
117
            $keyInputNameCache = 'globInputTypeMapper_names_'.$namespace;
118
            $this->mapClassToTypeArray = $this->cache->get($keyClassCache);
119
            $this->mapNameToType = $this->cache->get($keyNameCache);
120
            $this->mapClassToFactory = $this->cache->get($keyInputClassCache);
121
            $this->mapInputNameToFactory = $this->cache->get($keyInputNameCache);
122
            if ($this->mapClassToTypeArray === null || $this->mapNameToType === null || $this->mapClassToFactory === null || $this->mapInputNameToFactory) {
123
                $this->buildMap();
124
                // This is a very short lived cache. Useful to avoid overloading a server in case of heavy load.
125
                // Defaults to 2 seconds.
126
                $this->cache->set($keyClassCache, $this->mapClassToTypeArray, $this->globTtl);
127
                $this->cache->set($keyNameCache, $this->mapNameToType, $this->globTtl);
128
                $this->cache->set($keyInputClassCache, $this->mapClassToFactory, $this->globTtl);
129
                $this->cache->set($keyInputNameCache, $this->mapInputNameToFactory, $this->globTtl);
130
            }
131
        }
132
        return $this->mapClassToTypeArray;
133
    }
134
135
    private function buildMap(): void
136
    {
137
        $explorer = new GlobClassExplorer($this->namespace, $this->cache, $this->globTtl, ClassNameMapper::createFromComposerFile(null, null, true));
138
        $classes = $explorer->getClasses();
139
        foreach ($classes as $className) {
140
            if (!\class_exists($className)) {
141
                continue;
142
            }
143
            $refClass = new \ReflectionClass($className);
144
            if (!$refClass->isInstantiable()) {
145
                continue;
146
            }
147
148
            $type = $this->annotationReader->getTypeAnnotation($refClass);
149
150
            if ($type !== null) {
151
                if (isset($this->mapClassToTypeArray[$type->getClass()])) {
152
                    /*if ($this->mapClassToTypeArray[$type->getClass()] === $className) {
153
                        // Already mapped. Let's continue
154
                        continue;
155
                    }*/
156
                    throw DuplicateMappingException::createForType($type->getClass(), $this->mapClassToTypeArray[$type->getClass()], $className);
157
                }
158
                $this->storeTypeInCache($className, $type, $refClass->getFileName());
159
            }
160
161
            foreach ($refClass->getMethods() as $method) {
162
                $factory = $this->annotationReader->getFactoryAnnotation($method);
163
                if ($factory !== null) {
164
                    [$inputName, $className] = $this->inputTypeGenerator->getInputTypeNameAndClassName($method);
165
166
                    if (isset($this->mapClassToFactory[$className])) {
167
                        throw DuplicateMappingException::createForFactory($className, $this->mapClassToFactory[$type->getClass()][0], $this->mapClassToFactory[$type->getClass()][1], $className, $method->name);
168
                    }
169
                    $this->storeInputTypeInCache($method, $inputName, $className, $refClass->getFileName());
170
                }
171
            }
172
173
        }
174
        $this->fullMapComputed = true;
175
    }
176
177
    /**
178
     * Stores in cache the mapping TypeClass <=> Object class <=> GraphQL type name.
179
     */
180
    private function storeTypeInCache(string $typeClassName, Type $type, string $typeFileName): void
181
    {
182
        $objectClassName = $type->getClass();
183
        $this->mapClassToTypeArray[$objectClassName] = $typeClassName;
184
        $this->cache->set('globTypeMapperByClass_'.str_replace('\\', '_', $objectClassName), [
185
            'filemtime' => filemtime($typeFileName),
186
            'fileName' => $typeFileName,
187
            'typeClass' => $typeClassName
188
        ], $this->mapTtl);
189
        $typeName = $this->namingStrategy->getOutputTypeName($typeClassName, $type);
190
        $this->mapNameToType[$typeName] = $typeClassName;
191
        $this->cache->set('globTypeMapperByName_'.$typeName, [
192
            'filemtime' => filemtime($typeFileName),
193
            'fileName' => $typeFileName,
194
            'typeClass' => $typeClassName
195
        ], $this->mapTtl);
196
    }
197
198
    /**
199
     * Stores in cache the mapping between InputType name <=> Object class
200
     */
201
    private function storeInputTypeInCache(ReflectionMethod $refMethod, string $inputName, string $className, string $fileName): void
202
    {
203
        $refArray = [$refMethod->getDeclaringClass()->getName(), $refMethod->getName()];
204
        $this->mapClassToFactory[$className] = $refArray;
205
        $this->cache->set('globInputTypeMapperByClass_'.str_replace('\\', '_', $className), [
206
            'filemtime' => filemtime($fileName),
207
            'fileName' => $fileName,
208
            'factory' => $refArray
209
        ], $this->mapTtl);
210
        $this->mapInputNameToFactory[$inputName] = [];
211
        $this->cache->set('globInputTypeMapperByName_'.$inputName, [
212
            'filemtime' => filemtime($fileName),
213
            'fileName' => $fileName,
214
            'factory' => $refArray
215
        ], $this->mapTtl);
216
    }
217
218
219
    private function getTypeFromCacheByObjectClass(string $className): ?string
220
    {
221
        if (isset($this->mapClassToTypeArray[$className])) {
222
            return $this->mapClassToTypeArray[$className];
223
        }
224
225
        // Let's try from the cache
226
        $item = $this->cache->get('globTypeMapperByClass_'.str_replace('\\', '_', $className));
227
        if ($item !== null) {
228
            [
229
                'filemtime' => $filemtime,
230
                'fileName' => $typeFileName,
231
                'typeClass' => $typeClassName
232
            ] = $item;
233
234
            if ($filemtime === filemtime($typeFileName)) {
235
                $this->mapClassToTypeArray[$className] = $typeClassName;
236
                return $typeClassName;
237
            }
238
        }
239
240
        // cache miss
241
        return null;
242
    }
243
244
    private function getTypeFromCacheByGraphQLTypeName(string $graphqlTypeName): ?string
245
    {
246
        if (isset($this->mapNameToType[$graphqlTypeName])) {
247
            return $this->mapNameToType[$graphqlTypeName];
248
        }
249
250
        // Let's try from the cache
251
        $item = $this->cache->get('globTypeMapperByName_'.$graphqlTypeName);
252
        if ($item !== null) {
253
            [
254
                'filemtime' => $filemtime,
255
                'fileName' => $typeFileName,
256
                'typeClass' => $typeClassName
257
            ] = $item;
258
259
            if ($filemtime === filemtime($typeFileName)) {
260
                $this->mapNameToType[$graphqlTypeName] = $typeClassName;
261
                return $typeClassName;
262
            }
263
        }
264
265
        // cache miss
266
        return null;
267
    }
268
269
    /**
270
     * @return string[]|null A pointer to the factory [$className, $methodName] or null on cache miss
271
     */
272
    private function getFactoryFromCacheByObjectClass(string $className): ?array
273
    {
274
        if (isset($this->mapClassToFactory[$className])) {
275
            return $this->mapClassToFactory[$className];
276
        }
277
278
        // Let's try from the cache
279
        $item = $this->cache->get('globInputTypeMapperByClass_'.str_replace('\\', '_', $className));
280
        if ($item !== null) {
281
            [
282
                'filemtime' => $filemtime,
283
                'fileName' => $typeFileName,
284
                'factory' => $factory
285
            ] = $item;
286
287
            if ($filemtime === filemtime($typeFileName)) {
288
                $this->mapClassToFactory[$className] = $factory;
289
                return $factory;
290
            }
291
        }
292
293
        // cache miss
294
        return null;
295
    }
296
297
    /**
298
     * @return string[]|null A pointer to the factory [$className, $methodName] or null on cache miss
299
     */
300
    private function getFactoryFromCacheByGraphQLInputTypeName(string $graphqlTypeName): ?array
0 ignored issues
show
Unused Code introduced by
The method getFactoryFromCacheByGraphQLInputTypeName() is not used, and could be removed.

This check looks for private methods that have been defined, but are not used inside the class.

Loading history...
301
    {
302
        if (isset($this->mapInputNameToFactory[$graphqlTypeName])) {
303
            return $this->mapInputNameToFactory[$graphqlTypeName];
304
        }
305
306
        // Let's try from the cache
307
        $item = $this->cache->get('globInputTypeMapperByName_'.$graphqlTypeName);
308
        if ($item !== null) {
309
            [
310
                'filemtime' => $filemtime,
311
                'fileName' => $typeFileName,
312
                'factory' => $factory
313
            ] = $item;
314
315
            if ($filemtime === filemtime($typeFileName)) {
316
                $this->mapInputNameToFactory[$graphqlTypeName] = $factory;
317
                return $factory;
318
            }
319
        }
320
321
        // cache miss
322
        return null;
323
    }
324
325
    /**
326
     * Returns true if this type mapper can map the $className FQCN to a GraphQL type.
327
     *
328
     * @param string $className
329
     * @return bool
330
     */
331
    public function canMapClassToType(string $className): bool
332
    {
333
        $typeClassName = $this->getTypeFromCacheByObjectClass($className);
334
335
        if ($typeClassName === null) {
336
            $this->getMap();
337
        }
338
339
        return isset($this->mapClassToTypeArray[$className]);
340
    }
341
342
    /**
343
     * Maps a PHP fully qualified class name to a GraphQL type.
344
     *
345
     * @param string $className
346
     * @param RecursiveTypeMapperInterface $recursiveTypeMapper
347
     * @return ObjectType
348
     * @throws CannotMapTypeException
349
     */
350
    public function mapClassToType(string $className, RecursiveTypeMapperInterface $recursiveTypeMapper): ObjectType
351
    {
352
        $typeClassName = $this->getTypeFromCacheByObjectClass($className);
353
354
        if ($typeClassName === null) {
355
            $this->getMap();
356
        }
357
358
        if (!isset($this->mapClassToTypeArray[$className])) {
359
            throw CannotMapTypeException::createForType($className);
360
        }
361
        return $this->typeGenerator->mapAnnotatedObject($this->container->get($this->mapClassToTypeArray[$className]), $recursiveTypeMapper);
362
    }
363
364
    /**
365
     * Returns the list of classes that have matching input GraphQL types.
366
     *
367
     * @return string[]
368
     */
369
    public function getSupportedClasses(): array
370
    {
371
        return array_keys($this->getMap());
372
    }
373
374
    /**
375
     * Returns true if this type mapper can map the $className FQCN to a GraphQL input type.
376
     *
377
     * @param string $className
378
     * @return bool
379
     */
380
    public function canMapClassToInputType(string $className): bool
381
    {
382
        $factory = $this->getFactoryFromCacheByObjectClass($className);
383
384
        if ($factory === null) {
385
            $this->getMap();
386
        }
387
        return isset($this->mapClassToFactory[$className]);
388
    }
389
390
    /**
391
     * Maps a PHP fully qualified class name to a GraphQL input type.
392
     *
393
     * @param string $className
394
     * @param RecursiveTypeMapperInterface $recursiveTypeMapper
395
     * @return InputObjectType
396
     * @throws CannotMapTypeException
397
     */
398
    public function mapClassToInputType(string $className, RecursiveTypeMapperInterface $recursiveTypeMapper): InputObjectType
399
    {
400
        $factory = $this->getFactoryFromCacheByObjectClass($className);
401
402
        if ($factory === null) {
403
            $this->getMap();
404
        }
405
406
        if (!isset($this->mapClassToFactory[$className])) {
407
            throw CannotMapTypeException::createForInputType($className);
408
        }
409
        return $this->inputTypeGenerator->mapFactoryMethod($this->container->get($this->mapClassToFactory[$className][0]), $this->mapClassToFactory[$className][1], $recursiveTypeMapper);
410
    }
411
412
    /**
413
     * Returns a GraphQL type by name (can be either an input or output type)
414
     *
415
     * @param string $typeName The name of the GraphQL type
416
     * @param RecursiveTypeMapperInterface $recursiveTypeMapper
417
     * @return \GraphQL\Type\Definition\Type&(InputType|OutputType)
418
     * @throws CannotMapTypeException
419
     * @throws \ReflectionException
420
     */
421
    public function mapNameToType(string $typeName, RecursiveTypeMapperInterface $recursiveTypeMapper): \GraphQL\Type\Definition\Type
422
    {
423
        $typeClassName = $this->getTypeFromCacheByGraphQLTypeName($typeName);
424
425
        if ($typeClassName === null) {
426
            $this->getMap();
427
        }
428
429
        if (!isset($this->mapNameToType[$typeName])) {
430
            throw CannotMapTypeException::createForName($typeName);
431
        }
432
        return $this->typeGenerator->mapAnnotatedObject($this->container->get($this->mapNameToType[$typeName]), $recursiveTypeMapper);
433
    }
434
435
    /**
436
     * Returns true if this type mapper can map the $typeName GraphQL name to a GraphQL type.
437
     *
438
     * @param string $typeName The name of the GraphQL type
439
     * @return bool
440
     */
441
    public function canMapNameToType(string $typeName): bool
442
    {
443
        $typeClassName = $this->getTypeFromCacheByGraphQLTypeName($typeName);
444
445
        if ($typeClassName !== null) {
446
            return true;
447
        }
448
449
        $this->getMap();
450
451
        return isset($this->mapNameToType[$typeName]);
452
    }
453
}
454