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

QueryProcessor::processQuery()   B

Complexity

Conditions 3
Paths 2

Size

Total Lines 36
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 17
nc 2
nop 3
dl 0
loc 36
rs 8.8571
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\Session\AccountProxyInterface;
7
use Drupal\graphql\GraphQL\Visitors\CacheMetadataCollector;
8
use Drupal\graphql\Plugin\SchemaPluginManager;
9
use Drupal\graphql\GraphQL\QueryProvider\QueryProviderInterface;
10
use GraphQL\Error\Error;
11
use GraphQL\Error\FormattedError;
12
use GraphQL\Executor\ExecutionResult;
13
use GraphQL\Executor\Executor;
14
use GraphQL\Executor\Promise\Adapter\SyncPromiseAdapter;
15
use GraphQL\Executor\Promise\PromiseAdapter;
16
use GraphQL\Language\AST\DocumentNode;
17
use GraphQL\Language\Parser;
18
use GraphQL\Language\Visitor;
19
use GraphQL\Server\Helper;
20
use GraphQL\Server\OperationParams;
21
use GraphQL\Server\RequestError;
22
use GraphQL\Server\ServerConfig;
23
use GraphQL\Type\Schema;
24
use GraphQL\Utils\AST;
25
use GraphQL\Utils\Utils;
26
use GraphQL\Validator\DocumentValidator;
27
28
class QueryProcessor {
29
30
  /**
31
   * The current user account.
32
   *
33
   * @var \Drupal\Core\Session\AccountProxyInterface
34
   */
35
  protected $currentUser;
36
37
  /**
38
   * The schema plugin manager.
39
   *
40
   * @var \Drupal\graphql\Plugin\SchemaPluginManager
41
   */
42
  protected $pluginManager;
43
44
  /**
45
   * The query provider service.
46
   *
47
   * @var \Drupal\graphql\GraphQL\QueryProvider\QueryProviderInterface
48
   */
49
  protected $queryProvider;
50
51
  /**
52
   * Processor constructor.
53
   *
54
   * @param \Drupal\Core\Session\AccountProxyInterface $currentUser
55
   *   The current user.
56
   * @param \Drupal\graphql\Plugin\SchemaPluginManager $pluginManager
57
   *   The schema plugin manager.
58
   * @param \Drupal\graphql\GraphQL\QueryProvider\QueryProviderInterface $queryProvider
59
   *   The query provider service.
60
   */
61
  public function __construct(
62
    AccountProxyInterface $currentUser,
63
    SchemaPluginManager $pluginManager,
64
    QueryProviderInterface $queryProvider
65
  ) {
66
    $this->currentUser = $currentUser;
67
    $this->pluginManager = $pluginManager;
68
    $this->queryProvider = $queryProvider;
69
  }
70
71
  /**
72
   * Processes one or multiple graphql operations.
73
   *
74
   * @param string $schema
75
   *   The plugin id of the schema to use.
76
   * @param \GraphQL\Server\OperationParams|\GraphQL\Server\OperationParams[] $params
77
   *   The graphql operation(s) to execute.
78
   * @param array $globals
79
   *   The query context.
80
   *
81
   * @return \Drupal\graphql\GraphQL\Execution\QueryResult|\Drupal\graphql\GraphQL\Execution\QueryResult[]
82
   *   The query result.
83
   *
84
   */
85
  public function processQuery($schema, $params, array $globals = []) {
86
    // Load the plugin from the schema manager.
87
    $plugin = $this->pluginManager->createInstance($schema);
88
    $schema = $plugin->getSchema();
89
90
    // If the current user has appropriate permissions, allow to bypass
91
    // the secure fields restriction.
92
    $globals['bypass field security'] = $this->currentUser->hasPermission('bypass graphql field security');
93
94
    // Create the server config.
95
    $config = ServerConfig::create();
96
    $config->setDebug(!empty($globals['development']));
97
    $config->setSchema($schema);
98
    $config->setQueryBatching(TRUE);
99
    $config->setContext(function () use ($globals) {
100
      // Each document (e.g. in a batch query) gets its own resolve context but
101
      // the global parameters are shared. This allows us to collect the cache
102
      // metadata and contextual values (e.g. inheritance for language) for each
103
      // query separately.
104
      return new ResolveContext($globals);
105
    });
106
107
    $config->setPersistentQueryLoader(function ($id, OperationParams $params) {
108
      if ($query = $this->queryProvider->getQuery($id, $params)) {
109
        return $query;
110
      }
111
112
      throw new RequestError(sprintf("Failed to load query map for id '%s'.", $id));
113
    });
114
115
    if (is_array($params)) {
116
      return $this->executeBatch($config, $params);
117
    }
118
119
    return $this->executeSingle($config, $params);
120
  }
121
122
  /**
123
   * @param \GraphQL\Server\ServerConfig $config
124
   * @param \GraphQL\Server\OperationParams $params
125
   *
126
   * @return mixed
127
   */
128
  public function executeSingle(ServerConfig $config, OperationParams $params) {
129
    $adapter = new SyncPromiseAdapter();
130
    $result = $this->executeOperation($adapter, $config, $params, FALSE);
131
    return $adapter->wait($result);
132
  }
133
134
  /**
135
   * @param \GraphQL\Server\ServerConfig $config
136
   * @param array $params
137
   *
138
   * @return mixed
139
   */
140
  public function executeBatch(ServerConfig $config, array $params) {
141
    $adapter = new SyncPromiseAdapter();
142
    $result = array_map(function ($params) use ($adapter, $config) {
143
      return $this->executeOperation($adapter, $config, $params, TRUE);
144
    }, $params);
145
146
    $result = $adapter->all($result);
147
    return $adapter->wait($result);
148
  }
149
150
  /**
151
   * @param \GraphQL\Executor\Promise\PromiseAdapter $adapter
152
   * @param \GraphQL\Server\ServerConfig $config
153
   * @param \GraphQL\Server\OperationParams $params
154
   * @param bool $batching
155
   *
156
   * @return \GraphQL\Executor\Promise\Promise
157
   */
158
  protected function executeOperation(PromiseAdapter $adapter, ServerConfig $config, OperationParams $params, $batching = FALSE) {
159
    try {
160
      if (!$config->getSchema()) {
161
        throw new \LogicException('Missing schema for query execution.');
162
      }
163
164
      if ($batching && !$config->getQueryBatching()) {
165
        throw new RequestError('Batched queries are not supported by this server.');
166
      }
167
168
      if ($errors = (new Helper())->validateOperationParams($params)) {
169
        $errors = Utils::map($errors, function (RequestError $err) {
170
          return Error::createLocatedError($err, NULL, NULL);
171
        });
172
173
        return $adapter->createFulfilled(new QueryResult(NULL, $errors));
174
      }
175
176
      $schema = $config->getSchema();
177
      $variables = $params->variables;
178
      $operation = $params->operation;
179
      $document = $params->queryId ? $this->loadPersistedQuery($config, $params) : $params->query;
180
      if (!$document instanceof DocumentNode) {
181
        $document = Parser::parse($document);
182
183
        // Assume that pre-parsed documents are already validated. This allows
184
        // us to store pre-validated query documents e.g. for persisted queries
185
        // effectively improving performance by skipping run-time validation.
186
        $rules = $this->resolveValidationRules($config, $params, $document, $operation);
187
        if ($errors = DocumentValidator::validate($schema, $document, $rules)) {
188
          return $adapter->createFulfilled(new QueryResult(NULL, $errors));
189
        }
190
      }
191
192
      if ($params->isReadOnly() && AST::getOperation($document, $operation) !== 'query') {
193
        throw new RequestError('GET requests are only supported for query operations.');
194
      }
195
196
      // TODO: Collect cache metadata from AST and perform a cach lookup.
197
198
      $resolver = $config->getFieldResolver();
199
      $root = $this->resolveRootValue($config, $params, $document, $operation);
200
      $context = $this->resolveContextValue($config, $params, $document, $operation);
201
      $promise = Executor::promiseToExecute(
202
        $adapter,
203
        $schema,
204
        $document,
205
        $root,
206
        $context,
207
        $variables,
208
        $operation,
209
        $resolver
210
      );
211
212
      return $promise->then(function (ExecutionResult $result) use ($context) {
213
        $metadata = (new CacheableMetadata())->addCacheableDependency($context);
214
        return new QueryResult($result->data, $result->errors, $result->extensions, $metadata);
215
      });
216
    }
217
    catch (RequestError $exception) {
218
      $result = $adapter->createFulfilled(new QueryResult(NULL, [Error::createLocatedError($exception)]));
219
    }
220
    catch (Error $exception) {
221
      $result = $adapter->createFulfilled(new QueryResult(NULL, [$exception]));
222
    }
223
224
    return $result->then(function(QueryResult $result) use ($config) {
225
      if ($config->getErrorsHandler()) {
226
        $result->setErrorsHandler($config->getErrorsHandler());
227
      }
228
229
      if ($config->getErrorFormatter() || $config->getDebug()) {
230
        $result->setErrorFormatter(FormattedError::prepareFormatter($config->getErrorFormatter(), $config->getDebug()));
231
      }
232
233
      return $result;
234
    });
235
  }
236
237
  /**
238
   * @param \GraphQL\Server\ServerConfig $config
239
   * @param \GraphQL\Server\OperationParams $params
240
   * @param \GraphQL\Language\AST\DocumentNode $document
241
   * @param $operation
242
   *
243
   * @return callable|mixed
244
   */
245 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...
246
    $root = $config->getRootValue();
247
    if (is_callable($root)) {
248
      $root = $root($params, $document, $operation);
249
    }
250
251
    return $root;
252
  }
