View source
<?php
namespace Drupal\graphql\GraphQL\Execution;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Cache\Context\CacheContextsManager;
use Drupal\graphql\Plugin\SchemaPluginManager;
use Drupal\graphql\GraphQL\Cache\CacheableRequestError;
use GraphQL\Error\Error;
use GraphQL\Error\FormattedError;
use GraphQL\Executor\ExecutionResult;
use GraphQL\Executor\Executor;
use GraphQL\Executor\Promise\Adapter\SyncPromiseAdapter;
use GraphQL\Executor\Promise\PromiseAdapter;
use GraphQL\Language\AST\DocumentNode;
use GraphQL\Language\Parser;
use GraphQL\Language\Visitor;
use GraphQL\Server\Helper;
use GraphQL\Server\OperationParams;
use GraphQL\Server\RequestError;
use GraphQL\Server\ServerConfig;
use GraphQL\Utils\AST;
use GraphQL\Utils\TypeInfo;
use GraphQL\Utils\Utils;
use GraphQL\Validator\Rules\AbstractValidationRule;
use GraphQL\Validator\ValidationContext;
use GraphQL\Validator\Rules\QueryComplexity;
use Symfony\Component\HttpFoundation\RequestStack;
class QueryProcessor {
protected $pluginManager;
protected $cacheBackend;
protected $contextsManager;
protected $requestStack;
public function __construct(CacheContextsManager $contextsManager, SchemaPluginManager $pluginManager, CacheBackendInterface $cacheBackend, RequestStack $requestStack) {
$this->contextsManager = $contextsManager;
$this->pluginManager = $pluginManager;
$this->cacheBackend = $cacheBackend;
$this->requestStack = $requestStack;
}
public function processQuery($schema, $params) {
$plugin = $this->pluginManager
->createInstance($schema);
$config = $plugin
->getServer();
if (is_array($params)) {
return $this
->executeBatch($config, $params);
}
return $this
->executeSingle($config, $params);
}
public function executeSingle(ServerConfig $config, OperationParams $params) {
$adapter = new SyncPromiseAdapter();
$result = $this
->executeOperationWithReporting($adapter, $config, $params, FALSE);
return $adapter
->wait($result);
}
public function executeBatch(ServerConfig $config, array $params) {
$adapter = new SyncPromiseAdapter();
$result = array_map(function ($params) use ($adapter, $config) {
return $this
->executeOperationWithReporting($adapter, $config, $params, TRUE);
}, $params);
$result = $adapter
->all($result);
return $adapter
->wait($result);
}
protected function executeOperationWithReporting(PromiseAdapter $adapter, ServerConfig $config, OperationParams $params, $batching = FALSE) {
$result = $this
->executeOperation($adapter, $config, $params, $batching);
return $result
->then(function (QueryResult $result) use ($config) {
if ($config
->getErrorsHandler()) {
$result
->setErrorsHandler($config
->getErrorsHandler());
}
if ($config
->getErrorFormatter() || $config
->getDebug()) {
$result
->setErrorFormatter(FormattedError::prepareFormatter($config
->getErrorFormatter(), $config
->getDebug()));
}
return $result;
});
}
protected function executeOperation(PromiseAdapter $adapter, ServerConfig $config, OperationParams $params, $batching = FALSE) {
try {
if (!$config
->getSchema()) {
throw new Error('Missing schema for query execution.');
}
if ($batching && !$config
->getQueryBatching()) {
throw new RequestError('Batched queries are not supported by this server.');
}
if ($errors = $this
->validateOperationParams($params)) {
return $adapter
->createFulfilled(new QueryResult(NULL, $errors));
}
$persisted = isset($params->queryId);
$document = $persisted ? $this
->loadPersistedQuery($config, $params) : $params->query;
if (!$document instanceof DocumentNode) {
$document = Parser::parse($document);
}
$operation = $params->operation;
$type = AST::getOperation($document, $operation);
if ($params
->isReadOnly() && $type !== 'query') {
throw new RequestError('GET requests are only supported for query operations.');
}
if ($type === 'query') {
return $this
->executeCacheableOperation($adapter, $config, $params, $document, !$persisted);
}
return $this
->executeUncachableOperation($adapter, $config, $params, $document, !$persisted);
} catch (CacheableRequestError $exception) {
return $adapter
->createFulfilled(new QueryResult(NULL, [
Error::createLocatedError($exception),
], [], $exception));
} catch (RequestError $exception) {
return $adapter
->createFulfilled(new QueryResult(NULL, [
Error::createLocatedError($exception),
]));
} catch (Error $exception) {
return $adapter
->createFulfilled(new QueryResult(NULL, [
$exception,
]));
}
}
protected function executeCacheableOperation(PromiseAdapter $adapter, ServerConfig $config, OperationParams $params, DocumentNode $document, $validate = TRUE) {
$contextCacheId = 'ccid:' . $this
->cacheIdentifier($params, $document);
if (!$config
->getDebug() && ($contextCache = $this->cacheBackend
->get($contextCacheId))) {
$contexts = $contextCache->data ?: [];
$cid = 'cid:' . $this
->cacheIdentifier($params, $document, $contexts);
if ($cache = $this->cacheBackend
->get($cid)) {
return $adapter
->createFulfilled($cache->data);
}
}
$result = $this
->doExecuteOperation($adapter, $config, $params, $document, $validate);
return $result
->then(function (QueryResult $result) use ($contextCacheId, $params, $document) {
if ($result
->getCacheMaxAge() !== 0) {
$contexts = $result
->getCacheContexts();
$expire = $this
->maxAgeToExpire($result
->getCacheMaxAge());
$tags = $result
->getCacheTags();
$cid = 'cid:' . $this
->cacheIdentifier($params, $document, $contexts);
$this->cacheBackend
->set($contextCacheId, $contexts, $expire, $tags);
$this->cacheBackend
->set($cid, $result, $expire, $tags);
}
return $result;
});
}
protected function executeUncachableOperation(PromiseAdapter $adapter, ServerConfig $config, OperationParams $params, DocumentNode $document, $validate = TRUE) {
$result = $this
->doExecuteOperation($adapter, $config, $params, $document, $validate);
return $result
->then(function (QueryResult $result) {
$result
->mergeCacheMaxAge(0);
return $result;
});
}
protected function doExecuteOperation(PromiseAdapter $adapter, ServerConfig $config, OperationParams $params, DocumentNode $document, $validate = TRUE) {
if ($validate && ($errors = $this
->validateOperation($config, $params, $document))) {
return $adapter
->createFulfilled(new QueryResult(NULL, $errors));
}
$operation = $params->operation;
$variables = $params->variables;
$context = $this
->resolveContextValue($config, $params, $document, $operation);
$root = $this
->resolveRootValue($config, $params, $document, $operation);
$resolver = $config
->getFieldResolver();
$schema = $config
->getSchema();
$promise = Executor::promiseToExecute($adapter, $schema, $document, $root, $context, $variables, $operation, $resolver);
return $promise
->then(function (ExecutionResult $result) use ($context) {
$metadata = (new CacheableMetadata())
->addCacheContexts($context
->getCacheContexts())
->addCacheTags($context
->getCacheTags())
->setCacheMaxAge($context
->getCacheMaxAge());
if ($context
->getGlobal('development') || !empty($result->errors)) {
$metadata
->setCacheMaxAge(0);
}
return new QueryResult($result->data, $result->errors, $result->extensions, $metadata);
});
}
protected function validateOperationParams(OperationParams $params) {
$errors = (new Helper())
->validateOperationParams($params);
return array_map(function (RequestError $error) {
return Error::createLocatedError($error, NULL, NULL);
}, $errors);
}
protected function validateOperation(ServerConfig $config, OperationParams $params, DocumentNode $document) {
$operation = $params->operation;
if (!($rules = $this
->resolveValidationRules($config, $params, $document, $operation))) {
return [];
}
$schema = $config
->getSchema();
$info = new TypeInfo($schema);
$validation = new ValidationContext($schema, $document, $info);
$visitors = array_values(array_map(function (AbstractValidationRule $rule) use ($validation, $params) {
if ($rule instanceof QueryComplexity && !empty($params->variables)) {
$rule
->setRawVariableValues($params->variables);
}
return $rule($validation);
}, $rules));
Visitor::visit($document, Visitor::visitWithTypeInfo($info, Visitor::visitInParallel($visitors)));
return $validation
->getErrors();
}
protected function resolveRootValue(ServerConfig $config, OperationParams $params, DocumentNode $document, $operation) {
$root = $config
->getRootValue();
if (is_callable($root)) {
$root = $root($params, $document, $operation);
}
return $root;
}
protected function resolveContextValue(ServerConfig $config, OperationParams $params, DocumentNode $document, $operation) {
$context = $config
->getContext();
if (is_callable($context)) {
$context = $context($params, $document, $operation);
}
return $context;
}
protected function resolveValidationRules(ServerConfig $config, OperationParams $params, DocumentNode $document, $operation) {
$rules = $config
->getValidationRules();
if (is_callable($rules)) {
$rules = $rules($params, $document, $operation);
if (!is_array($rules)) {
throw new RequestError(sprintf("Expecting validation rules to be array or callable returning array, but got: %s", Utils::printSafe($rules)));
}
}
return $rules;
}
protected function loadPersistedQuery(ServerConfig $config, OperationParams $params) {
if (!($loader = $config
->getPersistentQueryLoader())) {
throw new RequestError('Persisted queries are not supported by this server.');
}
$source = $loader($params->queryId, $params);
if (!is_string($source) && !$source instanceof DocumentNode) {
throw new RequestError(sprintf('The persisted query loader must return query string or instance of %s but got: %s.', DocumentNode::class, Utils::printSafe($source)));
}
return $source;
}
protected function serializeDocument(DocumentNode $document) {
return $this
->sanitizeRecursive(AST::toArray($document));
}
protected function sanitizeRecursive(array $item) {
unset($item['loc']);
foreach ($item as &$value) {
if (is_array($value)) {
$value = $this
->sanitizeRecursive($value);
}
}
return $item;
}
protected function cacheIdentifier(OperationParams $params, DocumentNode $document, array $contexts = []) {
$contexts = $contexts;
$keys = $this->contextsManager
->convertTokensToKeys($contexts)
->getKeys();
$variables = $params->variables ?: [];
ksort($variables);
$hash = hash('sha256', json_encode([
'query' => $this
->serializeDocument($document),
'variables' => $variables,
]));
return implode(':', array_values(array_merge([
$hash,
], $keys)));
}
protected function filterCacheContexts(array $contexts) {
return array_filter($contexts, function ($context) {
return strpos($context, 'languages:') !== 0;
});
}
protected function maxAgeToExpire($maxAge) {
$time = $this->requestStack
->getMasterRequest()->server
->get('REQUEST_TIME');
return $maxAge === Cache::PERMANENT ? Cache::PERMANENT : (int) $time + $maxAge;
}
}