Completed
Pull Request — 8.x-3.x (#642)
by
unknown
02:10
created

QueryProcessor   D

Complexity

Total Complexity 58

Size/Duplication

Total Lines 533
Duplicated Lines 3 %

Coupling/Cohesion

Components 1
Dependencies 14

Importance

Changes 0
Metric Value
dl 16
loc 533
rs 4.5599
c 0
b 0
f 0
wmc 58
lcom 1
cbo 14

19 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 15 1
B processQuery() 0 52 5
A executeSingle() 0 5 1
A executeBatch() 0 9 1
A executeOperationWithReporting() 0 16 4
C executeOperation() 0 53 14
B executeCacheableOperation() 0 22 7
A executeUncachableOperation() 0 8 1
A doExecuteOperation() 0 34 3
A validateOperationParams() 0 6 1
A validateOperation() 0 21 2
A resolveRootValue() 8 8 2
A resolveContextValue() 8 8 2
A resolveValidationRules() 0 12 3
A loadPersistedQuery() 0 12 4
A serializeDocument() 0 3 1
A sanitizeRecursive() 0 11 3
A cacheIdentifier() 0 17 2
A filterCacheContexts() 0 5 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like QueryProcessor often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use QueryProcessor, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Drupal\graphql\GraphQL\Execution;
4
5
use Drupal\Core\Cache\CacheableDependencyInterface;
6
use Drupal\Core\Cache\CacheableMetadata;
7
use Drupal\Core\Cache\CacheBackendInterface;
8
use Drupal\Core\Cache\Context\CacheContextsManager;
9
use Drupal\Core\Session\AccountProxyInterface;
10
use Drupal\graphql\Plugin\SchemaPluginManager;
11
use Drupal\graphql\GraphQL\QueryProvider\QueryProviderInterface;
12
use Drupal\graphql\GraphQL\Cache\CacheableRequestError;
13
use GraphQL\Error\Error;
14
use GraphQL\Error\FormattedError;
15
use GraphQL\Executor\ExecutionResult;
16
use GraphQL\Executor\Executor;
17
use GraphQL\Executor\Promise\Adapter\SyncPromiseAdapter;
18
use GraphQL\Executor\Promise\PromiseAdapter;
19
use GraphQL\Language\AST\DocumentNode;
20
use GraphQL\Language\Parser;
21
use GraphQL\Language\Visitor;
22
use GraphQL\Server\Helper;
23
use GraphQL\Server\OperationParams;
24
use GraphQL\Server\RequestError;
25
use GraphQL\Server\ServerConfig;
26
use GraphQL\Utils\AST;
27
use GraphQL\Utils\TypeInfo;
28
use GraphQL\Utils\Utils;
29
use GraphQL\Validator\DocumentValidator;
30
use GraphQL\Validator\Rules\AbstractValidationRule;
31
use GraphQL\Validator\ValidationContext;
32
33
// TODO: Refactor this and clean it up.
34
class QueryProcessor {
35
36
  /**
37
   * The current user account.
38
   *
39
   * @var \Drupal\Core\Session\AccountProxyInterface
40
   */
41
  protected $currentUser;
42
43
  /**
44
   * The schema plugin manager.
45
   *
46
   * @var \Drupal\graphql\Plugin\SchemaPluginManager
47
   */
48
  protected $pluginManager;
49
50
  /**
51
   * The query provider service.
52
   *
53
   * @var \Drupal\graphql\GraphQL\QueryProvider\QueryProviderInterface
54
   */
55
  protected $queryProvider;
56
57
  /**
58
   * The cache backend for caching query results.
59
   *
60
   * @var \Drupal\Core\Cache\CacheBackendInterface
61
   */
62
  protected $cacheBackend;
63
64
  /**
65
   * The cache contexts manager service.
66
   *
67
   * @var \Drupal\Core\Cache\Context\CacheContextsManager
68
   */
69
  protected $contextsManager;
70
71
  /**
72
   * The configuration service parameter.
73
   *
74
   * @var array
75
   */
76
  protected $config;
77
78
  /**
79
   * Processor constructor.
80
   *
81
   * @param \Drupal\Core\Session\AccountProxyInterface $currentUser
82
   *   The current user.
83
   * @param \Drupal\Core\Cache\Context\CacheContextsManager $contextsManager
84
   *   The cache contexts manager service.
85
   * @param \Drupal\graphql\Plugin\SchemaPluginManager $pluginManager
86
   *   The schema plugin manager.
87
   * @param \Drupal\graphql\GraphQL\QueryProvider\QueryProviderInterface $queryProvider
88
   *   The query provider service.
89
   * @param \Drupal\Core\Cache\CacheBackendInterface $cacheBackend
90
   *   The cache backend for caching query results.
91
   * @param array $config
92
   *   The configuration service parameter.
93
   */
94
  public function __construct(
95
    AccountProxyInterface $currentUser,
96
    CacheContextsManager $contextsManager,
97
    SchemaPluginManager $pluginManager,
98
    QueryProviderInterface $queryProvider,
99
    CacheBackendInterface $cacheBackend,
100
    array $config
101
  ) {
102
    $this->currentUser = $currentUser;
103
    $this->contextsManager = $contextsManager;
104
    $this->pluginManager = $pluginManager;
105
    $this->queryProvider = $queryProvider;
106
    $this->cacheBackend = $cacheBackend;
107
    $this->config = $config;
108
  }
109
110
  /**
111
   * Processes one or multiple graphql operations.
112
   *
113
   * @param string $schema
114
   *   The plugin id of the schema to use.
115
   * @param \GraphQL\Server\OperationParams|\GraphQL\Server\OperationParams[] $params
116
   *   The graphql operation(s) to execute.
117
   * @param array $globals
118
   *   The query context.
119
   *
120
   * @return \Drupal\graphql\GraphQL\Execution\QueryResult|\Drupal\graphql\GraphQL\Execution\QueryResult[]
121
   *   The query result.
122
   *
123
   */
124
  public function processQuery($schema, $params, array $globals = []) {
125
    // Load the plugin from the schema manager.
126
    $plugin = $this->pluginManager->createInstance($schema);
127
    $schema = $plugin->getSchema();
128
129
    // If the current user has appropriate permissions, allow to bypass
130
    // the secure fields restriction.
131
    $globals['bypass field security'] = $this->currentUser->hasPermission('bypass graphql field security');
132
133
    // Create the server config.
134
    $config = ServerConfig::create();
135
    $config->setDebug(!empty($this->config['development']));
136
    $config->setSchema($schema);
137
    $config->setQueryBatching(TRUE);
138
    $config->setContext(function () use ($globals, $plugin) {
139
      // Each document (e.g. in a batch query) gets its own resolve context but
140
      // the global parameters are shared. This allows us to collect the cache
141
      // metadata and contextual values (e.g. inheritance for language) for each
142
      // query separately.
143
      $context = new ResolveContext($globals);
144
      if ($plugin instanceof CacheableDependencyInterface) {
0 ignored issues
show
Bug introduced by
The class Drupal\Core\Cache\CacheableDependencyInterface does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
145
        $context->addCacheableDependency($plugin)->addCacheTags(['graphql_response']);
146
      }
147
148
      return $context;
149
    });
150
151
    $config->setValidationRules(function (OperationParams $params, DocumentNode $document, $operation) {
0 ignored issues
show
Unused Code introduced by
The parameter $document is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $operation is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
152
      if (isset($params->queryId)) {
153
        // Assume that pre-parsed documents are already validated. This allows
154
        // us to store pre-validated query documents e.g. for persisted queries
155
        // effectively improving performance by skipping run-time validation.
156
        return [];
157
      }
158
159
      return array_values(DocumentValidator::defaultRules());
160
    });
161
162
    $config->setPersistentQueryLoader(function ($id, OperationParams $params) {
163
      if ($query = $this->queryProvider->getQuery($id, $params)) {
164
        return $query;
165
      }
166
167
      throw new RequestError(sprintf("Failed to load query map for id '%s'.", $id));
168
    });
169
170
    if (is_array($params)) {
171
      return $this->executeBatch($config, $params);
172
    }
173
174
    return $this->executeSingle($config, $params);
175
  }
176
177
  /**
178
   * @param \GraphQL\Server\ServerConfig $config
179
   * @param \GraphQL\Server\OperationParams $params
180
   *
181
   * @return mixed
182
   */
183
  public function executeSingle(ServerConfig $config, OperationParams $params) {
184
    $adapter = new SyncPromiseAdapter();
185
    $result = $this->executeOperationWithReporting($adapter, $config, $params, FALSE);
186
    return $adapter->wait($result);
187
  }
188
189
  /**
190
   * @param \GraphQL\Server\ServerConfig $config
191
   * @param array $params
192
   *
193
   * @return mixed
194
   */
195
  public function executeBatch(ServerConfig $config, array $params) {
196
    $adapter = new SyncPromiseAdapter();
197
    $result = array_map(function ($params) use ($adapter, $config) {
198
      return $this->executeOperationWithReporting($adapter, $config, $params, TRUE);
199
    }, $params);
200
201
    $result = $adapter->all($result);
202
    return $adapter->wait($result);
203
  }
204
205
  /**
206
   * @param \GraphQL\Executor\Promise\PromiseAdapter $adapter
207
   * @param \GraphQL\Server\ServerConfig $config
208
   * @param \GraphQL\Server\OperationParams $params
209
   * @param bool $batching
210
   *
211
   * @return \GraphQL\Executor\Promise\Promise
212
   */
213
  protected function executeOperationWithReporting(PromiseAdapter $adapter, ServerConfig $config, OperationParams $params, $batching = FALSE) {
214
    $result = $this->executeOperation($adapter, $config, $params, $batching);
215
216
    // Format and print errors.
217
    return $result->then(function(QueryResult $result) use ($config) {
218
      if ($config->getErrorsHandler()) {
219
        $result->setErrorsHandler($config->getErrorsHandler());
220
      }
221
222
      if ($config->getErrorFormatter() || $config->getDebug()) {
223
        $result->setErrorFormatter(FormattedError::prepareFormatter($config->getErrorFormatter(), $config->getDebug()));
224
      }
225
226
      return $result;
227
    });
228
  }
229
230
  /**
231
   * @param \GraphQL\Executor\Promise\PromiseAdapter $adapter
232
   * @param \GraphQL\Server\ServerConfig $config
233
   * @param \GraphQL\Server\OperationParams $params
234
   * @param bool $batching
235
   *
236
   * @return \GraphQL\Executor\Promise\Promise
237
   */
238
  protected function executeOperation(PromiseAdapter $adapter, ServerConfig $config, OperationParams $params, $batching = FALSE) {
239
    try {
240
      if (!$config->getSchema()) {
241
        throw new \LogicException('Missing schema for query execution.');
242
      }
243
244
      if ($batching && !$config->getQueryBatching()) {
245
        throw new RequestError('Batched queries are not supported by this server.');
246
      }
247
248
      if ($errors = $this->validateOperationParams($params)) {
249
        return $adapter->createFulfilled(new QueryResult(NULL, $errors));
250
      }
251
252
      $document = $params->queryId ? $this->loadPersistedQuery($config, $params) : $params->query;
253
      if (!$document instanceof DocumentNode) {
254
        $document = Parser::parse($document);
255
      }
256
257
      // Read the operation type from the document. Subscriptions and mutations
258
      // only work through POST requests. One cannot have mutations and queries
259
      // in the same document, hence this check is sufficient.
260
      $operation = $params->operation;
261
      $type = AST::getOperation($document, $operation);
262
      if ($params->isReadOnly() && $type !== 'query') {
263
        throw new RequestError('GET requests are only supported for query operations.');
264
      }
265
266
      // If one of the validation rules found any problems, do not resolve the
267
      // query and bail out early instead.
268
      if ($errors = $this->validateOperation($config, $params, $document)) {
269
        return $adapter->createFulfilled(new QueryResult(NULL, $errors));
270
      }
271
272
      // Only queries can be cached (mutations and subscriptions can't).
273
      if ($type === 'query') {
274
        return $this->executeCacheableOperation($adapter, $config, $params, $document);
275
      }
276
277
      return $this->executeUncachableOperation($adapter, $config, $params, $document);
278
    }
279
    catch (CacheableRequestError $exception) {
280
      return $adapter->createFulfilled(
281
        new QueryResult(NULL, [Error::createLocatedError($exception)], [], $exception)
282
      );
283
    }
284
    catch (RequestError $exception) {
285
      return $adapter->createFulfilled(new QueryResult(NULL, [Error::createLocatedError($exception)]));
286
    }
287
    catch (Error $exception) {
288
      return $adapter->createFulfilled(new QueryResult(NULL, [$exception]));
289
    }
290
  }
291
292
  /**
293
   * @param \GraphQL\Executor\Promise\PromiseAdapter $adapter
294
   * @param \GraphQL\Server\ServerConfig $config
295
   * @param \GraphQL\Server\OperationParams $params
296
   * @param \GraphQL\Language\AST\DocumentNode $document
297
   *
298
   * @return \GraphQL\Executor\Promise\Promise|mixed
299
   */
300
  protected function executeCacheableOperation(PromiseAdapter $adapter, ServerConfig $config, OperationParams $params, DocumentNode $document) {
301
    $contextCacheId = 'ccid:' . $this->cacheIdentifier($params, $document, new CacheableMetadata());
302
303
    if (!$config->getDebug() && ($contextCache = $this->cacheBackend->get($contextCacheId)) && $contexts = $contextCache->data) {
304
      $cacheId = 'cid:' . $this->cacheIdentifier($params, $document, (new CacheableMetadata())->addCacheContexts($contexts));
305
      if (($cache = $this->cacheBackend->get($cacheId)) && $result = $cache->data) {
306
        return $adapter->createFulfilled($result);
307
      }
308
    }
309
310
    $result = $this->doExecuteOperation($adapter, $config, $params, $document);
311
312
    return $result->then(function (QueryResult $result) use ($contextCacheId, $params, $document) {
313
      // Write this query into the cache if it is cacheable.
314
      if ($result->getCacheMaxAge() !== 0) {
315
        $cacheId = 'cid:' . $this->cacheIdentifier($params, $document, (new CacheableMetadata())->addCacheContexts($result->getCacheContexts()));
316
        $this->cacheBackend->set($contextCacheId, $result->getCacheContexts(), $result->getCacheMaxAge(), $result->getCacheTags());
317
        $this->cacheBackend->set($cacheId, $result, $result->getCacheMaxAge(), $result->getCacheTags());
318
      }
319
      return $result;
320
    });
321
  }
322
323
  /**
324
   * @param \GraphQL\Executor\Promise\PromiseAdapter $adapter
325
   * @param \GraphQL\Server\ServerConfig $config
326
   * @param \GraphQL\Server\OperationParams $params
327
   * @param \GraphQL\Language\AST\DocumentNode $document
328
   *
329
   * @return \GraphQL\Executor\Promise\Promise
330
   */
331
  protected function executeUncachableOperation(PromiseAdapter $adapter, ServerConfig $config, OperationParams $params, DocumentNode $document) {
332
    $result = $this->doExecuteOperation($adapter, $config, $params, $document);
333
    return $result->then(function (QueryResult $result) {
334
      // Mark the query result as uncacheable.
335
      $result->mergeCacheMaxAge(0);
336
      return $result;
337
    });
338
  }
339
340
  /**
341
   * @param \GraphQL\Executor\Promise\PromiseAdapter $adapter
342
   * @param \GraphQL\Server\ServerConfig $config
343
   * @param \GraphQL\Server\OperationParams $params
344
   * @param \GraphQL\Language\AST\DocumentNode $document
345
   *
346
   * @return \GraphQL\Executor\Promise\Promise
347
   */
348
  protected function doExecuteOperation(PromiseAdapter $adapter, ServerConfig $config, OperationParams $params, DocumentNode $document) {
349
    $operation = $params->operation;
350
    $variables = $params->variables;
351
    $context = $this->resolveContextValue($config, $params, $document, $operation);
352
    $root = $this->resolveRootValue($config, $params, $document, $operation);
353
    $resolver = $config->getFieldResolver();
354
    $schema = $config->getSchema();
355
356
    $promise = Executor::promiseToExecute(
357
      $adapter,
358
      $schema,
359
      $document,
360
      $root,
361
      $context,
362
      $variables,
363
      $operation,
364
      $resolver
365
    );
366
367
    return $promise->then(function (ExecutionResult $result) use ($context) {
368
369
      $metadata = (new CacheableMetadata())
370
        ->addCacheContexts($this->filterCacheContexts($context->getCacheContexts()))
371
        ->addCacheTags($context->getCacheTags())
372
        ->setCacheMaxAge($context->getCacheMaxAge());
373
374
      // Do not cache in development mode or if there are any errors.
375
      if ($context->getGlobal('development') || !empty($result->errors)) {
376
        $metadata->setCacheMaxAge(0);
377
      }
378
379
      return new QueryResult($result->data, $result->errors, $result->extensions, $metadata);
380
    });
381
  }
382
383
  /**
384
   * @param \GraphQL\Server\OperationParams $params
385
   *
386
   * @return array
387
   */
388
  protected function validateOperationParams(OperationParams $params) {
389
    $errors = (new Helper())->validateOperationParams($params);
390
    return array_map(function (RequestError $error) {
391
      return Error::createLocatedError($error, NULL, NULL);
392
    }, $errors);
393
  }
394
395
  /**
396
   * @param \GraphQL\Server\ServerConfig $config
397
   * @param \GraphQL\Server\OperationParams $params
398
   * @param \GraphQL\Language\AST\DocumentNode $document
399
   *
400
   * @return \GraphQL\Error\Error[]
401
   */
402
  protected function validateOperation(ServerConfig $config, OperationParams $params, DocumentNode $document) {
403
    $operation = $params->operation;
404
    // Skip validation if there are no validation rules to be applied.
405
    if (!$rules = $this->resolveValidationRules($config, $params, $document, $operation)) {
406
      return [];
407
    }
408
409
    $schema = $config->getSchema();
410
    $info = new TypeInfo($schema);
411
    $validation = new ValidationContext($schema, $document, $info);
412
    $visitors = array_values(array_map(function (AbstractValidationRule $rule) use ($validation) {
413
      return $rule($validation);
414
    }, $rules));
415
416
    // Run the query visitor with the prepared validation rules and the cache
417
    // metadata collector and query complexity calculator.
418
    Visitor::visit($document, Visitor::visitWithTypeInfo($info, Visitor::visitInParallel($visitors)));
419
420
    // Return any possible errors collected during validation.
421
    return $validation->getErrors();
422
  }
423
424
  /**
425
   * @param \GraphQL\Server\ServerConfig $config
426
   * @param \GraphQL\Server\OperationParams $params
427
   * @param \GraphQL\Language\AST\DocumentNode $document
428
   * @param $operation
429
   *
430
   * @return mixed
431
   */
432 View Code Duplication
  protected function resolveRootValue(ServerConfig $config, OperationParams $params, DocumentNode $document, $operation) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
433
    $root = $config->getRootValue();
434
    if (is_callable($root)) {
435
      $root = $root($params, $document, $operation);
436
    }
437
438
    return $root;
439
  }
440
441
  /**
442
   * @param \GraphQL\Server\ServerConfig $config
443
   * @param \GraphQL\Server\OperationParams $params
444
   * @param \GraphQL\Language\AST\DocumentNode $document
445
   * @param $operation
446
   *
447
   * @return mixed
448
   */
449 View Code Duplication
  protected function resolveContextValue(ServerConfig $config, OperationParams $params, DocumentNode $document, $operation) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
450
    $context = $config->getContext();
451
    if (is_callable($context)) {
452
      $context = $context($params, $document, $operation);
453
    }
454
455
    return $context;
456
  }
