Completed
Push — master ( cc6449...623d82 )
by David
16s
created

GlobTypeMapper::buildMap()   A

Complexity

Conditions 6
Paths 6

Size

Total Lines 28
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 15
dl 0
loc 28
rs 9.2222
c 0
b 0
f 0
cc 6
nc 6
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\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
            $keyNameCache = 'globTypeMapper_names_'.str_replace('\\', '_', $this->namespace);
92
            $this->mapClassToTypeArray = $this->cache->get($key);
93
            $this->mapNameToType = $this->cache->get($keyNameCache);
94
            if ($this->mapClassToTypeArray === null || $this->mapNameToType === null) {
95
                $this->buildMap();
96
                // This is a very short lived cache. Useful to avoid overloading a server in case of heavy load.
97
                // Defaults to 2 seconds.
98
                $this->cache->set($key, $this->mapClassToTypeArray, $this->globTtl);
99
                $this->cache->set($keyNameCache, $this->mapNameToType, $this->globTtl);
100
            }
101
        }
102
        return $this->mapClassToTypeArray;
103
    }
104
105
    private function buildMap(): void
106
    {
107
        $explorer = new GlobClassExplorer($this->namespace, $this->cache, $this->globTtl, ClassNameMapper::createFromComposerFile(null, null, true));
108
        $classes = $explorer->getClasses();
109
        foreach ($classes as $className) {
110
            if (!\class_exists($className)) {
111
                continue;
112
            }
113
            $refClass = new \ReflectionClass($className);
114
            if (!$refClass->isInstantiable()) {
115
                continue;
116
            }
117
118
            $type = $this->annotationReader->getTypeAnnotation($refClass);
119
120
            if ($type === null) {
121
                continue;
122
            }
123
            if (isset($this->mapClassToTypeArray[$type->getClass()])) {
124
                /*if ($this->mapClassToTypeArray[$type->getClass()] === $className) {
125
                    // Already mapped. Let's continue
126
                    continue;
127
                }*/
128
                throw DuplicateMappingException::create($type->getClass(), $this->mapClassToTypeArray[$type->getClass()], $className);
129
            }
130
            $this->storeTypeInCache($className, $type, $refClass->getFileName());
131
        }
132
        $this->fullMapComputed = true;
133
    }
134
135
    /**
136
     * Stores in cache the mapping TypeClass <=> Object class <=> GraphQL type name.
137
     */
138
    private function storeTypeInCache(string $typeClassName, Type $type, string $typeFileName)
139
    {
140
        $objectClassName = $type->getClass();
141
        $this->mapClassToTypeArray[$objectClassName] = $typeClassName;
142
        $this->cache->set('globTypeMapperByClass_'.str_replace('\\', '_', $objectClassName), [
143
            'filemtime' => filemtime($typeFileName),
144
            'typeFileName' => $typeFileName,
145
            'typeClass' => $typeClassName
146
        ], $this->mapTtl);
147
        $typeName = $this->typeGenerator->getName($typeClassName, $type);
148
        $this->mapNameToType[$typeName] = $typeClassName;
149
        $this->cache->set('globTypeMapperByName_'.$typeName, [
150
            'filemtime' => filemtime($typeFileName),
151
            'typeFileName' => $typeFileName,
152
            'typeClass' => $typeClassName
153
        ], $this->mapTtl);
154
    }
155
156
    private function getTypeFromCacheByObjectClass(string $className): ?string
157
    {
158
        if (isset($this->mapClassToTypeArray[$className])) {
159
            return $this->mapClassToTypeArray[$className];
160
        }
161
162
        // Let's try from the cache
163
        $item = $this->cache->get('globTypeMapperByClass_'.str_replace('\\', '_', $className));
164
        if ($item !== null) {
165
            [
166
                'filemtime' => $filemtime,
167
                'typeFileName' => $typeFileName,
168
                'typeClass' => $typeClassName
169
            ] = $item;
170
171
            if ($filemtime === filemtime($typeFileName)) {
172
                $this->mapClassToTypeArray[$className] = $typeClassName;
173
                return $typeClassName;
174
            }
175
        }
176
177
        // cache miss
178
        return null;
179
    }
180
181
    private function getTypeFromCacheByGraphQLTypeName(string $graphqlTypeName): ?string
