Completed
Push — 8.x-3.x ( f20bec...270ca1 )
by Sebastian
01:29
created

QueryProcessor   D

Complexity

Total Complexity 59

Size/Duplication

Total Lines 497
Duplicated Lines 3.22 %

Coupling/Cohesion

Components 1
Dependencies 12

Importance

Changes 0
Metric Value
dl 16
loc 497
rs 4.08
c 0
b 0
f 0
wmc 59
lcom 1
cbo 12

20 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 11 1
A processQuery() 0 11 2
A executeSingle() 0 5 1
A executeBatch() 0 9 1
A executeOperationWithReporting() 0 16 4
C executeOperation() 0 48 13
B executeCacheableOperation() 0 25 6
A executeUncachableOperation() 0 8 1
B doExecuteOperation() 0 39 5
A validateOperationParams() 0 6 1
A validateOperation() 0 26 4
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
A maxAgeToExpire() 0 4 2

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\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) {
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...
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) {
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...
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;
0 ignored issues
show
Bug introduced by
Why assign $contexts to itself?

This checks looks for cases where a variable has been assigned to itself.

This assignement can be removed without consequences.

Loading history...
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