3 namespace Drupal\Core\EventSubscriber;
5 use Drupal\Core\Ajax\AjaxResponse;
6 use Drupal\Core\Cache\CacheableDependencyInterface;
7 use Drupal\Core\Cache\CacheableResponseInterface;
8 use Drupal\Core\Controller\ControllerResolverInterface;
9 use Drupal\Core\Render\AttachmentsInterface;
10 use Drupal\Core\Render\BubbleableMetadata;
11 use Drupal\Core\Render\RenderContext;
12 use Drupal\Core\Render\RendererInterface;
13 use Symfony\Component\EventDispatcher\EventSubscriberInterface;
14 use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
15 use Symfony\Component\HttpKernel\KernelEvents;
18 * Subscriber that wraps controllers, to handle early rendering.
20 * When controllers call drupal_render() (RendererInterface::render()) outside
21 * of a render context, we call that "early rendering". Controllers should
22 * return only render arrays, but we cannot prevent controllers from doing early
23 * rendering. The problem with early rendering is that the bubbleable metadata
24 * (cacheability & attachments) are lost.
26 * This can lead to broken pages (missing assets), stale pages (missing cache
27 * tags causing a page not to be invalidated) or even security problems (missing
28 * cache contexts causing a cached page not to be varied sufficiently).
30 * This event subscriber wraps all controller executions in a closure that sets
31 * up a render context. Consequently, any early rendering will have their
32 * bubbleable metadata (assets & cacheability) stored on that render context.
34 * If the render context is empty, then the controller either did not do any
35 * rendering at all, or used the RendererInterface::renderRoot() or
36 * ::renderPlain() methods. In that case, no bubbleable metadata is lost.
38 * If the render context is not empty, then the controller did use
39 * drupal_render(), and bubbleable metadata was collected. This bubbleable
40 * metadata is then merged onto the render array.
42 * In other words: this just exists to ease the transition to Drupal 8: it
43 * allows controllers that return render arrays (the majority) and
44 * \Drupal\Core\Ajax\AjaxResponse\AjaxResponse objects (a sizable minority that
45 * often involve a fair amount of rendering) to still do early rendering. But
46 * controllers that return any other kind of response are already expected to
47 * do the right thing, so if early rendering is detected in such a case, an
48 * exception is thrown.
50 * @see \Drupal\Core\Render\RendererInterface
51 * @see \Drupal\Core\Render\Renderer
53 * @todo Remove in Drupal 9.0.0, by disallowing early rendering.
55 class EarlyRenderingControllerWrapperSubscriber implements EventSubscriberInterface {
58 * The controller resolver.
60 * @var \Drupal\Core\Controller\ControllerResolverInterface
62 protected $controllerResolver;
67 * @var \Drupal\Core\Render\RendererInterface
72 * Constructs a new EarlyRenderingControllerWrapperSubscriber instance.
74 * @param \Drupal\Core\Controller\ControllerResolverInterface $controller_resolver
75 * The controller resolver.
76 * @param \Drupal\Core\Render\RendererInterface $renderer
79 public function __construct(ControllerResolverInterface $controller_resolver, RendererInterface $renderer) {
80 $this->controllerResolver = $controller_resolver;
81 $this->renderer = $renderer;
85 * Ensures bubbleable metadata from early rendering is not lost.
87 * @param \Symfony\Component\HttpKernel\Event\FilterControllerEvent $event
88 * The controller event.
90 public function onController(FilterControllerEvent $event) {
91 $controller = $event->getController();
93 // See \Symfony\Component\HttpKernel\HttpKernel::handleRaw().
94 $arguments = $this->controllerResolver->getArguments($event->getRequest(), $controller);
96 $event->setController(function () use ($controller, $arguments) {
97 return $this->wrapControllerExecutionInRenderContext($controller, $arguments);
102 * Wraps a controller execution in a render context.
104 * @param callable $controller
105 * The controller to execute.
106 * @param array $arguments
107 * The arguments to pass to the controller.
110 * The return value of the controller.
112 * @throws \LogicException
113 * When early rendering has occurred in a controller that returned a
114 * Response or domain object that cares about attachments or cacheability.
116 * @see \Symfony\Component\HttpKernel\HttpKernel::handleRaw()
118 protected function wrapControllerExecutionInRenderContext($controller, array $arguments) {
119 $context = new RenderContext();
121 $response = $this->renderer->executeInRenderContext($context, function () use ($controller, $arguments) {
122 // Now call the actual controller, just like HttpKernel does.
123 return call_user_func_array($controller, $arguments);
126 // If early rendering happened, i.e. if code in the controller called
127 // drupal_render() outside of a render context, then the bubbleable metadata
128 // for that is stored in the current render context.
129 if (!$context->isEmpty()) {
130 /** @var \Drupal\Core\Render\BubbleableMetadata $early_rendering_bubbleable_metadata */
131 $early_rendering_bubbleable_metadata = $context->pop();
133 // If a render array or AjaxResponse is returned by the controller, merge
134 // the "lost" bubbleable metadata.
135 if (is_array($response)) {
136 BubbleableMetadata::createFromRenderArray($response)
137 ->merge($early_rendering_bubbleable_metadata)
138 ->applyTo($response);
140 elseif ($response instanceof AjaxResponse) {
141 $response->addAttachments($early_rendering_bubbleable_metadata->getAttachments());
142 // @todo Make AjaxResponse cacheable in
143 // https://www.drupal.org/node/956186. Meanwhile, allow contrib
145 if ($response instanceof CacheableResponseInterface) {
146 $response->addCacheableDependency($early_rendering_bubbleable_metadata);
149 // If a non-Ajax Response or domain object is returned and it cares about
150 // attachments or cacheability, then throw an exception: early rendering
151 // is not permitted in that case. It is the developer's responsibility
152 // to not use early rendering.
153 elseif ($response instanceof AttachmentsInterface || $response instanceof CacheableResponseInterface || $response instanceof CacheableDependencyInterface) {
154 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)));
157 // A Response or domain object is returned that does not care about
158 // attachments nor cacheability; for instance, a RedirectResponse. It is
159 // safe to discard any early rendering metadata.
169 public static function getSubscribedEvents() {
170 $events[KernelEvents::CONTROLLER][] = ['onController'];