182
    {
183
        if (isset($this->mapNameToType[$graphqlTypeName])) {
184
            return $this->mapNameToType[$graphqlTypeName];
185
        }
186
187
        // Let's try from the cache
188
        $item = $this->cache->get('globTypeMapperByName_'.$graphqlTypeName);
189
        if ($item !== null) {
190
            [
191
                'filemtime' => $filemtime,
192
                'typeFileName' => $typeFileName,
193
                'typeClass' => $typeClassName
194
            ] = $item;
195
196
            if ($filemtime === filemtime($typeFileName)) {
197
                $this->mapNameToType[$graphqlTypeName] = $typeClassName;
198
                return $typeClassName;
199
            }
200
        }
201
202
        // cache miss
203
        return null;
204
    }
205
206
    /**
207
     * Returns true if this type mapper can map the $className FQCN to a GraphQL type.
208
     *
209
     * @param string $className
210
     * @return bool
211
     */
212
    public function canMapClassToType(string $className): bool
213
    {
214
        $typeClassName = $this->getTypeFromCacheByObjectClass($className);
215
216
        if ($typeClassName === null) {
217
            $this->getMap();
218
        }
219
220
        return isset($this->mapClassToTypeArray[$className]);
221
    }
222
223
    /**
224
     * Maps a PHP fully qualified class name to a GraphQL type.
225
     *
226
     * @param string $className
227
     * @param RecursiveTypeMapperInterface $recursiveTypeMapper
228
     * @return ObjectType
229
     * @throws CannotMapTypeException
230
     */
231
    public function mapClassToType(string $className, RecursiveTypeMapperInterface $recursiveTypeMapper): ObjectType
232
    {
233
        $typeClassName = $this->getTypeFromCacheByObjectClass($className);
234
235
        if ($typeClassName === null) {
236
            $this->getMap();
237
        }
238
239
        if (!isset($this->mapClassToTypeArray[$className])) {
240
            throw CannotMapTypeException::createForType($className);
241
        }
242
        return $this->typeGenerator->mapAnnotatedObject($this->container->get($this->mapClassToTypeArray[$className]), $recursiveTypeMapper);
243
    }
244
245
    /**
246
     * Returns the list of classes that have matching input GraphQL types.
247
     *
248
     * @return string[]
249
     */
250
    public function getSupportedClasses(): array
251
    {
252
        return array_keys($this->getMap());
253
    }
254
255
    /**
256
     * Returns true if this type mapper can map the $className FQCN to a GraphQL input type.
257
     *
258
     * @param string $className
259
     * @return bool
260
     */
261
    public function canMapClassToInputType(string $className): bool
262
    {
263
        return false;
264
    }
265
266
    /**
267
     * Maps a PHP fully qualified class name to a GraphQL input type.
268
     *
269
     * @param string $className
270
     * @return InputType
271
     * @throws CannotMapTypeException
272
     */
273
    public function mapClassToInputType(string $className): InputType
274
    {
275
        throw CannotMapTypeException::createForInputType($className);
276
    }
277
278
    /**
279
     * Returns a GraphQL type by name (can be either an input or output type)
280
     *
281
     * @param string $typeName The name of the GraphQL type
282
     * @param RecursiveTypeMapperInterface $recursiveTypeMapper
283
     * @return \GraphQL\Type\Definition\Type&(InputType|OutputType)
284
     * @throws CannotMapTypeException
285
     * @throws \ReflectionException
286
     */
287
    public function mapNameToType(string $typeName, RecursiveTypeMapperInterface $recursiveTypeMapper): \GraphQL\Type\Definition\Type
288
    {
289
        $typeClassName = $this->getTypeFromCacheByGraphQLTypeName($typeName);
290
291
        if ($typeClassName === null) {
292
            $this->getMap();
293
        }
294
295
        if (!isset($this->mapNameToType[$typeName])) {
296
            throw CannotMapTypeException::createForName($typeName);
297
        }
298
        return $this->typeGenerator->mapAnnotatedObject($this->container->get($this->mapNameToType[$typeName]), $recursiveTypeMapper);
299
    }
300
301
    /**
302
     * Returns true if this type mapper can map the $typeName GraphQL name to a GraphQL type.
303
     *
304
     * @param string $typeName The name of the GraphQL type
305
     * @return bool
306
     */
307
    public function canMapNameToType(string $typeName): bool
308
    {
309
        $typeClassName = $this->getTypeFromCacheByGraphQLTypeName($typeName);
310
311
        if ($typeClassName !== null) {
312
            return true;
313
        }
314
315
        $this->getMap();
316
317
        return isset($this->mapNameToType[$typeName]);
318
    }
319
}
320