Passed
Pull Request — master (#63)
by David
02:35
created

GlobTypeMapper::getMap()   A

Complexity

Conditions 6
Paths 3

Size

Total Lines 23
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 17
dl 0
loc 23
rs 9.0777
c 0
b 0
f 0
cc 6
nc 3
nop 0
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[$className][0], $this->mapClassToFactory[$className][1], $refClass->getName(), $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] = $refArray;
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
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
        if ($typeClassName === null) {
425
            $factory = $this->getFactoryFromCacheByGraphQLInputTypeName($typeName);
426
            if ($factory === null) {
427
                $this->getMap();
428
            }
429
        }
430
431
        if (isset($this->mapNameToType[$typeName])) {
432
            return $this->typeGenerator->mapAnnotatedObject($this->container->get($this->mapNameToType[$typeName]), $recursiveTypeMapper);
433
        }
434
        if (isset($this->mapInputNameToFactory[$typeName])) {
435
            $factory = $this->mapInputNameToFactory[$typeName];
436
            return $this->inputTypeGenerator->mapFactoryMethod($this->container->get($factory[0]), $factory[1], $recursiveTypeMapper);
437
        }
438
439
        throw CannotMapTypeException::createForName($typeName);
440
    }
441
442
    /**
443
     * Returns true if this type mapper can map the $typeName GraphQL name to a GraphQL type.
444
     *
445
     * @param string $typeName The name of the GraphQL type
446
     * @return bool
447
     */
448
    public function canMapNameToType(string $typeName): bool
449
    {
450
        $typeClassName = $this->getTypeFromCacheByGraphQLTypeName($typeName);
451
452
        if ($typeClassName !== null) {
453
            return true;
454
        }
455
456
        $factory = $this->getFactoryFromCacheByGraphQLInputTypeName($typeName);
457
        if ($factory !== null) {
458
            return true;
459
        }
460
461
        $this->getMap();
462
463
        return isset($this->mapNameToType[$typeName]) || isset($this->mapInputNameToFactory[$typeName]);
464
    }
465
}
466