comparison core/lib/Drupal/Core/EventSubscriber/EarlyRenderingControllerWrapperSubscriber.php @ 0:4c8ae668cc8c

Initial import (non-working)
author Chris Cannam
date Wed, 29 Nov 2017 16:09:58 +0000
parents
children 129ea1e6d783
comparison
equal deleted inserted replaced
-1:000000000000 0:4c8ae668cc8c
1 <?php
2
3 namespace Drupal\Core\EventSubscriber;
4
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;
16
17 /**
18 * Subscriber that wraps controllers, to handle early rendering.
19 *
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.
25 *
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).
29 *
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.
33 *
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.
37 *
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.
41 *
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.
49 *
50 * @see \Drupal\Core\Render\RendererInterface
51 * @see \Drupal\Core\Render\Renderer
52 *
53 * @todo Remove in Drupal 9.0.0, by disallowing early rendering.
54 */
55 class EarlyRenderingControllerWrapperSubscriber implements EventSubscriberInterface {
56
57 /**
58 * The controller resolver.
59 *
60 * @var \Drupal\Core\Controller\ControllerResolverInterface
61 */
62 protected $controllerResolver;
63
64 /**
65 * The renderer.
66 *
67 * @var \Drupal\Core\Render\RendererInterface
68 */
69 protected $renderer;
70
71 /**
72 * Constructs a new EarlyRenderingControllerWrapperSubscriber instance.
73 *
74 * @param \Drupal\Core\Controller\ControllerResolverInterface $controller_resolver
75 * The controller resolver.
76 * @param \Drupal\Core\Render\RendererInterface $renderer
77 * The renderer.
78 */
79 public function __construct(ControllerResolverInterface $controller_resolver, RendererInterface $renderer) {
80 $this->controllerResolver = $controller_resolver;
81 $this->renderer = $renderer;
82 }
83
84 /**
85 * Ensures bubbleable metadata from early rendering is not lost.
86 *
87 * @param \Symfony\Component\HttpKernel\Event\FilterControllerEvent $event
88 * The controller event.
89 */
90 public function onController(FilterControllerEvent $event) {
91 $controller = $event->getController();
92
93 // See \Symfony\Component\HttpKernel\HttpKernel::handleRaw().
94 $arguments = $this->controllerResolver->getArguments($event->getRequest(), $controller);
95
96 $event->setController(function () use ($controller, $arguments) {
97 return $this->wrapControllerExecutionInRenderContext($controller, $arguments);
98 });
99 }
100
101 /**
102 * Wraps a controller execution in a render context.
103 *
104 * @param callable $controller
105 * The controller to execute.
106 * @param array $arguments
107 * The arguments to pass to the controller.
108 *
109 * @return mixed
110 * The return value of the controller.
111 *
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.
115 *
116 * @see \Symfony\Component\HttpKernel\HttpKernel::handleRaw()
117 */
118 protected function wrapControllerExecutionInRenderContext($controller, array $arguments) {
119 $context = new RenderContext();
120
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);
124 });
125
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();
132
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);
139 }
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
144 // subclasses to be.
145 if ($response instanceof CacheableResponseInterface) {
146 $response->addCacheableDependency($early_rendering_bubbleable_metadata);
147 }
148 }
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)));
155 }
156 else {
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.
160 }
161 }
162
163 return $response;
164 }
165
166 /**
167 * {@inheritdoc}
168 */
169 public static function getSubscribedEvents() {
170 $events[KernelEvents::CONTROLLER][] = ['onController'];
171
172 return $events;
173 }
174
175 }