123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175 |
- <?php
- namespace Drupal\Core\EventSubscriber;
- use Drupal\Core\Ajax\AjaxResponse;
- use Drupal\Core\Cache\CacheableDependencyInterface;
- use Drupal\Core\Cache\CacheableResponseInterface;
- 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\Controller\ArgumentResolverInterface;
- 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 argument resolver.
- *
- * @var \Symfony\Component\HttpKernel\Controller\ArgumentResolverInterface
- */
- protected $argumentResolver;
- /**
- * The renderer.
- *
- * @var \Drupal\Core\Render\RendererInterface
- */
- protected $renderer;
- /**
- * Constructs a new EarlyRenderingControllerWrapperSubscriber instance.
- *
- * @param \Symfony\Component\HttpKernel\Controller\ArgumentResolverInterface $argument_resolver
- * The argument resolver.
- * @param \Drupal\Core\Render\RendererInterface $renderer
- * The renderer.
- */
- public function __construct(ArgumentResolverInterface $argument_resolver, RendererInterface $renderer) {
- $this->argumentResolver = $argument_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->argumentResolver->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;
- }
- }
|