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