457
458
  /**
459
   * @param \GraphQL\Server\ServerConfig $config
460
   * @param \GraphQL\Server\OperationParams $params
461
   * @param \GraphQL\Language\AST\DocumentNode $document
462
   * @param $operation
463
   *
464
   * @return array
465
   */
466
  protected function resolveValidationRules(ServerConfig $config, OperationParams $params, DocumentNode $document, $operation) {
467
    // Allow customizing validation rules per operation:
468
    $rules = $config->getValidationRules();
469
    if (is_callable($rules)) {
470
      $rules = $rules($params, $document, $operation);
471
      if (!is_array($rules)) {
472
        throw new \LogicException(sprintf("Expecting validation rules to be array or callable returning array, but got: %s", Utils::printSafe($rules)));
473
      }
474
    }
475
476
    return $rules;
477
  }
478
479
  /**
480
   * @param \GraphQL\Server\ServerConfig $config
481
   * @param \GraphQL\Server\OperationParams $params
482
   *
483
   * @return mixed
484
   * @throws \GraphQL\Server\RequestError
485
   */
486
  protected function loadPersistedQuery(ServerConfig $config, OperationParams $params) {
487
    if (!$loader = $config->getPersistentQueryLoader()) {
488
      throw new RequestError('Persisted queries are not supported by this server.');
489
    }
490
491
    $source = $loader($params->queryId, $params);
492
    if (!is_string($source) && !$source instanceof DocumentNode) {
493
      throw new \LogicException(sprintf('The persisted query loader must return query string or instance of %s but got: %s.', DocumentNode::class, Utils::printSafe($source)));
494
    }
495
496
    return $source;
497
  }
