Completed
Pull Request — 8.x-3.x (#653)
by Sebastian
02:47
created

QueryProcessor   C

Complexity

Total Complexity 55

Size/Duplication

Total Lines 467
Duplicated Lines 3.43 %

Coupling/Cohesion

Components 1
Dependencies 11

Importance

Changes 0
Metric Value
dl 16
loc 467
rs 6
c 0
b 0
f 0
wmc 55
lcom 1
cbo 11

19 Methods

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