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

QueryProcessor::executeOperation()   C

Complexity

Conditions 16
Paths 86

Size

Total Lines 61
Code Lines 30

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 16
eloc 30
nc 86
nop 4
dl 0
loc 61
rs 6.2087
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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->executeOperation($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->executeOperation($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 executeOperation(PromiseAdapter $adapter, ServerConfig $config, OperationParams $params, $batching = FALSE) {
196
    try {
197
      if (!$config->getSchema()) {
198
        throw new \LogicException('Missing schema for query execution.');
199
      }
200
201
      if ($batching && !$config->getQueryBatching()) {
202
        throw new RequestError('Batched queries are not supported by this server.');
203
      }
204
205
      if ($errors = $this->validateOperationParams($params)) {
206
        return $adapter->createFulfilled(new QueryResult(NULL, $errors));
207
      }
208
209
      $document = $params->queryId ? $this->loadPersistedQuery($config, $params) : $params->query;
210
      if (!$document instanceof DocumentNode) {
211
        $document = Parser::parse($document);
212
      }
213
214
      // Read the operation type from the document. Subscriptions and mutations
215
      // only work through POST requests. One cannot have mutations and queries
216
      // in the same document, hence this check is sufficient.
217
      $operation = $params->operation;
218
      $type = AST::getOperation($document, $operation);
219
      if ($params->isReadOnly() && $type !== 'query') {
220
        throw new RequestError('GET requests are only supported for query operations.');
221
      }
222
223
      // If one of the validation rules found any problems, do not resolve the
224
      // query and bail out early instead.
225
      if ($errors = $this->validateOperation($config, $params, $document)) {
226
        return $adapter->createFulfilled(new QueryResult(NULL, $errors));
227
      }
228
229
      // Only queries can be cached (mutations and subscriptions can't).
230
      if ($type === 'query') {
231
        return $this->executeCacheableOperation($adapter, $config, $params, $document);
232
      }
233
234
      return $this->executeUncachableOperation($adapter, $config, $params, $document);
235
    }
236
    catch (RequestError $exception) {
237
      $result = $adapter->createFulfilled(new QueryResult(NULL, [Error::createLocatedError($exception)]));
238
    }
239
    catch (Error $exception) {
240
      $result = $adapter->createFulfilled(new QueryResult(NULL, [$exception]));
241
    }
242
243
    // Format and print errors.
244
    return $result->then(function(QueryResult $result) use ($config) {
245
      if ($config->getErrorsHandler()) {
246
        $result->setErrorsHandler($config->getErrorsHandler());
247
      }
248
249
      if ($config->getErrorFormatter() || $config->getDebug()) {
250
        $result->setErrorFormatter(FormattedError::prepareFormatter($config->getErrorFormatter(), $config->getDebug()));
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
   *
263
   * @return \GraphQL\Executor\Promise\Promise|mixed
264
   */
265
  protected function executeCacheableOperation(PromiseAdapter $adapter, ServerConfig $config, OperationParams $params, DocumentNode $document) {
266
    $schema = $config->getSchema();
267
268
    // Collect cache contexts from the query document.
269
    $contexts = [];
270
    $info = new TypeInfo($schema);
271
    $visitor = (new CacheContextsCollector())->getVisitor($info, $contexts);
272
    Visitor::visit($document, Visitor::visitWithTypeInfo($info, $visitor));
273
274
    // Generate a cache identifier from the collected contexts.
275
    $metadata = (new CacheableMetadata())->addCacheContexts($contexts);
276
    $cid = $this->cacheIdentifier($document, $metadata);
277
    if (($cache = $this->cacheBackend->get($cid)) && $result = $cache->data) {
278
      return $adapter->createFulfilled($result);
279
    }
280
281
    $result = $this->doExecuteOperation($adapter, $config, $params, $document);
282
    return $result->then(function (QueryResult $result) use ($cid, $metadata) {
283
      if (array_diff($result->getCacheContexts(), $metadata->getCacheContexts())) {
284
        throw new \LogicException('The query result yielded cache contexts that were not part of the static query analysis.');
285
      }
286
287
      // Add the statically collected cache contexts to the result.
288
      $result->addCacheableDependency($metadata);
289
      // Write this query into the cache if it is cacheable.
290
      if ($result->getCacheMaxAge() !== 0) {
291
        $this->cacheBackend->set($cid, $result, $result->getCacheMaxAge(), $result->getCacheTags());
292
      }
293
294
      return $result;
295
    });
296
  }
297
298
  /**
299
   * @param \GraphQL\Executor\Promise\PromiseAdapter $adapter
300
   * @param \GraphQL\Server\ServerConfig $config
301
   * @param \GraphQL\Server\OperationParams $params
302
   * @param \GraphQL\Language\AST\DocumentNode $document
303
   *
304
   * @return \GraphQL\Executor\Promise\Promise
305
   */
306
  protected function executeUncachableOperation(PromiseAdapter $adapter, ServerConfig $config, OperationParams $params, DocumentNode $document) {
307
    return $this->doExecuteOperation($adapter, $config, $params, $document);
308
  }
309
310
  /**
311
   * @param \GraphQL\Executor\Promise\PromiseAdapter $adapter
312
   * @param \GraphQL\Server\ServerConfig $config
313
   * @param \GraphQL\Server\OperationParams $params
314
   * @param \GraphQL\Language\AST\DocumentNode $document
315
   *
316
   * @return \GraphQL\Executor\Promise\Promise
317
   */
318
  protected function doExecuteOperation(PromiseAdapter $adapter, ServerConfig $config, OperationParams $params, DocumentNode $document) {
319
    $operation = $params->operation;
320
    $variables = $params->variables;
321
    $context = $this->resolveContextValue($config, $params, $document, $operation);
322
    $root = $this->resolveRootValue($config, $params, $document, $operation);
323
    $resolver = $config->getFieldResolver();
324
    $schema = $config->getSchema();
325
326
    $promise = Executor::promiseToExecute(
327
      $adapter,
328
      $schema,
329
      $document,
330
      $root,
331
      $context,
332
      $variables,
333
      $operation,
334
      $resolver
335
    );
336
337
    return $promise->then(function (ExecutionResult $result) use ($context) {
338
      // Add the collected cache metadata to the result.
339
      $metadata = (new CacheableMetadata())->addCacheableDependency($context);
340
      $output = new QueryResult($result->data, $result->errors, $result->extensions, $metadata);
341
342
      return $output;
343
    });
344
  }
345
346
  /**
347
   * @param \GraphQL\Server\OperationParams $params
348
   *
349
   * @return array
350
   */
351
  protected function validateOperationParams(OperationParams $params) {
352
    $errors = (new Helper())->validateOperationParams($params);
353
    return array_map(function (RequestError $error) {
354
      return Error::createLocatedError($error, NULL, NULL);
355
    }, $errors);
356
  }
357
358
  /**
359
   * @param \GraphQL\Server\ServerConfig $config
360
   * @param \GraphQL\Server\OperationParams $params
361
   * @param \GraphQL\Language\AST\DocumentNode $document
362
   *
363
   * @return \GraphQL\Error\Error[]
364
   */
365
  protected function validateOperation(ServerConfig $config, OperationParams $params, DocumentNode $document) {
366
    $operation = $params->operation;
367
    // Skip validation if there are no validation rules to be applied.
368
    if (!$rules = $this->resolveValidationRules($config, $params, $document, $operation)) {
369
      return [];
370
    }
371
372
    $schema = $config->getSchema();
373
    $info = new TypeInfo($schema);
374
    $validation = new ValidationContext($schema, $document, $info);
375
    $visitors = array_values(array_map(function (AbstractValidationRule $rule) use ($validation) {
376
      return $rule($validation);
377
    }, $rules));
378
379
    // Run the query visitor with the prepared validation rules and the cache
380
    // metadata collector and query complexity calculator.
381
    Visitor::visit($document, Visitor::visitWithTypeInfo($info, Visitor::visitInParallel($visitors)));
382
383
    // Return any possible errors collected during validation.
384
    return $validation->getErrors();
385
  }
386
387
  /**
388
   * @param \GraphQL\Server\ServerConfig $config
389
   * @param \GraphQL\Server\OperationParams $params
390
   * @param \GraphQL\Language\AST\DocumentNode $document
391
   * @param $operation
392
   *
393
   * @return mixed
394
   */
395 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...
396
    $root = $config->getRootValue();
397
    if (is_callable($root)) {
398
      $root = $root($params, $document, $operation);
399
    }
400
401
    return $root;
402
  }
403
404
  /**
405
   * @param \GraphQL\Server\ServerConfig $config
406
   * @param \GraphQL\Server\OperationParams $params
407
   * @param \GraphQL\Language\AST\DocumentNode $document
408
   * @param $operation
409
   *
410
   * @return mixed
411
   */
412 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...
413
    $context = $config->getContext();
414
    if (is_callable($context)) {
415
      $context = $context($params, $document, $operation);
416
    }
417
418
    return $context;
419
  }
420
421
  /**
422
   * @param \GraphQL\Server\ServerConfig $config
423
   * @param \GraphQL\Server\OperationParams $params
424
   * @param \GraphQL\Language\AST\DocumentNode $document
425
   * @param $operation
426
   *
427
   * @return array
428
   */
429
  protected function resolveValidationRules(ServerConfig $config, OperationParams $params, DocumentNode $document, $operation) {
430
    // Allow customizing validation rules per operation:
431
    $rules = $config->getValidationRules();
432
    if (is_callable($rules)) {
433
      $rules = $rules($params, $document, $operation);
434
      if (!is_array($rules)) {
435
        throw new \LogicException(sprintf("Expecting validation rules to be array or callable returning array, but got: %s", Utils::printSafe($rules)));
436
      }
437
    }
438
439
    return $rules;
440
  }
441
442
  /**
443
   * @param \GraphQL\Server\ServerConfig $config
444
   * @param \GraphQL\Server\OperationParams $params
445
   *
446
   * @return mixed
447
   * @throws \GraphQL\Server\RequestError
448
   */
449
  protected function loadPersistedQuery(ServerConfig $config, OperationParams $params) {
450
    if (!$loader = $config->getPersistentQueryLoader()) {
451
      throw new RequestError('Persisted queries are not supported by this server.');
452
    }
453
454
    $source = $loader($params->queryId, $params);
455
    if (!is_string($source) && !$source instanceof DocumentNode) {
456
      throw new \LogicException(sprintf('The persisted query loader must return query string or instance of %s but got: %s.', DocumentNode::class, Utils::printSafe($source)));
457
    }
458
459
    return $source;
460
  }
461
462
  /**
463
   * @param \GraphQL\Language\AST\DocumentNode $document
464
   *
465
   * @return array
466
   */
467
  protected function serializeDocument(DocumentNode $document) {
468
    return $this->sanitizeRecursive(AST::toArray($document));
469
  }
470
471
  /**
472
   * @param array $item
473
   *
474
   * @return array
475
   */
476
  protected function sanitizeRecursive(array $item) {
477
    unset($item['loc']);
478
479
    foreach ($item as &$value) {
480
      if (is_array($value)) {
481
        $value = $this->sanitizeRecursive($value);
482
      }
483
    }
484
485
    return $item;
486
  }
487
488
  /**
489
   * @param \GraphQL\Language\AST\DocumentNode $document
490
   * @param \Drupal\Core\Cache\CacheableMetadata $metadata
491
   *
492
   * @return string
493
   */
494
  protected function cacheIdentifier(DocumentNode $document, CacheableMetadata $metadata) {
495
    $contexts = $metadata->getCacheContexts();
496
    $keys = $this->contextsManager->convertTokensToKeys($contexts)->getKeys();
497
    // Prepend the hash of the serialized document to the cache contexts.
498
    $hash = hash('sha256', json_encode($this->serializeDocument($document)));
499
    return implode(':', array_values(array_merge([$hash], $keys)));
500
  }
501
}
502