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

QueryProcessor   C

Complexity

Total Complexity 41

Size/Duplication

Total Lines 363
Duplicated Lines 4.41 %

Coupling/Cohesion

Components 1
Dependencies 18

Importance

Changes 0
Metric Value
dl 16
loc 363
rs 6.1968
c 0
b 0
f 0
wmc 41
lcom 1
cbo 18

12 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 9 1
B processQuery() 0 47 4
A executeSingle() 0 5 1
A executeBatch() 0 9 1
D executeOperation() 0 107 18
A resolveRootValue() 8 8 2
A resolveContextValue() 8 8 2
A resolveValidationRules() 0 12 3
A resolveAllowedComplexity() 0 3 1
A loadPersistedQuery() 0 12 4
A serializeDocument() 0 3 1
A sanitizeRecursive() 0 11 3

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\Session\AccountProxyInterface;
7
use Drupal\graphql\GraphQL\Visitors\CacheMetadataCalculator;
8
use Drupal\graphql\GraphQL\Visitors\ComplexityCalculator;
9
use Drupal\graphql\GraphQL\Visitors\QueryEdgeCollector;
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
   * Processor constructor.
57
   *
58
   * @param \Drupal\Core\Session\AccountProxyInterface $currentUser
59
   *   The current user.
60
   * @param \Drupal\graphql\Plugin\SchemaPluginManager $pluginManager
61
   *   The schema plugin manager.
62
   * @param \Drupal\graphql\GraphQL\QueryProvider\QueryProviderInterface $queryProvider
63
   *   The query provider service.
64
   */
65
  public function __construct(
66
    AccountProxyInterface $currentUser,
67
    SchemaPluginManager $pluginManager,
68
    QueryProviderInterface $queryProvider
69
  ) {
70
    $this->currentUser = $currentUser;
71
    $this->pluginManager = $pluginManager;
72
    $this->queryProvider = $queryProvider;
73
  }
74
75
  /**
76
   * Processes one or multiple graphql operations.
77
   *
78
   * @param string $schema
79
   *   The plugin id of the schema to use.
80
   * @param \GraphQL\Server\OperationParams|\GraphQL\Server\OperationParams[] $params
81
   *   The graphql operation(s) to execute.
82
   * @param array $globals
83
   *   The query context.
84
   *
85
   * @return \Drupal\graphql\GraphQL\Execution\QueryResult|\Drupal\graphql\GraphQL\Execution\QueryResult[]
86
   *   The query result.
87
   *
88
   */
89
  public function processQuery($schema, $params, array $globals = []) {
90
    // Load the plugin from the schema manager.
91
    $plugin = $this->pluginManager->createInstance($schema);
92
    $schema = $plugin->getSchema();
93
94
    // If the current user has appropriate permissions, allow to bypass
95
    // the secure fields restriction.
96
    $globals['bypass field security'] = $this->currentUser->hasPermission('bypass graphql field security');
97
98
    // Create the server config.
99
    $config = ServerConfig::create();
100
    $config->setDebug(!empty($globals['development']));
101
    $config->setSchema($schema);
102
    $config->setQueryBatching(TRUE);
103
    $config->setContext(function () use ($globals) {
104
      // Each document (e.g. in a batch query) gets its own resolve context but
105
      // the global parameters are shared. This allows us to collect the cache
106
      // metadata and contextual values (e.g. inheritance for language) for each
107
      // query separately.
108
      return new ResolveContext($globals);
109
    });
110
111
    $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...
112
      if (isset($params->queryId)) {
113
        // Assume that pre-parsed documents are already validated. This allows
114
        // us to store pre-validated query documents e.g. for persisted queries
115
        // effectively improving performance by skipping run-time validation.
116
        return [];
117
      }
118
119
      return array_values(DocumentValidator::allRules());
120
    });
121
122
    $config->setPersistentQueryLoader(function ($id, OperationParams $params) {
123
      if ($query = $this->queryProvider->getQuery($id, $params)) {
124
        return $query;
125
      }
126
127
      throw new RequestError(sprintf("Failed to load query map for id '%s'.", $id));
128
    });
129
130
    if (is_array($params)) {
131
      return $this->executeBatch($config, $params);
132
    }
133
134
    return $this->executeSingle($config, $params);
135
  }
136
137
  /**
138
   * @param \GraphQL\Server\ServerConfig $config
139
   * @param \GraphQL\Server\OperationParams $params
140
   *
141
   * @return mixed
142
   */
