Code Coverage |
||||||||||
Classes and Traits |
Functions and Methods |
Lines |
||||||||
Total | |
0.00% |
0 / 1 |
|
0.00% |
0 / 5 |
CRAP | |
0.00% |
0 / 35 |
EarlyRenderingControllerWrapperSubscriber | |
0.00% |
0 / 1 |
|
0.00% |
0 / 5 |
156 | |
0.00% |
0 / 35 |
__construct | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 3 |
|||
onController | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 5 |
|||
anonymous function | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 2 |
|||
wrapControllerExecutionInRenderContext | |
0.00% |
0 / 1 |
72 | |
0.00% |
0 / 1 |
|||
getSubscribedEvents | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 3 |
<?php | |
/** | |
* @file | |
* Contains \Drupal\Core\EventSubscriber\EarlyRenderingControllerWrapperSubscriber. | |
*/ | |
namespace Drupal\Core\EventSubscriber; | |
use Drupal\Core\Ajax\AjaxResponse; | |
use Drupal\Core\Cache\CacheableDependencyInterface; | |
use Drupal\Core\Cache\CacheableResponseInterface; | |
use Drupal\Core\Controller\ControllerResolverInterface; | |
use Drupal\Core\Render\AttachmentsInterface; | |
use Drupal\Core\Render\BubbleableMetadata; | |
use Drupal\Core\Render\RenderContext; | |
use Drupal\Core\Render\RendererInterface; | |
use Symfony\Component\EventDispatcher\EventSubscriberInterface; | |
use Symfony\Component\HttpKernel\Event\FilterControllerEvent; | |
use Symfony\Component\HttpKernel\KernelEvents; | |
/** | |
* Subscriber that wraps controllers, to handle early rendering. | |
* | |
* When controllers call drupal_render() (RendererInterface::render()) outside | |
* of a render context, we call that "early rendering". Controllers should | |
* return only render arrays, but we cannot prevent controllers from doing early | |
* rendering. The problem with early rendering is that the bubbleable metadata | |
* (cacheability & attachments) are lost. | |
* | |
* This can lead to broken pages (missing assets), stale pages (missing cache | |
* tags causing a page not to be invalidated) or even security problems (missing | |
* cache contexts causing a cached page not to be varied sufficiently). | |
* | |
* This event subscriber wraps all controller executions in a closure that sets | |
* up a render context. Consequently, any early rendering will have their | |
* bubbleable metadata (assets & cacheability) stored on that render context. | |
* | |
* If the render context is empty, then the controller either did not do any | |
* rendering at all, or used the RendererInterface::renderRoot() or | |
* ::renderPlain() methods. In that case, no bubbleable metadata is lost. | |
* | |
* If the render context is not empty, then the controller did use | |
* drupal_render(), and bubbleable metadata was collected. This bubbleable | |
* metadata is then merged onto the render array. | |
* | |
* In other words: this just exists to ease the transition to Drupal 8: it | |
* allows controllers that return render arrays (the majority) and | |
* \Drupal\Core\Ajax\AjaxResponse\AjaxResponse objects (a sizable minority that | |
* often involve a fair amount of rendering) to still do early rendering. But | |
* controllers that return any other kind of response are already expected to | |
* do the right thing, so if early rendering is detected in such a case, an | |
* exception is thrown. | |
* | |
* @see \Drupal\Core\Render\RendererInterface | |
* @see \Drupal\Core\Render\Renderer | |
* | |
* @todo Remove in Drupal 9.0.0, by disallowing early rendering. | |
*/ | |
class EarlyRenderingControllerWrapperSubscriber implements EventSubscriberInterface { | |
/** | |
* The controller resolver. | |
* | |
* @var \Drupal\Core\Controller\ControllerResolverInterface | |
*/ | |
protected $controllerResolver; | |
/** | |
* The renderer. | |
* | |
* @var \Drupal\Core\Render\RendererInterface | |
*/ | |
protected $renderer; | |
/** | |
* Constructs a new EarlyRenderingControllerWrapperSubscriber instance. | |
* | |
* @param \Drupal\Core\Controller\ControllerResolverInterface $controller_resolver | |
* The controller resolver. | |
* @param \Drupal\Core\Render\RendererInterface $renderer | |
* The renderer. | |
*/ | |
public function __construct(ControllerResolverInterface $controller_resolver, RendererInterface $renderer) { | |
$this->controllerResolver = $controller_resolver; | |
$this->renderer = $renderer; | |
} | |
/** | |
* Ensures bubbleable metadata from early rendering is not lost. | |
* | |
* @param \Symfony\Component\HttpKernel\Event\FilterControllerEvent $event | |
* The controller event. | |
*/ | |
public function onController(FilterControllerEvent $event) { | |
$controller = $event->getController(); | |
// See \Symfony\Component\HttpKernel\HttpKernel::handleRaw(). | |
$arguments = $this->controllerResolver->getArguments($event->getRequest(), $controller); | |
$event->setController(function() use ($controller, $arguments) { | |
return $this->wrapControllerExecutionInRenderContext($controller, $arguments); | |
}); | |
} | |
/** | |
* Wraps a controller execution in a render context. | |
* | |
* @param callable $controller | |
* The controller to execute. | |
* @param array $arguments | |
* The arguments to pass to the controller. | |
* | |
* @return mixed | |
* The return value of the controller. | |
* | |
* @throws \LogicException | |
* When early rendering has occurred in a controller that returned a | |
* Response or domain object that cares about attachments or cacheability. | |
* | |
* @see \Symfony\Component\HttpKernel\HttpKernel::handleRaw() | |
*/ | |
protected function wrapControllerExecutionInRenderContext($controller, array $arguments) { | |
$context = new RenderContext(); | |
$response = $this->renderer->executeInRenderContext($context, function() use ($controller, $arguments) { | |
// Now call the actual controller, just like HttpKernel does. | |
return call_user_func_array($controller, $arguments); | |
}); | |
// If early rendering happened, i.e. if code in the controller called | |
// drupal_render() outside of a render context, then the bubbleable metadata | |
// for that is stored in the current render context. | |
if (!$context->isEmpty()) { | |
/** @var \Drupal\Core\Render\BubbleableMetadata $early_rendering_bubbleable_metadata */ | |
$early_rendering_bubbleable_metadata = $context->pop(); | |
// If a render array or AjaxResponse is returned by the controller, merge | |
// the "lost" bubbleable metadata. | |
if (is_array($response)) { | |
BubbleableMetadata::createFromRenderArray($response) | |
->merge($early_rendering_bubbleable_metadata) | |
->applyTo($response); | |
} | |
elseif ($response instanceof AjaxResponse) { | |
$response->addAttachments($early_rendering_bubbleable_metadata->getAttachments()); | |
// @todo Make AjaxResponse cacheable in | |
// https://www.drupal.org/node/956186. Meanwhile, allow contrib | |
// subclasses to be. | |
if ($response instanceof CacheableResponseInterface) { | |
$response->addCacheableDependency($early_rendering_bubbleable_metadata); | |
} | |
} | |
// If a non-Ajax Response or domain object is returned and it cares about | |
// attachments or cacheability, then throw an exception: early rendering | |
// is not permitted in that case. It is the developer's responsibility | |
// to not use early rendering. | |
elseif ($response instanceof AttachmentsInterface || $response instanceof CacheableResponseInterface || $response instanceof CacheableDependencyInterface) { | |
throw new \LogicException(sprintf('The controller result claims to be providing relevant cache metadata, but leaked metadata was detected. Please ensure you are not rendering content too early. Returned object class: %s.', get_class($response))); | |
} | |
else { | |
// A Response or domain object is returned that does not care about | |
// attachments nor cacheability; for instance, a RedirectResponse. It is | |
// safe to discard any early rendering metadata. | |
} | |
} | |
return $response; | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public static function getSubscribedEvents() { | |
$events[KernelEvents::CONTROLLER][] = ['onController']; | |
return $events; | |
} | |
} |