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

QueryProcessor::resolveContextValue()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 5

Duplication

Lines 8
Ratio 100 %

Importance

Changes 0
Metric Value
cc 2
eloc 5
nc 2
nop 4
dl 8
loc 8
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
namespace Drupal\graphql\GraphQL\Execution;
4
5
use Drupal\Core\Cache\CacheableMetadata;
6
use Drupal\Core\Render\RenderContext;
7
use Drupal\Core\Render\RendererInterface;
8
use Drupal\Core\Session\AccountProxyInterface;
9
use Drupal\graphql\Plugin\SchemaPluginManager;
10
use Drupal\graphql\GraphQL\QueryProvider\QueryProviderInterface;
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\GraphQL;
18
use GraphQL\Language\AST\DocumentNode;
19
use GraphQL\Language\Parser;
20
use GraphQL\Language\Source;
21
use GraphQL\Server\Helper;
22
use GraphQL\Server\OperationParams;
23
use GraphQL\Server\RequestError;
24
use GraphQL\Server\ServerConfig;
25
use GraphQL\Type\Schema;
26
use GraphQL\Utils\AST;
27
use GraphQL\Utils\Utils;
28
use GraphQL\Validator\DocumentValidator;
29
30
class QueryProcessor {
31
32
  /**
33
   * The current user account.
34
   *
35
   * @var \Drupal\Core\Session\AccountProxyInterface
36
   */
37
  protected $currentUser;
38
39
  /**
40
   * The schema plugin manager.
41
   *
42
   * @var \Drupal\graphql\Plugin\SchemaPluginManager
43
   */
44
  protected $pluginManager;
45
46
  /**
47
   * The query provider service.
48
   *
49
   * @var \Drupal\graphql\GraphQL\QueryProvider\QueryProviderInterface
50
   */
51
  protected $queryProvider;
52
53
  /**
54
   * The renderer service.
55
   *
56
   * @var \Drupal\Core\Render\RendererInterface
57
   */
58
  protected $renderer;
59
60
  /**
61
   * Processor constructor.
62
   *
63
   * @param \Drupal\Core\Render\RendererInterface $renderer
64
   *   The renderer service.
65
   * @param \Drupal\Core\Session\AccountProxyInterface $currentUser
66
   *   The current user.
67
   * @param \Drupal\graphql\Plugin\SchemaPluginManager $pluginManager
68
   *   The schema plugin manager.
69
   * @param \Drupal\graphql\GraphQL\QueryProvider\QueryProviderInterface $queryProvider
70
   *   The query provider service.
71
   */
72
  public function __construct(
73
    RendererInterface $renderer,
74
    AccountProxyInterface $currentUser,
75
    SchemaPluginManager $pluginManager,
76
    QueryProviderInterface $queryProvider
77
  ) {
78
    $this->renderer = $renderer;
79
    $this->currentUser = $currentUser;
80
    $this->pluginManager = $pluginManager;
81
    $this->queryProvider = $queryProvider;
82
  }
83
84
  /**
85
   * Processes one or multiple graphql operations.
86
   *
87
   * @param string $schema
88
   *   The plugin id of the schema to use.
89
   * @param \GraphQL\Server\OperationParams|\GraphQL\Server\OperationParams[] $params
90
   *   The graphql operation(s) to execute.
91
   * @param mixed $context
92
   *   The query context.
93
   * @param bool $debug
94
   *   Whether to run this query in debugging mode.
95
   *
96
   * @return \Drupal\graphql\GraphQL\Execution\QueryResult
97
   *   The query result.
98
   */
99
  public function processQuery($schema, $params, $context = NULL, $debug = FALSE) {
100
    // Load the plugin from the schema manager.
101
    $plugin = $this->pluginManager->createInstance($schema);
102
    $schema = $plugin->getSchema();
103
104
    // Create the server config.
105
    $config = ServerConfig::create();
106
    $config->setDebug($debug);
107
    $config->setSchema($schema);
108
    $config->setContext($context);
109
    $config->setQueryBatching(TRUE);
110
    $config->setPersistentQueryLoader(function ($id, OperationParams $params) {
111
      if ($query = $this->queryProvider->getQuery($id, $params)) {
112
        return $query;
113
      }
114
115
      throw new RequestError(sprintf("Failed to load query map for id '%s'.", $id));
116
    });
117
118
    return $this->executeQuery($config, $params);
119
  }
120
121
  /**
122
   * Executes one or multiple graphql operations.
123
   *
124
   * @param \GraphQL\Server\ServerConfig $config
125
   *   The server config.
126
   * @param \GraphQL\Server\OperationParams|\GraphQL\Server\OperationParams[] $params
127
   *   The graphql operation(s) to execute.
128
   *
129
   * @return \Drupal\graphql\GraphQL\Execution\QueryResult
130
   *   The result of executing the operations.
131
   */
132
  protected function executeQuery(ServerConfig $config, $params) {
133
    // Evaluating the request might lead to rendering of markup which in turn
134
    // might "leak" cache metadata. Therefore, we execute the request within a
135
    // render context and collect the leaked metadata afterwards.
136
    $context = new RenderContext();
137
    /** @var \GraphQL\Executor\ExecutionResult|\GraphQL\Executor\ExecutionResult[] $result */
138
    $result = $this->renderer->executeInRenderContext($context, function() use ($config, $params) {
139
      if (is_array($params)) {
140
        return $this->executeBatch($config, $params);
141
      }
142
143
      return $this->executeSingle($config, $params);
144
    });
145
146
    $metadata = new CacheableMetadata();
147
    // Apply render context cache metadata to the response.
148
    if (!$context->isEmpty()) {
149
      $metadata->addCacheableDependency($context->pop());
150
    }
151
152
    return new QueryResult($result, $metadata);
153
  }
154
155
  /**
156
   * @param \GraphQL\Server\ServerConfig $config
157
   * @param \GraphQL\Server\OperationParams $params
158
   *
159
   * @return mixed
160
   */
161
  public function executeSingle(ServerConfig $config, OperationParams $params) {
162
    $adapter = new SyncPromiseAdapter();
163
    $result = $this->promiseToExecuteOperation($adapter, $config, $params, FALSE);
164
    return $adapter->wait($result);
165
  }
166
167
  /**
168
   * @param \GraphQL\Server\ServerConfig $config
169
   * @param array $params
170
   *
171
   * @return mixed
172
   */
173
  public function executeBatch(ServerConfig $config, array $params) {
174
    $adapter = new SyncPromiseAdapter();
175
    $result = array_map(function ($params) use ($adapter, $config) {
176
      $this->promiseToExecuteOperation($adapter, $config, $params, TRUE);
177
    }, $params);
178
179
    $result = $adapter->all($result);
180
    return $adapter->wait($result);
181
  }
182
183
  /**
184
   * @param \GraphQL\Executor\Promise\PromiseAdapter $adapter
185
   * @param \GraphQL\Server\ServerConfig $config
186
   * @param \GraphQL\Server\OperationParams $params
187
   * @param bool $batching
188
   *
189
   * @return \GraphQL\Executor\Promise\Promise
190
   */
191
  protected function promiseToExecuteOperation(PromiseAdapter $adapter, ServerConfig $config, OperationParams $params, $batching = FALSE) {
192
    try {
193
      if (!$config->getSchema()) {
194
        throw new \LogicException('Missing schema for query execution.');
195
      }
196
197
      if ($batching && !$config->getQueryBatching()) {
198
        throw new RequestError('Batched queries are not supported by this server.');
199
      }
200
201
      if ($errors = (new Helper())->validateOperationParams($params)) {
202
        $errors = Utils::map($errors, function (RequestError $err) {
203
          return Error::createLocatedError($err, NULL, NULL);
204
        });
205
206
        return $adapter->createFulfilled(
207
          new ExecutionResult(NULL, $errors)
208
        );
209
      }
210
211
      $variables = $params->variables;
212
      $operation = $params->operation;
213
      $document = $params->queryId ? $this->loadPersistedQuery($config, $params) : $params->query;
214
      if (!$document instanceof DocumentNode) {
215
        $document = Parser::parse($document);
216
      }
217
218
      if ($params->isReadOnly() && AST::getOperation($document, $operation) !== 'query') {
219
        throw new RequestError('GET requests are only supported for query operations.');
220
      }
221
222
      $schema = $config->getSchema();
223
      $resolver = $config->getFieldResolver();
224
      $root = $this->resolveRootValue($config, $params, $document, $operation);
225
      $context = $this->resolveContextValue($config, $params, $document, $operation);
226
      $rules = $this->resolveValidationRules($config, $params, $document, $operation);
227
      $result = $this->promiseToExecute(
228
        $adapter,
229
        $schema,
230
        $document,
231
        $root,
232
        $context,
233
        $variables,
234
        $operation,
235
        $resolver,
236
        $rules
237
      );
238
    }
239
    catch (RequestError $exception) {
240
      $result = $adapter->createFulfilled(new ExecutionResult(NULL, [Error::createLocatedError($exception)]));
241
    }
242
    catch (Error $exception) {
243
      $result = $adapter->createFulfilled(new ExecutionResult(NULL, [$exception]));
244
    }
245
246
    return $result->then(function(ExecutionResult $result) use ($config) {
247
      if ($config->getErrorsHandler()) {
248
        $result->setErrorsHandler($config->getErrorsHandler());
249
      }
250
251
      if ($config->getErrorFormatter() || $config->getDebug()) {
252
        $result->setErrorFormatter(FormattedError::prepareFormatter($config->getErrorFormatter(), $config->getDebug()));
253
      }
254
255
      return $result;
256
    });
257
  }
258
259
  /**
260
   * @param \GraphQL\Executor\Promise\PromiseAdapter $adapter
261
   * @param \GraphQL\Type\Schema $schema
262
   * @param \GraphQL\Language\AST\DocumentNode $document
263
   * @param null $root
264
   * @param null $context
265
   * @param null $variables
266
   * @param null $operation
267
   * @param callable|NULL $resolver
268
   * @param array|NULL $rules
269
   *
270
   * @return \GraphQL\Executor\Promise\Promise
271
   */
272
  protected function promiseToExecute(
273
    PromiseAdapter $adapter,
274
    Schema $schema,
275
    DocumentNode $document,
276
    $root = NULL,
277
    $context = NULL,
278
    $variables = NULL,
279
    $operation = NULL,
280
    callable $resolver = NULL,
281
    array $rules = NULL
282
  ) {
283
    try {
284
      if ($errors = DocumentValidator::validate($schema, $document, $rules)) {
0 ignored issues
show
Bug introduced by
It seems like $rules defined by parameter $rules on line 281 can also be of type array; however, GraphQL\Validator\DocumentValidator::validate() does only seem to accept null|array<integer,objec...bstractValidationRule>>, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
285
        return $adapter->createFulfilled(new ExecutionResult(NULL, $errors));
286
      }
287
288
      // TODO: Visit the document nodes, extract cache metadata and perform a cache lookup.
289
      return Executor::promiseToExecute($adapter, $schema, $document, $root, $context, $variables, $operation, $resolver);
290
    }
291
    catch (Error $exception) {
292
      return $adapter->createFulfilled(new ExecutionResult(NULL, [$exception]));
293
    }
294
  }
295
296
  /**
297
   * @param \GraphQL\Server\ServerConfig $config
298
   * @param \GraphQL\Server\OperationParams $params
299
   * @param \GraphQL\Language\AST\DocumentNode $document
300
   * @param $operation
301
   *
302
   * @return callable|mixed
303
   */
304 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...
305
    $root = $config->getRootValue();
306
    if (is_callable($root)) {
307
      $root = $root($params, $document, $operation);
308
    }
309
310
    return $root;
311
  }