143
  public function executeSingle(ServerConfig $config, OperationParams $params) {
144
    $adapter = new SyncPromiseAdapter();
145
    $result = $this->executeOperation($adapter, $config, $params, FALSE);
146
    return $adapter->wait($result);
147
  }
148
149
  /**
150
   * @param \GraphQL\Server\ServerConfig $config
151
   * @param array $params
152
   *
153
   * @return mixed
154
   */
155
  public function executeBatch(ServerConfig $config, array $params) {
156
    $adapter = new SyncPromiseAdapter();
157
    $result = array_map(function ($params) use ($adapter, $config) {
158
      return $this->executeOperation($adapter, $config, $params, TRUE);
159
    }, $params);
160
161
    $result = $adapter->all($result);
162
    return $adapter->wait($result);
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 \LogicException('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 = (new Helper())->validateOperationParams($params)) {
184
        $errors = Utils::map($errors, function (RequestError $err) {
185
          return Error::createLocatedError($err, NULL, NULL);
186
        });
187
188
        return $adapter->createFulfilled(new QueryResult(NULL, $errors));
189
      }
190
191
      $schema = $config->getSchema();
192
      $variables = $params->variables;
193
      $operation = $params->operation;
194
      $document = $params->queryId ? $this->loadPersistedQuery($config, $params) : $params->query;
195
      if (!$document instanceof DocumentNode) {
196
        $document = Parser::parse($document);
197
      }
198
199
      // Read the operation type from the document. Subscriptions and mutations
200
      // only work through POST requests. One cannot have mutations and queries
201
      // in the same document, hence this check is sufficient.
202
      $type = AST::getOperation($document, $operation);
203
      if ($params->isReadOnly() && $type !== 'query') {
204
        throw new RequestError('GET requests are only supported for query operations.');
205
      }
206
207
      /** @var \Drupal\graphql\GraphQL\Execution\ResolveContext $context */
208
      $context = $this->resolveContextValue($config, $params, $document, $operation);
209
      $complexity = $this->resolveAllowedComplexity($config, $params, $document, $operation);
210
      $rules = $this->resolveValidationRules($config, $params, $document, $operation);
211
      $info = new TypeInfo($schema);
212
      $validation = new ValidationContext($schema, $document, $info);
213
214
      // Add a special visitor to the set of validation rules which will collect
215
      // all nodes and fields (all possible edges) from the document to allow
216
      // for static query analysis (e.g. for calculating the query complexity or
217
      // for determining the cache metadata of a query so we can perform cache
218
      // lookups).
219
      $visitors = array_values(array_map(function (AbstractValidationRule $rule) use ($validation, $context) {
220
        return $rule->getVisitor($validation);
221
      }, array_merge($rules, [new QueryEdgeCollector(array_filter([
222
        // Query operations can be cached. Collect static cache metadata from
223
        // the document during the visitor phase.
224
        $type === 'query' ? new CacheMetadataCalculator($context) : NULL,
225
        $complexity !== NULL ? new ComplexityCalculator($complexity) : NULL,
0 ignored issues
show
Unused Code introduced by
The call to ComplexityCalculator::__construct() has too many arguments starting with $complexity.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
226
      ]))])));
227
228
      // Run the query visitor with the prepared validation rules and the cache
229
      // metadata collector and query complexity calculator.
230
      Visitor::visit($document, Visitor::visitWithTypeInfo($info, Visitor::visitInParallel($visitors)));
231
232
      // If one of the validation rules found any problems, do not resolve the
233
      // query and bail out early instead.
234
      if ($errors = $validation->getErrors()) {
235
        return $adapter->createFulfilled(new QueryResult(NULL, $errors));
236
      }
237
238
      // TODO: Perform a cach lookup.
239
      if ($context->getCacheMaxAge() !== 0) {
240
        $hash = hash('sha256', $this->serializeDocument($document));
0 ignored issues
show
Unused Code introduced by
$hash is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
241
      }
242
243
      $resolver = $config->getFieldResolver();
244
      $root = $this->resolveRootValue($config, $params, $document, $operation);
245
      $promise = Executor::promiseToExecute(
246
        $adapter,
247
        $schema,
248
        $document,
249
        $root,
250
        $context,
251
        $variables,
252
        $operation,
253
        $resolver
254
      );
255
256
      return $promise->then(function (ExecutionResult $result) use ($context) {
257
        $metadata = (new CacheableMetadata())->addCacheableDependency($context);
258
        return new QueryResult($result->data, $result->errors, $result->extensions, $metadata);
259
      });
260
    }
