1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Drupal\graphql\GraphQL\Execution; |
4
|
|
|
|
5
|
|
|
use Drupal\Core\Cache\Cache; |
6
|
|
|
use Drupal\Core\Cache\CacheableMetadata; |
7
|
|
|
use Drupal\Core\Cache\CacheBackendInterface; |
8
|
|
|
use Drupal\Core\Cache\Context\CacheContextsManager; |
9
|
|
|
use Drupal\graphql\Plugin\SchemaPluginManager; |
10
|
|
|
use Drupal\graphql\GraphQL\Cache\CacheableRequestError; |
11
|
|
|
use GraphQL\Error\Error; |
12
|
|
|
use GraphQL\Error\FormattedError; |
13
|
|
|
use GraphQL\Executor\ExecutionResult; |
14
|
|
|
use GraphQL\Executor\Executor; |
15
|
|
|
use GraphQL\Executor\Promise\Adapter\SyncPromiseAdapter; |
16
|
|
|
use GraphQL\Executor\Promise\PromiseAdapter; |
17
|
|
|
use GraphQL\Language\AST\DocumentNode; |
18
|
|
|
use GraphQL\Language\Parser; |
19
|
|
|
use GraphQL\Language\Visitor; |
20
|
|
|
use GraphQL\Server\Helper; |
21
|
|
|
use GraphQL\Server\OperationParams; |
22
|
|
|
use GraphQL\Server\RequestError; |
23
|
|
|
use GraphQL\Server\ServerConfig; |
24
|
|
|
use GraphQL\Utils\AST; |
25
|
|
|
use GraphQL\Utils\TypeInfo; |
26
|
|
|
use GraphQL\Utils\Utils; |
27
|
|
|
use GraphQL\Validator\Rules\AbstractValidationRule; |
28
|
|
|
use GraphQL\Validator\ValidationContext; |
29
|
|
|
use GraphQL\Validator\Rules\QueryComplexity; |
30
|
|
|
use Symfony\Component\HttpFoundation\RequestStack; |
31
|
|
|
|
32
|
|
|
// TODO: Refactor this and clean it up. |
33
|
|
|
class QueryProcessor { |
34
|
|
|
|
35
|
|
|
/** |
36
|
|
|
* The schema plugin manager. |
37
|
|
|
* |
38
|
|
|
* @var \Drupal\graphql\Plugin\SchemaPluginManager |
39
|
|
|
*/ |
40
|
|
|
protected $pluginManager; |
41
|
|
|
|
42
|
|
|
/** |
43
|
|
|
* The cache backend for caching query results. |
44
|
|
|
* |
45
|
|
|
* @var \Drupal\Core\Cache\CacheBackendInterface |
46
|
|
|
*/ |
47
|
|
|
protected $cacheBackend; |
48
|
|
|
|
49
|
|
|
/** |
50
|
|
|
* The cache contexts manager service. |
51
|
|
|
* |
52
|
|
|
* @var \Drupal\Core\Cache\Context\CacheContextsManager |
53
|
|
|
*/ |
54
|
|
|
protected $contextsManager; |
55
|
|
|
|
56
|
|
|
/** |
57
|
|
|
* The request stack. |
58
|
|
|
* |
59
|
|
|
* @var \Symfony\Component\HttpFoundation\RequestStack |
60
|
|
|
*/ |
61
|
|
|
protected $requestStack; |
62
|
|
|
|
63
|
|
|
/** |
64
|
|
|
* Processor constructor. |
65
|
|
|
* |
66
|
|
|
* @param \Drupal\Core\Cache\Context\CacheContextsManager $contextsManager |
67
|
|
|
* The cache contexts manager service. |
68
|
|
|
* @param \Drupal\graphql\Plugin\SchemaPluginManager $pluginManager |
69
|
|
|
* The schema plugin manager. |
70
|
|
|
* @param \Drupal\Core\Cache\CacheBackendInterface $cacheBackend |
71
|
|
|
* The cache backend for caching query results. |
72
|
|
|
* @param \Symfony\Component\HttpFoundation\RequestStack $requestStack |
73
|
|
|
* The request stack. |
74
|
|
|
*/ |
75
|
|
|
public function __construct( |
76
|
|
|
CacheContextsManager $contextsManager, |
77
|
|
|
SchemaPluginManager $pluginManager, |
78
|
|
|
CacheBackendInterface $cacheBackend, |
79
|
|
|
RequestStack $requestStack |
80
|
|
|
) { |
81
|
|
|
$this->contextsManager = $contextsManager; |
82
|
|
|
$this->pluginManager = $pluginManager; |
83
|
|
|
$this->cacheBackend = $cacheBackend; |
84
|
|
|
$this->requestStack = $requestStack; |
85
|
|
|
} |
86
|
|
|
|
87
|
|
|
/** |
88
|
|
|
* Processes one or multiple graphql operations. |
89
|
|
|
* |
90
|
|
|
* @param string $schema |
91
|
|
|
* The plugin id of the schema to use. |
92
|
|
|
* @param \GraphQL\Server\OperationParams|\GraphQL\Server\OperationParams[] $params |
93
|
|
|
* The graphql operation(s) to execute. |
94
|
|
|
* |
95
|
|
|
* @return \Drupal\graphql\GraphQL\Execution\QueryResult|\Drupal\graphql\GraphQL\Execution\QueryResult[] |
96
|
|
|
* The query result. |
97
|
|
|
* |
98
|
|
|
* @throws \Drupal\Component\Plugin\Exception\PluginException |
99
|
|
|
*/ |
100
|
|
|
public function processQuery($schema, $params) { |
101
|
|
|
// Load the plugin from the schema manager. |
102
|
|
|
$plugin = $this->pluginManager->createInstance($schema); |
103
|
|
|
$config = $plugin->getServer(); |
104
|
|
|
|
105
|
|
|
if (is_array($params)) { |
106
|
|
|
return $this->executeBatch($config, $params); |
107
|
|
|
} |
108
|
|
|
|
109
|
|
|
return $this->executeSingle($config, $params); |
110
|
|
|
} |
111
|
|
|
|
112
|
|
|
/** |
113
|
|
|
* @param \GraphQL\Server\ServerConfig $config |
114
|
|
|
* @param \GraphQL\Server\OperationParams $params |
115
|
|
|
* |
116
|
|
|
* @return mixed |
117
|
|
|
*/ |
118
|
|
|
public function executeSingle(ServerConfig $config, OperationParams $params) { |
119
|
|
|
$adapter = new SyncPromiseAdapter(); |
120
|
|
|
$result = $this->executeOperationWithReporting($adapter, $config, $params, FALSE); |
121
|
|
|
return $adapter->wait($result); |
122
|
|
|
} |
123
|
|
|
|
124
|
|
|
/** |
125
|
|
|
* @param \GraphQL\Server\ServerConfig $config |
126
|
|
|
* @param array $params |
127
|
|
|
* |
128
|
|
|
* @return mixed |
129
|
|
|
*/ |
130
|
|
|
public function executeBatch(ServerConfig $config, array $params) { |
131
|
|
|
$adapter = new SyncPromiseAdapter(); |
132
|
|
|
$result = array_map(function ($params) use ($adapter, $config) { |
133
|
|
|
return $this->executeOperationWithReporting($adapter, $config, $params, TRUE); |
134
|
|
|
}, $params); |
135
|
|
|
|
136
|
|
|
$result = $adapter->all($result); |
137
|
|
|
return $adapter->wait($result); |
138
|
|
|
} |
139
|
|
|
|
140
|
|
|
/** |
141
|
|
|
* @param \GraphQL\Executor\Promise\PromiseAdapter $adapter |
142
|
|
|
* @param \GraphQL\Server\ServerConfig $config |
143
|
|
|
* @param \GraphQL\Server\OperationParams $params |
144
|
|
|
* @param bool $batching |
145
|
|
|
* |
146
|
|
|
* @return \GraphQL\Executor\Promise\Promise |
147
|
|
|
*/ |
148
|
|
|
protected function executeOperationWithReporting(PromiseAdapter $adapter, ServerConfig $config, OperationParams $params, $batching = FALSE) { |
149
|
|
|
$result = $this->executeOperation($adapter, $config, $params, $batching); |
150
|
|
|
|
151
|
|
|
// Format and print errors. |
152
|
|
|
return $result->then(function(QueryResult $result) use ($config) { |
153
|
|
|
if ($config->getErrorsHandler()) { |
154
|
|
|
$result->setErrorsHandler($config->getErrorsHandler()); |
155
|
|
|
} |
156
|
|
|
|
157
|
|
|
if ($config->getErrorFormatter() || $config->getDebug()) { |
158
|
|
|
$result->setErrorFormatter(FormattedError::prepareFormatter($config->getErrorFormatter(), $config->getDebug())); |
159
|
|
|
} |
160
|
|
|
|
161
|
|
|
return $result; |
162
|
|
|
}); |
163
|
|
|
} |
164
|
|
|
|
165
|
|
|
/** |
166
|
|
|
* @param \GraphQL\Executor\Promise\PromiseAdapter $adapter |
167
|
|
|
* @param \GraphQL\Server\ServerConfig $config |
168
|
|
|
* @param \GraphQL\Server\OperationParams $params |
169
|
|
|
* @param bool $batching |
170
|
|
|
* |
171
|
|
|
* @return \GraphQL\Executor\Promise\Promise |
172
|
|
|
*/ |
173
|
|
|
protected function executeOperation(PromiseAdapter $adapter, ServerConfig $config, OperationParams $params, $batching = FALSE) { |
174
|
|
|
try { |
175
|
|
|
if (!$config->getSchema()) { |
176
|
|
|
throw new Error('Missing schema for query execution.'); |
177
|
|
|
} |
178
|
|
|
|
179
|
|
|
if ($batching && !$config->getQueryBatching()) { |
180
|
|
|
throw new RequestError('Batched queries are not supported by this server.'); |
181
|
|
|
} |
182
|
|
|
|
183
|
|
|
if ($errors = $this->validateOperationParams($params)) { |
184
|
|
|
return $adapter->createFulfilled(new QueryResult(NULL, $errors)); |
185
|
|
|
} |
186
|
|
|
|
187
|
|
|
$persisted = isset($params->queryId); |
188
|
|
|
$document = $persisted ? $this->loadPersistedQuery($config, $params) : $params->query; |
189
|
|
|
if (!$document instanceof DocumentNode) { |
190
|
|
|
$document = Parser::parse($document); |
191
|
|
|
} |
192
|
|
|
|
193
|
|
|
// Read the operation type from the document. Subscriptions and mutations |
194
|
|
|
// only work through POST requests. One cannot have mutations and queries |
195
|
|
|
// in the same document, hence this check is sufficient. |
196
|
|
|
$operation = $params->operation; |
197
|
|
|
$type = AST::getOperation($document, $operation); |
198
|
|
|
if ($params->isReadOnly() && $type !== 'query') { |
199
|
|
|
throw new RequestError('GET requests are only supported for query operations.'); |
200
|
|
|
} |
201
|
|
|
|
202
|
|
|
// Only queries can be cached (mutations and subscriptions can't). |
203
|
|
|
if ($type === 'query') { |
204
|
|
|
return $this->executeCacheableOperation($adapter, $config, $params, $document, !$persisted); |
205
|
|
|
} |
206
|
|
|
|
207
|
|
|
return $this->executeUncachableOperation($adapter, $config, $params, $document, !$persisted); |
208
|
|
|
} |
209
|
|
|
catch (CacheableRequestError $exception) { |
210
|
|
|
return $adapter->createFulfilled( |
211
|
|
|
new QueryResult(NULL, [Error::createLocatedError($exception)], [], $exception) |
212
|
|
|
); |
213
|
|
|
} |
214
|
|
|
catch (RequestError $exception) { |
215
|
|
|
return $adapter->createFulfilled(new QueryResult(NULL, [Error::createLocatedError($exception)])); |
216
|
|
|
} |
217
|
|
|
catch (Error $exception) { |
218
|
|
|
return $adapter->createFulfilled(new QueryResult(NULL, [$exception])); |
219
|
|
|
} |
220
|
|
|
} |
221
|
|
|
|
222
|
|
|
/** |
223
|
|
|
* @param \GraphQL\Executor\Promise\PromiseAdapter $adapter |
224
|
|
|
* @param \GraphQL\Server\ServerConfig $config |
225
|
|
|
* @param \GraphQL\Server\OperationParams $params |
226
|
|
|
* @param \GraphQL\Language\AST\DocumentNode $document |
227
|
|
|
* @param bool $validate |
228
|
|
|
* |
229
|
|
|
* @return \GraphQL\Executor\Promise\Promise|mixed |
230
|
|
|
*/ |
231
|
|
|
protected function executeCacheableOperation(PromiseAdapter $adapter, ServerConfig $config, OperationParams $params, DocumentNode $document, $validate = TRUE) { |
232
|
|
|
$contextCacheId = 'ccid:' . $this->cacheIdentifier($params, $document); |
233
|
|
|
if (!$config->getDebug() && $contextCache = $this->cacheBackend->get($contextCacheId)) { |
234
|
|
|
$contexts = $contextCache->data ?: []; |
235
|
|
|
$cid = 'cid:' . $this->cacheIdentifier($params, $document, $contexts); |
236
|
|
|
if ($cache = $this->cacheBackend->get($cid)) { |
237
|
|
|
return $adapter->createFulfilled($cache->data); |
238
|
|
|
} |
239
|
|
|
} |
240
|
|
|
|
241
|
|
|
$result = $this->doExecuteOperation($adapter, $config, $params, $document, $validate); |
242
|
|
|
return $result->then(function (QueryResult $result) use ($contextCacheId, $params, $document) { |
243
|
|
|
// Write this query into the cache if it is cacheable. |
244
|
|
|
if ($result->getCacheMaxAge() !== 0) { |
245
|
|
|
$contexts = $result->getCacheContexts(); |
246
|
|
|
$expire = $this->maxAgeToExpire($result->getCacheMaxAge()); |
247
|
|
|
$tags = $result->getCacheTags(); |
248
|
|
|
$cid = 'cid:' . $this->cacheIdentifier($params, $document, $contexts); |
249
|
|
|
$this->cacheBackend->set($contextCacheId, $contexts, $expire, $tags); |
250
|
|
|
$this->cacheBackend->set($cid, $result, $expire, $tags); |
251
|
|
|
} |
252
|
|
|
|
253
|
|
|
return $result; |
254
|
|
|
}); |
255
|
|
|
} |
256
|
|
|
|
257
|
|
|
/** |
258
|
|
|
* @param \GraphQL\Executor\Promise\PromiseAdapter $adapter |
259
|
|
|
* @param \GraphQL\Server\ServerConfig $config |
260
|
|
|
* @param \GraphQL\Server\OperationParams $params |
261
|
|
|
* @param \GraphQL\Language\AST\DocumentNode $document |
262
|
|
|
* @param bool $validate |
263
|
|
|
* |
264
|
|
|
* @return \GraphQL\Executor\Promise\Promise |
265
|
|
|
*/ |
266
|
|
|
protected function executeUncachableOperation(PromiseAdapter $adapter, ServerConfig $config, OperationParams $params, DocumentNode $document, $validate = TRUE) { |
267
|
|
|
$result = $this->doExecuteOperation($adapter, $config, $params, $document, $validate); |
268
|
|
|
return $result->then(function (QueryResult $result) { |
269
|
|
|
// Mark the query result as uncacheable. |
270
|
|
|
$result->mergeCacheMaxAge(0); |
271
|
|
|
return $result; |
272
|
|
|
}); |
273
|
|
|
} |
274
|
|
|
|
275
|
|
|
/** |
276
|
|
|
* @param \GraphQL\Executor\Promise\PromiseAdapter $adapter |
277
|
|
|
* @param \GraphQL\Server\ServerConfig $config |
278
|
|
|
* @param \GraphQL\Server\OperationParams $params |
279
|
|
|
* @param \GraphQL\Language\AST\DocumentNode $document |
280
|
|
|
* @param bool $validate |
281
|
|
|
* |
282
|
|
|
* @return \GraphQL\Executor\Promise\Promise |
283
|
|
|
*/ |
284
|
|
|
protected function doExecuteOperation(PromiseAdapter $adapter, ServerConfig $config, OperationParams $params, DocumentNode $document, $validate = TRUE) { |
285
|
|
|
// If one of the validation rules found any problems, do not resolve the |
286
|
|
|
// query and bail out early instead. |
287
|
|
|
if ($validate && $errors = $this->validateOperation($config, $params, $document)) { |
288
|
|
|
return $adapter->createFulfilled(new QueryResult(NULL, $errors)); |
289
|
|
|
} |
290
|
|
|
|
291
|
|
|
$operation = $params->operation; |
292
|
|
|
$variables = $params->variables; |
293
|
|
|
$context = $this->resolveContextValue($config, $params, $document, $operation); |
294
|
|
|
$root = $this->resolveRootValue($config, $params, $document, $operation); |
295
|
|
|
$resolver = $config->getFieldResolver(); |
296
|
|
|
$schema = $config->getSchema(); |
297
|
|
|
|
298
|
|
|
$promise = Executor::promiseToExecute( |
299
|
|
|
$adapter, |
300
|
|
|
$schema, |
301
|
|
|
$document, |
302
|
|
|
$root, |
303
|
|
|
$context, |
304
|
|
|
$variables, |
305
|
|
|
$operation, |
306
|
|
|
$resolver |
307
|
|
|
); |
308
|
|
|
|
309
|
|
|
return $promise->then(function (ExecutionResult $result) use ($context) { |
310
|
|
|
$metadata = (new CacheableMetadata()) |
311
|
|
|
->addCacheContexts($context->getCacheContexts()) |
312
|
|
|
->addCacheTags($context->getCacheTags()) |
313
|
|
|
->setCacheMaxAge($context->getCacheMaxAge()); |
314
|
|
|
|
315
|
|
|
// Do not cache in development mode or if there are any errors. |
316
|
|
|
if ($context->getGlobal('development') || !empty($result->errors)) { |
317
|
|
|
$metadata->setCacheMaxAge(0); |
318
|
|
|
} |
319
|
|
|
|
320
|
|
|
return new QueryResult($result->data, $result->errors, $result->extensions, $metadata); |
321
|
|
|
}); |
322
|
|
|
} |
323
|
|
|
|
324
|
|
|
/** |
325
|
|
|
* @param \GraphQL\Server\OperationParams $params |
326
|
|
|
* |
327
|
|
|
* @return array |
328
|
|
|
*/ |
329
|
|
|
protected function validateOperationParams(OperationParams $params) { |
330
|
|
|
$errors = (new Helper())->validateOperationParams($params); |
331
|
|
|
return array_map(function (RequestError $error) { |
332
|
|
|
return Error::createLocatedError($error, NULL, NULL); |
333
|
|
|
}, $errors); |
334
|
|
|
} |
335
|
|
|
|
336
|
|
|
/** |
337
|
|
|
* @param \GraphQL\Server\ServerConfig $config |
338
|
|
|
* @param \GraphQL\Server\OperationParams $params |
339
|
|
|
* @param \GraphQL\Language\AST\DocumentNode $document |
340
|
|
|
* |
341
|
|
|
* @return \GraphQL\Error\Error[] |
342
|
|
|
* @throws \Exception |
343
|
|
|
*/ |
344
|
|
|
protected function validateOperation(ServerConfig $config, OperationParams $params, DocumentNode $document) { |
345
|
|
|
$operation = $params->operation; |
346
|
|
|
// Skip validation if there are no validation rules to be applied. |
347
|
|
|
if (!$rules = $this->resolveValidationRules($config, $params, $document, $operation)) { |
348
|
|
|
return []; |
349
|
|
|
} |
350
|
|
|
|
351
|
|
|
$schema = $config->getSchema(); |
352
|
|
|
$info = new TypeInfo($schema); |
353
|
|
|
$validation = new ValidationContext($schema, $document, $info); |
354
|
|
|
$visitors = array_values(array_map(function (AbstractValidationRule $rule) use ($validation, $params) { |
355
|
|
|
// Set current variable values for QueryComplexity validation rule case |
356
|
|
|
// @see \GraphQL\GraphQL::promiseToExecute for equivalent |
357
|
|
|
if ($rule instanceof QueryComplexity && !empty($params->variables)) { |
358
|
|
|
$rule->setRawVariableValues($params->variables); |
359
|
|
|
} |
360
|
|
|
return $rule($validation); |
361
|
|
|
}, $rules)); |
362
|
|
|
|
363
|
|
|
// Run the query visitor with the prepared validation rules and the cache |
364
|
|
|
// metadata collector and query complexity calculator. |
365
|
|
|
Visitor::visit($document, Visitor::visitWithTypeInfo($info, Visitor::visitInParallel($visitors))); |
366
|
|
|
|
367
|
|
|
// Return any possible errors collected during validation. |
368
|
|
|
return $validation->getErrors(); |
369
|
|
|
} |
370
|
|
|
|
371
|
|
|
/** |
372
|
|
|
* @param \GraphQL\Server\ServerConfig $config |
373
|
|
|
* @param \GraphQL\Server\OperationParams $params |
374
|
|
|
* @param \GraphQL\Language\AST\DocumentNode $document |
375
|
|
|
* @param $operation |
376
|
|
|
* |
377
|
|
|
* @return mixed |
378
|
|
|
*/ |
379
|
|
View Code Duplication |
protected function resolveRootValue(ServerConfig $config, OperationParams $params, DocumentNode $document, $operation) { |
|
|
|
|
380
|
|
|
$root = $config->getRootValue(); |
381
|
|
|
if (is_callable($root)) { |
382
|
|
|
$root = $root($params, $document, $operation); |
383
|
|
|
} |
384
|
|
|
|
385
|
|
|
return $root; |
386
|
|
|
} |
387
|
|
|
|
388
|
|
|
/** |
389
|
|
|
* @param \GraphQL\Server\ServerConfig $config |
390
|
|
|
* @param \GraphQL\Server\OperationParams $params |
391
|
|
|
* @param \GraphQL\Language\AST\DocumentNode $document |
392
|
|
|
* @param $operation |
393
|
|
|
* |
394
|
|
|
* @return mixed |
395
|
|
|
*/ |
396
|
|
View Code Duplication |
protected function resolveContextValue(ServerConfig $config, OperationParams $params, DocumentNode $document, $operation) { |
|
|
|
|
397
|
|
|
$context = $config->getContext(); |
398
|
|
|
if (is_callable($context)) { |
399
|
|
|
$context = $context($params, $document, $operation); |
400
|
|
|
} |
401
|
|
|
|
402
|
|
|
return $context; |
403
|
|
|
} |
404
|
|
|
|
405
|
|
|
/** |
406
|
|
|
* @param \GraphQL\Server\ServerConfig $config |
407
|
|
|
* @param \GraphQL\Server\OperationParams $params |
408
|
|
|
* @param \GraphQL\Language\AST\DocumentNode $document |
409
|
|
|
* @param $operation |
410
|
|
|
* |
411
|
|
|
* @return array |
412
|
|
|
* @throws \GraphQL\Server\RequestError |
413
|
|
|
*/ |
414
|
|
|
protected function resolveValidationRules(ServerConfig $config, OperationParams $params, DocumentNode $document, $operation) { |
415
|
|
|
// Allow customizing validation rules per operation: |
416
|
|
|
$rules = $config->getValidationRules(); |
417
|
|
|
if (is_callable($rules)) { |
418
|
|
|
$rules = $rules($params, $document, $operation); |
419
|
|
|
if (!is_array($rules)) { |
420
|
|
|
throw new RequestError(sprintf("Expecting validation rules to be array or callable returning array, but got: %s", Utils::printSafe($rules))); |
421
|
|
|
} |
422
|
|
|
} |
423
|
|
|
|
424
|
|
|
return $rules; |
425
|
|
|
} |
426
|
|
|
|
427
|
|
|
/** |
428
|
|
|
* @param \GraphQL\Server\ServerConfig $config |
429
|
|
|
* @param \GraphQL\Server\OperationParams $params |
430
|
|
|
* |
431
|
|
|
* @return mixed |
432
|
|
|
* @throws \GraphQL\Server\RequestError |
433
|
|
|
*/ |
434
|
|
|
protected function loadPersistedQuery(ServerConfig $config, OperationParams $params) { |
435
|
|
|
if (!$loader = $config->getPersistentQueryLoader()) { |
436
|
|
|
throw new RequestError('Persisted queries are not supported by this server.'); |
437
|
|
|
} |
438
|
|
|
|
439
|
|
|
$source = $loader($params->queryId, $params); |
440
|
|
|
if (!is_string($source) && !$source instanceof DocumentNode) { |
441
|
|
|
throw new RequestError(sprintf('The persisted query loader must return query string or instance of %s but got: %s.', DocumentNode::class, Utils::printSafe($source))); |
442
|
|
|
} |
443
|
|
|
|
444
|
|
|
return $source; |
445
|
|
|
} |
446
|
|
|
|
447
|
|
|
/** |
448
|
|
|
* @param \GraphQL\Language\AST\DocumentNode $document |
449
|
|
|
* |
450
|
|
|
* @return array |
451
|
|
|
*/ |
452
|
|
|
protected function serializeDocument(DocumentNode $document) { |
453
|
|
|
return $this->sanitizeRecursive(AST::toArray($document)); |
454
|
|
|
} |
455
|
|
|
|
456
|
|
|
/** |
457
|
|
|
* @param array $item |
458
|
|
|
* |
459
|
|
|
* @return array |
460
|
|
|
*/ |
461
|
|
|
protected function sanitizeRecursive(array $item) { |
462
|
|
|
unset($item['loc']); |
463
|
|
|
|
464
|
|
|
foreach ($item as &$value) { |
465
|
|
|
if (is_array($value)) { |
466
|
|
|
$value = $this->sanitizeRecursive($value); |
467
|
|
|
} |
468
|
|
|
} |
469
|
|
|
|
470
|
|
|
return $item; |
471
|
|
|
} |
472
|
|
|
|
473
|
|
|
/** |
474
|
|
|
* @param \GraphQL\Server\OperationParams $params |
475
|
|
|
* @param \GraphQL\Language\AST\DocumentNode $document |
476
|
|
|
* @param array $contexts |
477
|
|
|
* |
478
|
|
|
* @return string |
479
|
|
|
*/ |
480
|
|
|
protected function cacheIdentifier(OperationParams $params, DocumentNode $document, array $contexts = []) { |
481
|
|
|
// Ignore language contexts since they are handled by graphql internally. |
482
|
|
|
$contexts = $contexts; |
|
|
|
|
483
|
|
|
$keys = $this->contextsManager->convertTokensToKeys($contexts)->getKeys(); |
484
|
|
|
|
485
|
|
|
// Sorting the variables will cause fewer cache vectors. |
486
|
|
|
$variables = $params->variables ?: []; |
487
|
|
|
ksort($variables); |
488
|
|
|
|
489
|
|
|
// Prepend the hash of the serialized document to the cache contexts. |
490
|
|
|
$hash = hash('sha256', json_encode([ |
491
|
|
|
'query' => $this->serializeDocument($document), |
492
|
|
|
'variables' => $variables, |
493
|
|
|
])); |
494
|
|
|
|
495
|
|
|
return implode(':', array_values(array_merge([$hash], $keys))); |
496
|
|
|
} |
497
|
|
|
|
498
|
|
|
/** |
499
|
|
|
* Filter unused contexts. |
500
|
|
|
* |
501
|
|
|
* Removes the language contexts from a list of context ids. |
502
|
|
|
* |
503
|
|
|
* @param string[] $contexts |
504
|
|
|
* The list of context id's. |
505
|
|
|
* |
506
|
|
|
* @return string[] |
507
|
|
|
* The filtered list of context id's. |
508
|
|
|
*/ |
509
|
|
|
protected function filterCacheContexts(array $contexts) { |
510
|
|
|
return array_filter($contexts, function ($context) { |
511
|
|
|
return strpos($context, 'languages:') !== 0; |
512
|
|
|
}); |
513
|
|
|
} |
514
|
|
|
|
515
|
|
|
/** |
516
|
|
|
* Maps a cache max age value to an "expire" value for the Cache API. |
517
|
|
|
* |
518
|
|
|
* @param int $maxAge |
519
|
|
|
* |
520
|
|
|
* @return int |
521
|
|
|
* A corresponding "expire" value. |
522
|
|
|
* |
523
|
|
|
* @see \Drupal\Core\Cache\CacheBackendInterface::set() |
524
|
|
|
*/ |
525
|
|
|
protected function maxAgeToExpire($maxAge) { |
526
|
|
|
$time = $this->requestStack->getMasterRequest()->server->get('REQUEST_TIME'); |
527
|
|
|
return ($maxAge === Cache::PERMANENT) ? Cache::PERMANENT : (int) $time + $maxAge; |
528
|
|
|
} |
529
|
|
|
} |
530
|
|
|
|
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.