312
313
  /**
314
   * @param \GraphQL\Server\ServerConfig $config
315
   * @param \GraphQL\Server\OperationParams $params
316
   * @param \GraphQL\Language\AST\DocumentNode $document
317
   * @param $operation
318
   *
319
   * @return callable|mixed
320
   */
321 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...
322
    $context = $config->getContext();
323
    if (is_callable($context)) {
324
      $context = $context($params, $document, $operation);
325
    }
326
327
    return $context;
328
  }
329
330
  /**
331
   * @param \GraphQL\Server\ServerConfig $config
332
   * @param \GraphQL\Server\OperationParams $params
333
   * @param \GraphQL\Language\AST\DocumentNode $document
334
   * @param $operation
335
   *
336
   * @return array|callable
337
   */
338
  protected function resolveValidationRules(ServerConfig $config, OperationParams $params, DocumentNode $document, $operation) {
339
    // Allow customizing validation rules per operation:
340
    $rules = $config->getValidationRules();
341
    if (is_callable($rules)) {
342
      $rules = $rules($params, $document, $operation);
343
      if (!is_array($rules)) {
344
        throw new \LogicException(sprintf("Expecting validation rules to be array or callable returning array, but got: %s", Utils::printSafe($rules)));
345
      }
346
    }
347
348
    return $rules;
349
  }
350
351
  /**
352
   * @param \GraphQL\Server\ServerConfig $config
353
   * @param \GraphQL\Server\OperationParams $params
354
   *
355
   * @return mixed
356
   * @throws \GraphQL\Server\RequestError
357
   */
358
  protected function loadPersistedQuery(ServerConfig $config, OperationParams $params) {
359
    if (!$loader = $config->getPersistentQueryLoader()) {
360
      throw new RequestError('Persisted queries are not supported by this server.');
361
    }
362
363
    $source = $loader($params->queryId, $params);
364
    if (!is_string($source) && !$source instanceof DocumentNode) {
365
      throw new \LogicException(sprintf('The persisted query loader must return query string or instance of %s but got: %s.', DocumentNode::class, Utils::printSafe($source)));
366
    }
367
368
    return $source;
369
  }
370
371
}
372