1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Drupal\graphql\GraphQL\Schema; |
4
|
|
|
|
5
|
|
|
use Drupal\graphql\GraphQL\CacheableEdgeInterface; |
6
|
|
|
use Drupal\graphql\GraphQL\Utility\TypeCollector; |
7
|
|
|
use Drupal\Core\Cache\Cache; |
8
|
|
|
use Drupal\Core\Cache\CacheableDependencyInterface; |
9
|
|
|
use Drupal\Core\Cache\CacheableMetadata; |
10
|
|
|
use Drupal\Core\Cache\CacheBackendInterface; |
11
|
|
|
use Drupal\Core\Cache\Context\CacheContextsManager; |
12
|
|
|
use Drupal\graphql\Plugin\GraphQL\SchemaPluginManager; |
13
|
|
|
use Drupal\graphql\Plugin\GraphQL\TypeSystemPluginInterface; |
14
|
|
|
use Drupal\graphql\Plugin\GraphQL\TypeSystemPluginReferenceInterface; |
15
|
|
|
use Symfony\Component\HttpFoundation\RequestStack; |
16
|
|
|
use Youshido\GraphQL\Schema\AbstractSchema; |
17
|
|
|
use Youshido\GraphQL\Type\InputObject\AbstractInputObjectType; |
18
|
|
|
use Youshido\GraphQL\Type\InterfaceType\AbstractInterfaceType; |
19
|
|
|
use Youshido\GraphQL\Type\Object\AbstractObjectType; |
20
|
|
|
|
21
|
|
|
/** |
22
|
|
|
* Loads and caches a generated GraphQL schema. |
23
|
|
|
*/ |
24
|
|
|
class SchemaLoader { |
25
|
|
|
|
26
|
|
|
/** |
27
|
|
|
* The cache contexts manager service. |
28
|
|
|
* |
29
|
|
|
* @var \Drupal\Core\Cache\Context\CacheContextsManager |
30
|
|
|
*/ |
31
|
|
|
protected $contextsManager; |
32
|
|
|
|
33
|
|
|
/** |
34
|
|
|
* The schema plugin manager service. |
35
|
|
|
* |
36
|
|
|
* @var \Drupal\graphql\Plugin\GraphQL\SchemaPluginManager |
37
|
|
|
*/ |
38
|
|
|
protected $schemaManager; |
39
|
|
|
|
40
|
|
|
/** |
41
|
|
|
* The schema cache backend. |
42
|
|
|
* |
43
|
|
|
* @var \Drupal\Core\Cache\CacheBackendInterface |
44
|
|
|
*/ |
45
|
|
|
protected $schemaCache; |
46
|
|
|
|
47
|
|
|
/** |
48
|
|
|
* The cache metadata cache. |
49
|
|
|
* |
50
|
|
|
* @var \Drupal\Core\Cache\CacheBackendInterface |
51
|
|
|
*/ |
52
|
|
|
protected $metadataCache; |
53
|
|
|
|
54
|
|
|
/** |
55
|
|
|
* The service configuration. |
56
|
|
|
* |
57
|
|
|
* @var array |
58
|
|
|
*/ |
59
|
|
|
protected $config; |
60
|
|
|
|
61
|
|
|
/** |
62
|
|
|
* The request stack service. |
63
|
|
|
* |
64
|
|
|
* @var \Symfony\Component\HttpFoundation\RequestStack |
65
|
|
|
*/ |
66
|
|
|
protected $requestStack; |
67
|
|
|
|
68
|
|
|
/** |
69
|
|
|
* Static cache of loaded schemas. |
70
|
|
|
* |
71
|
|
|
* @var \Drupal\graphql\Plugin\GraphQL\SchemaPluginInterface[] |
72
|
|
|
*/ |
73
|
|
|
protected $schemas = []; |
74
|
|
|
|
75
|
|
|
/** |
76
|
|
|
* Static cache of loaded cache metadata. |
77
|
|
|
* |
78
|
|
|
* @var \Drupal\Core\Cache\RefinableCacheableDependencyInterface[] |
79
|
|
|
*/ |
80
|
|
|
protected $metadata = []; |
81
|
|
|
|
82
|
|
|
/** |
83
|
|
|
* Constructs a SchemaLoader object. |
84
|
|
|
* |
85
|
|
|
* @param \Drupal\Core\Cache\Context\CacheContextsManager $contextsManager |
86
|
|
|
* The cache contexts manager service. |
87
|
|
|
* @param \Symfony\Component\HttpFoundation\RequestStack $requestStack |
88
|
|
|
* The request stack service. |
89
|
|
|
* @param \Drupal\graphql\Plugin\GraphQL\SchemaPluginManager $schemaManager |
90
|
|
|
* The schema plugin manager service. |
91
|
|
|
* @param \Drupal\Core\Cache\CacheBackendInterface $schemaCache |
92
|
|
|
* The schema cache backend. |
93
|
|
|
* @param \Drupal\Core\Cache\CacheBackendInterface $metadataCache |
94
|
|
|
* The metadata cache backend. |
95
|
|
|
* @param array $config |
96
|
|
|
* The configuration provided through the services.yml. |
97
|
|
|
*/ |
98
|
|
|
public function __construct( |
99
|
|
|
CacheContextsManager $contextsManager, |
100
|
|
|
RequestStack $requestStack, |
101
|
|
|
SchemaPluginManager $schemaManager, |
102
|
|
|
CacheBackendInterface $schemaCache, |
103
|
|
|
CacheBackendInterface $metadataCache, |
104
|
|
|
array $config |
105
|
|
|
) { |
106
|
|
|
$this->config = $config; |
107
|
|
|
$this->schemaManager = $schemaManager; |
108
|
|
|
$this->contextsManager = $contextsManager; |
109
|
|
|
$this->schemaCache = $schemaCache; |
110
|
|
|
$this->metadataCache = $metadataCache; |
111
|
|
|
$this->requestStack = $requestStack; |
112
|
|
|
} |
113
|
|
|
|
114
|
|
|
/** |
115
|
|
|
* Loads and caches the generated schema. |
116
|
|
|
* |
117
|
|
|
* @param string $name |
118
|
|
|
* The name of the schema to load. |
119
|
|
|
* |
120
|
|
|
* @return \Drupal\graphql\Plugin\GraphQL\SchemaPluginInterface |
121
|
|
|
* The generated GraphQL schema. |
122
|
|
|
*/ |
123
|
|
|
public function getSchema($name) { |
124
|
|
|
if (array_key_exists($name, $this->schemas)) { |
125
|
|
|
return $this->schemas[$name]; |
126
|
|
|
} |
127
|
|
|
|
128
|
|
|
// The cache key is made up of all of the globally known cache contexts. |
129
|
|
|
if (!empty($this->config['schema_cache'])) { |
130
|
|
|
if (($contextCache = $this->metadataCache->get("$name:schema")) && $contextCache->data) { |
131
|
|
|
$cid = $this->getCacheIdentifier($name, $contextCache->data); |
132
|
|
|
|
133
|
|
|
if (($schema = $this->schemaCache->get($cid)) && $schema->data) { |
134
|
|
|
return $this->schemas[$name] = $schema->data; |
135
|
|
|
} |
136
|
|
|
} |
137
|
|
|
} |
138
|
|
|
|
139
|
|
|
$this->schemas[$name] = $this->schemaManager->createInstance($name)->getSchema(); |
140
|
|
|
// If the schema is not cacheable, just return it directly. |
141
|
|
|
if (empty($this->config['schema_cache'])) { |
142
|
|
|
return $this->schemas[$name]; |
143
|
|
|
} |
144
|
|
|
|
145
|
|
|
// Compute the cache identifier, tag and expiry time. |
146
|
|
|
$schemaCacheMetadata = $this->getSchemaCacheMetadata($name); |
147
|
|
|
if ($schemaCacheMetadata->getCacheMaxAge() !== 0) { |
148
|
|
|
$tags = $schemaCacheMetadata->getCacheTags(); |
149
|
|
|
$expire = $this->maxAgeToExpire($schemaCacheMetadata->getCacheMaxAge()); |
150
|
|
|
$cid = $this->getCacheIdentifier($name, $schemaCacheMetadata); |
151
|
|
|
|
152
|
|
|
// Write the cache entry for the schema cache entries. |
153
|
|
|
$this->schemaCache->set($cid, $this->schemas[$name], $expire, $tags); |
154
|
|
|
} |
155
|
|
|
|
156
|
|
|
return $this->schemas[$name]; |
157
|
|
|
} |
158
|
|
|
|
159
|
|
|
/** |
160
|
|
|
* Retrieves the schema's cache metadata. |
161
|
|
|
* |
162
|
|
|
* @param string $name |
163
|
|
|
* The name of the schema. |
164
|
|
|
* @return \Drupal\Core\Cache\CacheableDependencyInterface |
165
|
|
|
* The cache metadata for the schema. |
166
|
|
|
*/ |
167
|
|
|
public function getSchemaCacheMetadata($name) { |
168
|
|
|
return $this->getCacheMetadata($name, "$name:schema", function (AbstractSchema $schema) { |
169
|
|
|
return $this->extractSchemaCacheMetadata($schema); |
170
|
|
|
}); |
171
|
|
|
} |
172
|
|
|
|
173
|
|
|
/** |
174
|
|
|
* Retrieves the schema's response cache metadata. |
175
|
|
|
* |
176
|
|
|
* @param string $name |
177
|
|
|
* The name of the schema. |
178
|
|
|
* @return \Drupal\Core\Cache\RefinableCacheableDependencyInterface |
179
|
|
|
* The cache metadata for the schema's responses. |
180
|
|
|
*/ |
181
|
|
|
public function getResponseCacheMetadata($name) { |
182
|
|
|
return $this->getCacheMetadata($name, "$name:response", function (AbstractSchema $schema) { |
183
|
|
|
return $this->extractResponseCacheMetadata($schema); |
184
|
|
|
})->addCacheableDependency($this->getSchemaCacheMetadata($name)); |
185
|
|
|
} |
186
|
|
|
|
187
|
|
|
/** |
188
|
|
|
* Helper function to load cache metadata from a schema. |
189
|
|
|
* |
190
|
|
|
* @param string $name |
191
|
|
|
* The name of the schema. |
192
|
|
|
* @param string $cid |
193
|
|
|
* The cache identifier for caching the metadata |
194
|
|
|
* @param callable $callback |
195
|
|
|
* Callback to return the cache metadata from the schema. |
196
|
|
|
* |
197
|
|
|
* @return \Drupal\Core\Cache\RefinableCacheableDependencyInterface |
198
|
|
|
* The cache metadata. |
199
|
|
|
*/ |
200
|
|
|
protected function getCacheMetadata($name, $cid, callable $callback) { |
201
|
|
|
if (array_key_exists($cid, $this->metadata)) { |
202
|
|
|
return $this->metadata[$cid]; |
203
|
|
|
} |
204
|
|
|
|
205
|
|
|
// The cache key is made up of all of the globally known cache contexts. |
206
|
|
|
if (!empty($this->config['schema_cache'])) { |
207
|
|
|
if (($metadataCache = $this->metadataCache->get($cid)) && $metadataCache->data) { |
208
|
|
|
return $this->metadata[$name] = $metadataCache->data; |
209
|
|
|
} |
210
|
|
|
} |
211
|
|
|
|
212
|
|
|
/** @var \Drupal\Core\Cache\RefinableCacheableDependencyInterface $metadata */ |
213
|
|
|
$schema = $this->getSchema($name); |
214
|
|
|
$metadata = $callback($schema); |
215
|
|
|
$this->metadata[$cid] = $metadata; |
216
|
|
|
if (empty($this->config['schema_cache'])) { |
217
|
|
|
return $this->metadata[$cid]; |
218
|
|
|
} |
219
|
|
|
|
220
|
|
|
// Use the schema cache metadata to determine cache expiry and tags. |
221
|
|
|
$schemaCacheMetadata = $this->getSchemaCacheMetadata($name); |
222
|
|
|
if ($schemaCacheMetadata->getCacheMaxAge() !== 0) { |
223
|
|
|
$tags = $schemaCacheMetadata->getCacheTags(); |
224
|
|
|
$expire = $this->maxAgeToExpire($schemaCacheMetadata->getCacheMaxAge()); |
225
|
|
|
|
226
|
|
|
// Write the cache entry for the response cache metadata. |
227
|
|
|
$this->metadataCache->set($cid, $metadata, $expire, $tags); |
228
|
|
|
} |
229
|
|
|
|
230
|
|
|
return $metadata; |
231
|
|
|
} |
232
|
|
|
|
233
|
|
|
/** |
234
|
|
|
* Collects schema cache metadata from all types registered with the schema. |
235
|
|
|
* |
236
|
|
|
* The cache metadata is statically cached. This means that the schema may not |
237
|
|
|
* be modified after this method has been called. |
238
|
|
|
* |
239
|
|
|
* @param \Youshido\GraphQL\Schema\AbstractSchema $schema |
240
|
|
|
* The schema to extract the cache metadata from. |
241
|
|
|
* |
242
|
|
|
* @return \Drupal\Core\Cache\CacheableMetadata |
243
|
|
|
* The cache metadata collected from the schema's types. |
244
|
|
|
*/ |
245
|
|
View Code Duplication |
protected function extractSchemaCacheMetadata(AbstractSchema $schema) { |
|
|
|
|
246
|
|
|
$metadata = new CacheableMetadata(); |
247
|
|
|
$metadata->setCacheMaxAge(Cache::PERMANENT); |
248
|
|
|
$metadata->addCacheTags(['graphql_schema']); |
249
|
|
|
|
250
|
|
|
$metadata->addCacheableDependency($this->collectCacheMetadata($schema, function (CacheableEdgeInterface $item, AbstractSchema $schema) { |
251
|
|
|
return $item->getSchemaCacheMetadata($schema); |
252
|
|
|
})); |
253
|
|
|
|
254
|
|
|
return $metadata; |
255
|
|
|
} |
256
|
|
|
|
257
|
|
|
/** |
258
|
|
|
* Collects result cache metadata from all types registered with the schema. |
259
|
|
|
* |
260
|
|
|
* The cache metadata is statically cached. This means that the schema may not |
261
|
|
|
* be modified after this method has been called. |
262
|
|
|
* |
263
|
|
|
* @param \Youshido\GraphQL\Schema\AbstractSchema $schema |
264
|
|
|
* The schema to extract the cache metadata from. |
265
|
|
|
* |
266
|
|
|
* @return \Drupal\Core\Cache\CacheableMetadata |
267
|
|
|
* The cache metadata collected from the schema's types. |
268
|
|
|
*/ |
269
|
|
View Code Duplication |
protected function extractResponseCacheMetadata(AbstractSchema $schema) { |
|
|
|
|
270
|
|
|
$metadata = new CacheableMetadata(); |
271
|
|
|
$metadata->setCacheMaxAge(Cache::PERMANENT); |
272
|
|
|
$metadata->addCacheTags(['graphql_response']); |
273
|
|
|
$metadata->addCacheContexts(['gql']); |
274
|
|
|
|
275
|
|
|
$metadata->addCacheableDependency($this->collectCacheMetadata($schema, function (CacheableEdgeInterface $item, AbstractSchema $schema) { |
276
|
|
|
return $item->getResponseCacheMetadata($schema); |
277
|
|
|
})); |
278
|
|
|
|
279
|
|
|
return $metadata; |
280
|
|
|
} |
281
|
|
|
|
282
|
|
|
/** |
283
|
|
|
* Recursively collects cache metadata from the generated schema. |
284
|
|
|
* |
285
|
|
|
* @param \Youshido\GraphQL\Schema\AbstractSchema $schema |
286
|
|
|
* The schema. |
287
|
|
|
* @param callable $extract |
288
|
|
|
* Callback to extract cache metadata from a plugin within the schema. |
289
|
|
|
* |
290
|
|
|
* @return \Drupal\Core\Cache\CacheableMetadata |
291
|
|
|
* The collected cache metadata. |
292
|
|
|
*/ |
293
|
|
|
protected function collectCacheMetadata(AbstractSchema $schema, callable $extract) { |
294
|
|
|
$metadata = new CacheableMetadata(); |
295
|
|
|
|
296
|
|
|
foreach (TypeCollector::collectTypes($schema) as $type) { |
297
|
|
|
if ($type instanceof CacheableEdgeInterface) { |
298
|
|
|
$metadata->addCacheableDependency($extract($type, $schema)); |
299
|
|
|
} |
300
|
|
|
|
301
|
|
|
if ($type instanceof AbstractObjectType || $type instanceof AbstractInputObjectType || $type instanceof AbstractInterfaceType) { |
302
|
|
|
foreach ($type->getFields() as $field) { |
303
|
|
|
if ($field instanceof CacheableEdgeInterface) { |
304
|
|
|
$metadata->addCacheableDependency($extract($field, $schema)); |
305
|
|
|
} |
306
|
|
|
} |
307
|
|
|
} |
308
|
|
|
} |
309
|
|
|
|
310
|
|
|
return $metadata; |
311
|
|
|
} |
312
|
|
|
|
313
|
|
|
/** |
314
|
|
|
* Maps a max age value to an "expire" value for the Cache API. |
315
|
|
|
* |
316
|
|
|
* @param int $maxAge |
317
|
|
|
* A max age value. |
318
|
|
|
* |
319
|
|
|
* @return int |
320
|
|
|
* A corresponding "expire" value. |
321
|
|
|
* |
322
|
|
|
* @see \Drupal\Core\Cache\CacheBackendInterface::set() |
323
|
|
|
*/ |
324
|
|
|
protected function maxAgeToExpire($maxAge) { |
325
|
|
|
return ($maxAge === Cache::PERMANENT) ? Cache::PERMANENT : (int) $this->requestStack->getMasterRequest()->server->get('REQUEST_TIME') + $maxAge; |
326
|
|
|
} |
327
|
|
|
|
328
|
|
|
/** |
329
|
|
|
* Generates a cache identifier for the passed cache contexts. |
330
|
|
|
* |
331
|
|
|
* @param string $name |
332
|
|
|
* The name of the schema. |
333
|
|
|
* @param \Drupal\Core\Cache\CacheableDependencyInterface $metadata |
334
|
|
|
* Optional array of cache context tokens. |
335
|
|
|
* |
336
|
|
|
* @return string The generated cache identifier. |
337
|
|
|
* The generated cache identifier. |
338
|
|
|
*/ |
339
|
|
|
protected function getCacheIdentifier($name, CacheableDependencyInterface $metadata) { |
340
|
|
|
$tokens = $metadata->getCacheContexts(); |
341
|
|
|
$keys = $this->contextsManager->convertTokensToKeys($tokens)->getKeys(); |
342
|
|
|
return implode(':', array_merge(['graphql', $name], array_values($keys))); |
343
|
|
|
} |
344
|
|
|
|
345
|
|
|
} |
346
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.