Mercurial > hg > isophonics-drupal-site
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 } |