498
499
  /**
500
   * @param \GraphQL\Language\AST\DocumentNode $document
501
   *
502
   * @return array
503
   */
504
  protected function serializeDocument(DocumentNode $document) {
505
    return $this->sanitizeRecursive(AST::toArray($document));
506
  }
507
508
  /**
509
   * @param array $item
510
   *
511
   * @return array
512
   */
513
  protected function sanitizeRecursive(array $item) {
514
    unset($item['loc']);
515
516
    foreach ($item as &$value) {
517
      if (is_array($value)) {
518
        $value = $this->sanitizeRecursive($value);
519
      }
520
    }
521
522
    return $item;
523
  }
524
525
  /**
526
   * @param \GraphQL\Server\OperationParams $params
527
   * @param \GraphQL\Language\AST\DocumentNode $document
528
   * @param \Drupal\Core\Cache\CacheableMetadata $metadata
529
   *
530
   * @return string
531
   */
532
  protected function cacheIdentifier(OperationParams $params, DocumentNode $document, CacheableMetadata $metadata) {
533
    // Ignore language contexts since they are handled by graphql internally.
534
    $contexts = $this->filterCacheContexts($metadata->getCacheContexts());
535
    $keys = $this->contextsManager->convertTokensToKeys($contexts)->getKeys();
536
537
    // Sorting the variables will cause fewer cache vectors.
538
    $variables = $params->variables ?: [];
539
    ksort($variables);
540
541
    // Prepend the hash of the serialized document to the cache contexts.
542
    $hash = hash('sha256', json_encode([
543
      'query' => $this->serializeDocument($document),
544
      'variables' => $variables,
545
    ]));
546
547
    return implode(':', array_values(array_merge([$hash], $keys)));
548
  }
549
550
  /**
551
   * Filter unused contexts.
552
   *
553
   * Removes the language contexts from a list of context ids.
554
   *
555
   * @param string[] $contexts
556
   *   The list of context id's.
557
   *
558
   * @return string[]
559
   *   The filtered list of context id's.
560
   */
561
  protected function filterCacheContexts(array $contexts) {
562
    return array_filter($contexts, function ($context) {
563
      return strpos($context, 'languages:') !== 0;
564
    });
565
  }
566
}
567