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

QueryProcessor   B

Complexity

Total Complexity 50

Size/Duplication

Total Lines 485
Duplicated Lines 3.3 %

Coupling/Cohesion

Components 1
Dependencies 15

Importance

Changes 0
Metric Value
dl 16
loc 485
rs 7.2559
c 0
b 0
f 0
wmc 50
lcom 1
cbo 15

18 Methods

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