Completed
Pull Request — master (#60)
by David
02:24
created

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