261
    catch (RequestError $exception) {
262
      $result = $adapter->createFulfilled(new QueryResult(NULL, [Error::createLocatedError($exception)]));
263
    }
264
    catch (Error $exception) {
265
      $result = $adapter->createFulfilled(new QueryResult(NULL, [$exception]));
266
    }
267
268
    return $result->then(function(QueryResult $result) use ($config) {
269
      if ($config->getErrorsHandler()) {
270
        $result->setErrorsHandler($config->getErrorsHandler());
271
      }
272
273
      if ($config->getErrorFormatter() || $config->getDebug()) {
274
        $result->setErrorFormatter(FormattedError::prepareFormatter($config->getErrorFormatter(), $config->getDebug()));
275
      }
276
277
      return $result;
278
    });
279
  }
280
281
  /**
282
   * @param \GraphQL\Server\ServerConfig $config
283
   * @param \GraphQL\Server\OperationParams $params
284
   * @param \GraphQL\Language\AST\DocumentNode $document
285
   * @param $operation
286
   *
287
   * @return mixed
288
   */
289 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...
290
    $root = $config->getRootValue();
291
    if (is_callable($root)) {
292
      $root = $root($params, $document, $operation);
293
    }
294
295
    return $root;
296
  }
297
298
  /**
299
   * @param \GraphQL\Server\ServerConfig $config
300
   * @param \GraphQL\Server\OperationParams $params
301
   * @param \GraphQL\Language\AST\DocumentNode $document
302
   * @param $operation
303
   *
304
   * @return mixed
305
   */
306 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...
307
    $context = $config->getContext();
308
    if (is_callable($context)) {
309
      $context = $context($params, $document, $operation);
310
    }
311
312
    return $context;
313
  }
314
315
  /**
316
   * @param \GraphQL\Server\ServerConfig $config
317
   * @param \GraphQL\Server\OperationParams $params
318
   * @param \GraphQL\Language\AST\DocumentNode $document
319
   * @param $operation
320
   *
321
   * @return array
322
   */
323
  protected function resolveValidationRules(ServerConfig $config, OperationParams $params, DocumentNode $document, $operation) {
324
    // Allow customizing validation rules per operation:
325
    $rules = $config->getValidationRules();
326
    if (is_callable($rules)) {
327
      $rules = $rules($params, $document, $operation);
328
      if (!is_array($rules)) {
329
        throw new \LogicException(sprintf("Expecting validation rules to be array or callable returning array, but got: %s", Utils::printSafe($rules)));
330
      }
331
    }
332
333
    return $rules;
334
  }
335
336
  /**
337
   * @param \GraphQL\Server\ServerConfig $config
338
   * @param \GraphQL\Server\OperationParams $params
339
   * @param \GraphQL\Language\AST\DocumentNode $document
340
   * @param $operation
341
   *
342
   * @return int|null
343
   */
344
  protected function resolveAllowedComplexity(ServerConfig $config, OperationParams $params, DocumentNode $document, $operation) {
0 ignored issues
show
Unused Code introduced by
The parameter $config 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 $params 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 $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...
345
    return NULL;
346
  }
347
348
  /**
349
   * @param \GraphQL\Server\ServerConfig $config
350
   * @param \GraphQL\Server\OperationParams $params
351
   *
352
   * @return mixed
353
   * @throws \GraphQL\Server\RequestError
354
   */
355
  protected function loadPersistedQuery(ServerConfig $config, OperationParams $params) {
356
    if (!$loader = $config->getPersistentQueryLoader()) {
357
      throw new RequestError('Persisted queries are not supported by this server.');
358
    }
359
360
    $source = $loader($params->queryId, $params);
361
    if (!is_string($source) && !$source instanceof DocumentNode) {
362
      throw new \LogicException(sprintf('The persisted query loader must return query string or instance of %s but got: %s.', DocumentNode::class, Utils::printSafe($source)));
363
    }
364
365
    return $source;
366
  }
367
368
  /**
369
   * @param \GraphQL\Language\AST\DocumentNode $document
370
   *
371
   * @return array
372
   */
373
  protected function serializeDocument(DocumentNode $document) {
374
    return $this->sanitizeRecursive(AST::toArray($document));
375
  }
376
377
  /**
378
   * @param array $item
379
   *
380
   * @return array
381
   */
382
  protected function sanitizeRecursive(array $item) {
383
    unset($item['loc']);
384
385
    foreach ($item as &$value) {
386
      if (is_array($value)) {
387
        $value = $this->sanitizeRecursive($value);
388
      }
389
    }
390
391
    return $item;
392
  }
393
394
}
395