diff --git a/src/Controller/SubrequestExtractionController.php b/src/Controller/SubrequestExtractionController.php index 68f223938..ab70fe2e9 100644 --- a/src/Controller/SubrequestExtractionController.php +++ b/src/Controller/SubrequestExtractionController.php @@ -2,9 +2,9 @@ namespace Drupal\graphql\Controller; -use Drupal\Core\Cache\CacheableMetadata; use Drupal\Core\Controller\ControllerBase; use Drupal\Core\Language\LanguageManagerInterface; +use Drupal\Core\Render\RenderContext; use Drupal\Core\Render\RendererInterface; use Drupal\graphql\SubRequestResponse; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -70,9 +70,19 @@ public function extract() { $request = $this->requestStack->getCurrentRequest(); $callback = $request->attributes->get('_graphql_subrequest'); - $metadata = new CacheableMetadata(); - $response = new SubRequestResponse($callback($metadata)); - $response->addCacheableDependency($metadata); + // @todo Remove this once https://www.drupal.org/project/drupal/issues/2940036#comment-12479912 is resolved. + $this->languageManager->reset(); + + // Collect any potentially leaked cache metadata released by the callback. + $context = new RenderContext(); + $result = $this->renderer->executeInRenderContext($context, function () use ($callback) { + return $callback(); + }); + + $response = new SubRequestResponse($result); + if (!$context->isEmpty()) { + $response->addCacheableDependency($context->pop()); + } return $response; } diff --git a/src/GraphQL/Buffers/SubRequestBuffer.php b/src/GraphQL/Buffers/SubRequestBuffer.php new file mode 100644 index 000000000..c29c1b43e --- /dev/null +++ b/src/GraphQL/Buffers/SubRequestBuffer.php @@ -0,0 +1,152 @@ +httpKernel = $httpKernel; + $this->requestStack = $requestStack; + } + + /** + * Add an item to the buffer. + * + * @param \Drupal\Core\Url $url + * The url object to run the subrequest on. + * @param callable $extract + * The callback to run within the sub-request. + * + * @return \Closure + * The callback to invoke to load the result for this buffer item. + */ + public function add(Url $url, callable $extract) { + $item = new \ArrayObject([ + 'url' => $url, + 'extract' => $extract, + ]); + + return $this->createBufferResolver($item); + } + + /** + * {@inheritdoc} + */ + protected function getBufferId($item) { + /** @var \Drupal\Core\GeneratedUrl $url */ + $url = $item['url']->toString(TRUE); + + return hash('sha256', json_encode([ + 'url' => $url->getGeneratedUrl(), + 'tags' => $url->getCacheTags(), + 'contexts' => $url->getCacheContexts(), + 'age' => $url->getCacheMaxAge(), + ])); + } + + /** + * Create a sub-request for the given url. + * + * @param \Symfony\Component\HttpFoundation\Request $current + * The current main request. + * @param array $buffer + * The buffer. + * @param string $url + * The url to run the subrequest on. + * + * @return \Symfony\Component\HttpFoundation\Request + * The request object. + */ + protected function createRequest(Request $current, array $buffer, string $url) { + $request = Request::create( + $url, + 'GET', + $current->query->all(), + $current->cookies->all(), + $current->files->all(), + $current->server->all() + ); + + $request->attributes->set('_graphql_subrequest', function () use ($buffer) { + return array_map(function ($item) { + return $item['extract']($item['url']); + }, $buffer); + }); + + if ($session = $current->getSession()) { + $request->setSession($session); + } + + return $request; + } + + /** + * {@inheritdoc} + */ + public function resolveBufferArray(array $buffer) { + /** @var \Drupal\Core\GeneratedUrl $url */ + $url = reset($buffer)['url']->toString(TRUE); + + $current = $this->requestStack->getCurrentRequest(); + $target = $url->getGeneratedUrl(); + $request = $this->createRequest($current, $buffer, $target); + + /** @var \Drupal\graphql\SubRequestResponse $response */ + $response = $this->httpKernel->handle($request, HttpKernelInterface::SUB_REQUEST); + while ($response instanceof LocalRedirectResponse) { + $target = $response->getTargetUrl(); + $request = $this->createRequest($current, $buffer, $target); + $response = $this->httpKernel->handle($request, HttpKernelInterface::SUB_REQUEST); + } + + if (!($response instanceof SubRequestResponse)) { + return array_fill_keys(array_keys($buffer), NULL); + } + + // @todo Remove the request stack manipulation once the core issue described at + // https://www.drupal.org/node/2613044 is resolved. + while ($this->requestStack->getCurrentRequest() !== $current) { + $this->requestStack->pop(); + } + + if ($url instanceof CacheableDependencyInterface) { + $response->addCacheableDependency($url); + } + + return $response->getResult(); + } + +}