253
254
  /**
255
   * @param \GraphQL\Server\ServerConfig $config
256
   * @param \GraphQL\Server\OperationParams $params
257
   * @param \GraphQL\Language\AST\DocumentNode $document
258
   * @param $operation
259
   *
260
   * @return callable|mixed
261
   */
262 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...
263
    $context = $config->getContext();
264
    if (is_callable($context)) {
265
      $context = $context($params, $document, $operation);
266
    }
267
268
    return $context;
269
  }
270
271
  /**
272
   * @param \GraphQL\Server\ServerConfig $config
273
   * @param \GraphQL\Server\OperationParams $params
274
   * @param \GraphQL\Language\AST\DocumentNode $document
275
   * @param $operation
276
   *
277
   * @return array|callable
278
   */
279
  protected function resolveValidationRules(ServerConfig $config, OperationParams $params, DocumentNode $document, $operation) {
280
    // Allow customizing validation rules per operation:
281
    $rules = $config->getValidationRules();
282
    if (is_callable($rules)) {
283
      $rules = $rules($params, $document, $operation);
284
      if (!is_array($rules)) {
285
        throw new \LogicException(sprintf("Expecting validation rules to be array or callable returning array, but got: %s", Utils::printSafe($rules)));
286
      }
287
    }
288
289
    return $rules;
290
  }
291
292
  /**
293
   * @param \GraphQL\Server\ServerConfig $config
294
   * @param \GraphQL\Server\OperationParams $params
295
   *
296
   * @return mixed
297
   * @throws \GraphQL\Server\RequestError
298
   */
299
  protected function loadPersistedQuery(ServerConfig $config, OperationParams $params) {
300
    if (!$loader = $config->getPersistentQueryLoader()) {
301
      throw new RequestError('Persisted queries are not supported by this server.');
302
    }
303
304
    $source = $loader($params->queryId, $params);
305
    if (!is_string($source) && !$source instanceof DocumentNode) {
306
      throw new \LogicException(sprintf('The persisted query loader must return query string or instance of %s but got: %s.', DocumentNode::class, Utils::printSafe($source)));
307
    }
308
309
    return $source;
310
  }
311
312
}
313