Issue #2450993 by Wim Leers, Fabianx, Crell, dawehner, effulgentsia: Rendered Cache Metadata created during the main controller request gets lost

8.0.x
Alex Pott 2015-07-05 19:00:05 +01:00
parent 2dbda2635a
commit ebb21d2804
32 changed files with 1289 additions and 199 deletions

View File

@ -1392,6 +1392,11 @@ services:
renderer:
class: Drupal\Core\Render\Renderer
arguments: ['@controller_resolver', '@theme.manager', '@plugin.manager.element_info', '@render_cache', '%renderer.config%']
early_rendering_controller_wrapper_subscriber:
class: Drupal\Core\EventSubscriber\EarlyRenderingControllerWrapperSubscriber
arguments: ['@controller_resolver', '@renderer']
tags:
- { name: event_subscriber }
email.validator:
class: Egulias\EmailValidator\EmailValidator

View File

@ -0,0 +1,166 @@
<?php
/**
* @file
* Contains \Drupal\Core\EventSubscriber\EarlyRenderingControllerWrapperSubscriber.
*/
namespace Drupal\Core\EventSubscriber;
use Drupal\Core\Cache\CacheableDependencyInterface;
use Drupal\Core\Cache\CacheableResponseInterface;
use Drupal\Core\Controller\ControllerResolverInterface;
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\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) to still do early
* rendering. But controllers that return responses are already expected to do
* the right thing: 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 controller resolver.
*
* @var \Drupal\Core\Controller\ControllerResolverInterface
*/
protected $controllerResolver;
/**
* The renderer.
*
* @var \Drupal\Core\Render\RendererInterface
*/
protected $renderer;
/**
* Constructs a new EarlyRenderingControllerWrapperSubscriber instance.
*
* @param \Drupal\Core\Controller\ControllerResolverInterface $controller_resolver
* The controller resolver.
* @param \Drupal\Core\Render\RendererInterface $renderer
* The renderer.
*/
public function __construct(ControllerResolverInterface $controller_resolver, RendererInterface $renderer) {
$this->controllerResolver = $controller_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->controllerResolver->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()) {
// If a render array is returned by the controller, merge the "lost"
// bubbleable metadata.
if (is_array($response)) {
$early_rendering_bubbleable_metadata = $context->pop();
BubbleableMetadata::createFromRenderArray($response)
->merge($early_rendering_bubbleable_metadata)
->applyTo($response);
}
// If a 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. E.g. a RedirectResponse. It is safe to
// discard any early rendering metadata.
}
}
return $response;
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
$events[KernelEvents::CONTROLLER][] = ['onController'];
return $events;
}
}

View File

@ -14,6 +14,7 @@ use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Render\HtmlResponse;
use Drupal\Core\Render\PageDisplayVariantSelectionEvent;
use Drupal\Core\Render\RenderCacheInterface;
use Drupal\Core\Render\RenderContext;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\Render\RenderEvents;
use Drupal\Core\Routing\RouteMatchInterface;
@ -181,7 +182,9 @@ class HtmlRenderer implements MainContentRendererInterface {
// ::renderResponse().
// @todo Remove this once https://www.drupal.org/node/2359901 lands.
if (!empty($main_content)) {
$this->renderer->render($main_content, FALSE);
$this->renderer->executeInRenderContext(new RenderContext(), function() use (&$main_content) {
return $this->renderer->render($main_content, FALSE);
});
$main_content = $this->renderCache->getCacheableRenderArray($main_content) + [
'#title' => isset($main_content['#title']) ? $main_content['#title'] : NULL
];

View File

@ -0,0 +1,62 @@
<?php
/**
* @file
* Contains \Drupal\Core\Render\RenderContext.
*/
namespace Drupal\Core\Render;
/**
* The render context: a stack containing bubbleable rendering metadata.
*
* A stack of \Drupal\Core\Render\BubbleableMetadata objects.
*
* @see \Drupal\Core\Render\RendererInterface
* @see \Drupal\Core\Render\Renderer
* @see \Drupal\Core\Render\BubbleableMetadata
*
* @internal
*/
class RenderContext extends \SplStack {
/**
* Updates the current frame of the stack.
*
* @param array &$element
* The element of the render array that has just been rendered. The stack
* frame for this element will be updated with the bubbleable rendering
* metadata of this element.
*/
public function update(&$element) {
// The latest frame represents the bubbleable metadata for the subtree.
$frame = $this->pop();
// Update the frame, but also update the current element, to ensure it
// contains up-to-date information in case it gets render cached.
$updated_frame = BubbleableMetadata::createFromRenderArray($element)->merge($frame);
$updated_frame->applyTo($element);
$this->push($updated_frame);
}
/**
* Bubbles the stack.
*
* Whenever another level in the render array has been rendered, the stack
* must be bubbled, to merge its rendering metadata with that of the parent
* element.
*/
public function bubble() {
// If there's only one frame on the stack, then this is the root call, and
// we can't bubble up further. ::renderRoot() will reset the stack, but we
// must not reset it here to allow users of ::executeInRenderContext() to
// access the stack directly.
if ($this->count() === 1) {
return;
}
// Merge the current and the parent stack frame.
$current = $this->pop();
$parent = $this->pop();
$this->push($current->merge($parent));
}
}

View File

@ -58,11 +58,24 @@ class Renderer implements RendererInterface {
protected $rendererConfig;
/**
* The stack containing bubbleable rendering metadata.
* The render context.
*
* @var \SplStack|null
* This must be static as long as some controllers rebuild the container
* during a request. This causes multiple renderer instances to co-exist
* simultaneously, render state getting lost, and therefore causing pages to
* fail to render correctly. As soon as it is guaranteed that during a request
* the same container is used, it no longer needs to be static.
*
* @var \Drupal\Core\Render\RenderContext|null
*/
protected static $stack;
protected static $context;
/**
* Whether we're currently in a ::renderRoot() call.
*
* @var bool
*/
protected $isRenderingRoot = FALSE;
/**
* Constructs a new Renderer.
@ -90,18 +103,29 @@ class Renderer implements RendererInterface {
* {@inheritdoc}
*/
public function renderRoot(&$elements) {
return $this->render($elements, TRUE);
// Disallow calling ::renderRoot() from within another ::renderRoot() call.
if ($this->isRenderingRoot) {
$this->isRenderingRoot = FALSE;
throw new \LogicException('A stray renderRoot() invocation is causing bubbling of attached assets to break.');
}
// Render in its own render context.
$this->isRenderingRoot = TRUE;
$output = $this->executeInRenderContext(new RenderContext(), function () use (&$elements) {
return $this->render($elements, TRUE);
});
$this->isRenderingRoot = FALSE;
return $output;
}
/**
* {@inheritdoc}
*/
public function renderPlain(&$elements) {
$current_stack = static::$stack;
$this->resetStack();
$output = $this->renderRoot($elements);
static::$stack = $current_stack;
return $output;
return $this->executeInRenderContext(new RenderContext(), function () use (&$elements) {
return $this->render($elements, TRUE);
});
}
/**
@ -151,16 +175,17 @@ class Renderer implements RendererInterface {
// possible that any of them throw an exception that will cause a different
// page to be rendered (e.g. throwing
// \Symfony\Component\HttpKernel\Exception\NotFoundHttpException will cause
// the 404 page to be rendered). That page might also use Renderer::render()
// but if exceptions aren't caught here, the stack will be left in an
// inconsistent state.
// Hence, catch all exceptions and reset the stack and re-throw them.
// the 404 page to be rendered). That page might also use
// Renderer::renderRoot() but if exceptions aren't caught here, it will be
// impossible to call Renderer::renderRoot() again.
// Hence, catch all exceptions, reset the isRenderingRoot property and
// re-throw exceptions.
try {
return $this->doRender($elements, $is_root_call);
}
catch (\Exception $e) {
// Reset stack and re-throw exception.
$this->resetStack();
// Mark the ::rootRender() call finished due to this exception & re-throw.
$this->isRenderingRoot = FALSE;
throw $e;
}
}
@ -200,10 +225,10 @@ class Renderer implements RendererInterface {
return '';
}
if (!isset(static::$stack)) {
static::$stack = new \SplStack();
if (!isset(static::$context)) {
throw new \LogicException("Render context is empty, because render() was called outside of a renderRoot() or renderPlain() call. Use renderPlain()/renderRoot() or #lazy_builder/#pre_render instead.");
}
static::$stack->push(new BubbleableMetadata());
static::$context->push(new BubbleableMetadata());
// Set the bubbleable rendering metadata that has configurable defaults, if:
// - this is the root call, to ensure that the final render array definitely
@ -244,10 +269,10 @@ class Renderer implements RendererInterface {
}
// The render cache item contains all the bubbleable rendering metadata
// for the subtree.
$this->updateStack($elements);
static::$context->update($elements);
// Render cache hit, so rendering is finished, all necessary info
// collected!
$this->bubbleStack();
static::$context->bubble();
return $elements['#markup'];
}
}
@ -345,9 +370,9 @@ class Renderer implements RendererInterface {
if (!empty($elements['#printed'])) {
// The #printed element contains all the bubbleable rendering metadata for
// the subtree.
$this->updateStack($elements);
static::$context->update($elements);
// #printed, so rendering is finished, all necessary info collected!
$this->bubbleStack();
static::$context->bubble();
return '';
}
@ -473,8 +498,8 @@ class Renderer implements RendererInterface {
$elements['#markup'] = $prefix . $elements['#children'] . $suffix;
// We've rendered this element (and its subtree!), now update the stack.
$this->updateStack($elements);
// We've rendered this element (and its subtree!), now update the context.
static::$context->update($elements);
// Cache the processed element if both $pre_bubbling_elements and $elements
// have the metadata necessary to generate a cache ID.
@ -496,13 +521,14 @@ class Renderer implements RendererInterface {
// that is handled earlier in Renderer::render().
if ($is_root_call) {
$this->replacePlaceholders($elements);
if (static::$stack->count() !== 1) {
// @todo remove as part of https://www.drupal.org/node/2511330.
if (static::$context->count() !== 1) {
throw new \LogicException('A stray drupal_render() invocation with $is_root_call = TRUE is causing bubbling of attached assets to break.');
}
}
// Rendering is finished, all necessary info collected!
$this->bubbleStack();
static::$context->bubble();
$elements['#printed'] = TRUE;
$elements['#markup'] = SafeMarkup::set($elements['#markup']);
@ -510,52 +536,24 @@ class Renderer implements RendererInterface {
}
/**
* Resets the renderer service's internal stack (used for bubbling metadata).
*
* Only necessary in very rare/advanced situations, such as when rendering an
* error page if an exception occurred *during* rendering.
* {@inheritdoc}
*/
protected function resetStack() {
static::$stack = NULL;
}
public function executeInRenderContext(RenderContext $context, callable $callable) {
// Store the current render context.
$current_context = static::$context;
/**
* Updates the stack.
*
* @param array &$element
* The element of the render array that has just been rendered. The stack
* frame for this element will be updated with the bubbleable rendering
* metadata of this element.
*/
protected function updateStack(&$element) {
// The latest frame represents the bubbleable metadata for the subtree.
$frame = static::$stack->pop();
// Update the frame, but also update the current element, to ensure it
// contains up-to-date information in case it gets render cached.
$updated_frame = BubbleableMetadata::createFromRenderArray($element)->merge($frame);
$updated_frame->applyTo($element);
static::$stack->push($updated_frame);
}
/**
* Bubbles the stack.
*
* Whenever another level in the render array has been rendered, the stack
* must be bubbled, to merge its rendering metadata with that of the parent
* element.
*/
protected function bubbleStack() {
// If there's only one frame on the stack, then this is the root call, and
// we can't bubble up further. Reset the stack for the next root call.
if (static::$stack->count() === 1) {
$this->resetStack();
return;
// Set the provided context and call the callable, it will use that context.
static::$context = $context;
$result = $callable();
// @todo Convert to an assertion in https://www.drupal.org/node/2408013
if (static::$context->count() > 1) {
throw new \LogicException('Bubbling failed.');
}
// Merge the current and the parent stack frame.
$current = static::$stack->pop();
$parent = static::$stack->pop();
static::$stack->push($current->merge($parent));
// Restore the original render context.
static::$context = $current_context;
return $result;
}
/**

View File

@ -22,6 +22,8 @@ interface RendererInterface {
* - system internals that are responsible for rendering the final HTML
* - render arrays for non-HTML responses, such as feeds
*
* (Cannot be executed within another render context.)
*
* @param array $elements
* The structured array describing the data to be rendered.
*
@ -29,6 +31,9 @@ interface RendererInterface {
* The rendered HTML.
*
* @see ::render()
*
* @throws \LogicException
* When called from inside another renderRoot() call.
*/
public function renderRoot(&$elements);
@ -45,9 +50,11 @@ interface RendererInterface {
* ::renderRoot() call, but that is generally highly problematic (and hence an
* exception is thrown when a ::renderRoot() call happens within another
* ::renderRoot() call). However, in this case, we only care about the output,
* not about the bubbling. Hence this uses a separate render stack, to not
* not about the bubbling. Hence this uses a separate render context, to not
* affect the parent ::renderRoot() call.
*
* (Can be executed within another render context: it runs in isolation.)
*
* @param array $elements
* The structured array describing the data to be rendered.
*
@ -88,8 +95,8 @@ interface RendererInterface {
* or configuration that can affect that rendering changes.
* - Placeholders, with associated self-contained placeholder render arrays,
* for executing code to handle dynamic requirements that cannot be cached.
* A stack of \Drupal\Core\Render\BubbleableMetadata objects can be used to
* perform this bubbling.
* A render context (\Drupal\Core\Render\RenderContext) can be used to perform
* bubbling; it is a stack of \Drupal\Core\Render\BubbleableMetadata objects.
*
* Additionally, whether retrieving from cache or not, it is important to
* know all of the assets (CSS and JavaScript) required by the rendered HTML,
@ -103,9 +110,9 @@ interface RendererInterface {
* - If this element has already been printed (#printed = TRUE) or the user
* does not have access to it (#access = FALSE), then an empty string is
* returned.
* - If no stack data structure has been created yet, it is done now. Next,
* - If no render context is set yet, an exception is thrown. Otherwise,
* an empty \Drupal\Core\Render\BubbleableMetadata is pushed onto the
* stack.
* render context.
* - If this element has #cache defined then the cached markup for this
* element will be returned if it exists in Renderer::render()'s cache. To
* use Renderer::render() caching, set the element's #cache property to an
@ -299,13 +306,12 @@ interface RendererInterface {
* The rendered HTML.
*
* @throws \LogicException
* If a root call to ::render() does not result in an empty stack, this
* indicates an erroneous ::render() root call (a root call within a
* root call, which makes no sense). Therefore, a logic exception is thrown.
* When called outside of a render context. (i.e. outside of a renderRoot(),
* renderPlain() or executeInRenderContext() call.)
* @throws \Exception
* If a #pre_render callback throws an exception, it is caught to reset the
* stack used for bubbling rendering metadata, and then the exception is re-
* thrown.
* If a #pre_render callback throws an exception, it is caught to mark the
* renderer as no longer being in a root render call, if any. Then the
* exception is rethrown.
*
* @see \Drupal\Core\Render\ElementInfoManagerInterface::getInfo()
* @see \Drupal\Core\Theme\ThemeManagerInterface::render()
@ -315,6 +321,37 @@ interface RendererInterface {
*/
public function render(&$elements, $is_root_call = FALSE);
/**
* Executes a callable within a render context.
*
* Only for very advanced use cases. Prefer using ::renderRoot() and
* ::renderPlain() instead.
*
* All rendering must happen within a render context. Within a render context,
* all bubbleable metadata is bubbled and hence tracked. Outside of a render
* context, it would be lost. This could lead to missing assets, incorrect
* cache variations (and thus security issues), insufficient cache
* invalidations, and so on.
*
* Any and all rendering must therefore happen within a render context, and it
* is this method that provides that.
*
* @see \Drupal\Core\Render\BubbleableMetadata
*
* @param \Drupal\Core\Render\RenderContext $context
* The render context to execute the callable within.
* @param callable $callable
* The callable to execute.
* @return mixed
* The callable's return value.
*
* @see \Drupal\Core\Render\RenderContext
*
* @throws \LogicException
* In case bubbling has failed, can only happen in case of broken code.
*/
public function executeInRenderContext(RenderContext $context, callable $callable);
/**
* Merges the bubbleable rendering metadata o/t 2nd render array with the 1st.
*

View File

@ -7,6 +7,7 @@
namespace Drupal\aggregator\Tests\Views;
use Drupal\Core\Render\RenderContext;
use Drupal\Core\Url;
use Drupal\views\Views;
use Drupal\views\Tests\ViewTestData;
@ -66,6 +67,9 @@ class IntegrationTest extends ViewUnitTestBase {
* Tests basic aggregator_item view.
*/
public function testAggregatorItemView() {
/** @var \Drupal\Core\Render\RendererInterface $renderer */
$renderer = \Drupal::service('renderer');
$feed = $this->feedStorage->create(array(
'title' => $this->randomMachineName(),
'url' => 'https://www.drupal.org/',
@ -112,13 +116,22 @@ class IntegrationTest extends ViewUnitTestBase {
foreach ($view->result as $row) {
$iid = $view->field['iid']->getValue($row);
$expected_link = \Drupal::l($items[$iid]->getTitle(), Url::fromUri($items[$iid]->getLink(), ['absolute' => TRUE]));
$this->assertEqual($view->field['title']->advancedRender($row), $expected_link, 'Ensure the right link is generated');
$output = $renderer->executeInRenderContext(new RenderContext(), function () use ($view, $row) {
return $view->field['title']->advancedRender($row);
});
$this->assertEqual($output, $expected_link, 'Ensure the right link is generated');
$expected_author = aggregator_filter_xss($items[$iid]->getAuthor());
$this->assertEqual($view->field['author']->advancedRender($row), $expected_author, 'Ensure the author got filtered');
$output = $renderer->executeInRenderContext(new RenderContext(), function () use ($view, $row) {
return $view->field['author']->advancedRender($row);
});
$this->assertEqual($output, $expected_author, 'Ensure the author got filtered');
$expected_description = aggregator_filter_xss($items[$iid]->getDescription());
$this->assertEqual($view->field['description']->advancedRender($row), $expected_description, 'Ensure the author got filtered');
$output = $renderer->executeInRenderContext(new RenderContext(), function () use ($view, $row) {
return $view->field['description']->advancedRender($row);
});
$this->assertEqual($output, $expected_description, 'Ensure the author got filtered');
}
}

View File

@ -8,6 +8,7 @@
namespace Drupal\comment\Tests\Views;
use Drupal\comment\Entity\Comment;
use Drupal\Core\Render\RenderContext;
use Drupal\Core\Session\AnonymousUserSession;
use Drupal\user\RoleInterface;
use Drupal\views\Views;
@ -58,6 +59,8 @@ class CommentFieldNameTest extends CommentTestBase {
* Test comment field name.
*/
public function testCommentFieldName() {
/** @var \Drupal\Core\Render\RendererInterface $renderer */
$renderer = \Drupal::service('renderer');
$view = Views::getView('test_comment_field_name');
$this->executeView($view);
@ -85,8 +88,14 @@ class CommentFieldNameTest extends CommentTestBase {
$view = Views::getView('test_comment_field_name');
$this->executeView($view);
// Test that data rendered.
$this->assertIdentical($this->comment->getFieldName(), $view->field['field_name']->advancedRender($view->result[0]));
$this->assertIdentical($this->customComment->getFieldName(), $view->field['field_name']->advancedRender($view->result[1]));
$output = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) {
return $view->field['field_name']->advancedRender($view->result[0]);
});
$this->assertIdentical($this->comment->getFieldName(), $output);
$output = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) {
return $view->field['field_name']->advancedRender($view->result[1]);
});
$this->assertIdentical($this->customComment->getFieldName(), $output);
}
}

View File

@ -7,6 +7,7 @@
namespace Drupal\file\Tests\Views;
use Drupal\Core\Render\RenderContext;
use Drupal\file\Entity\File;
use Drupal\views\Views;
use Drupal\views\Tests\ViewUnitTestBase;
@ -69,17 +70,22 @@ class ExtensionViewsFieldTest extends ViewUnitTestBase {
* Tests file extension views field handler extension_detect_tar option.
*/
public function testFileExtensionTarOption() {
/** @var \Drupal\Core\Render\RendererInterface $renderer */
$renderer = \Drupal::service('renderer');
$view = Views::getView('file_extension_view');
$view->setDisplay();
$this->executeView($view);
// Test without the tar option.
$this->assertEqual($view->field['extension']->advancedRender($view->result[0]), 'png');
$this->assertEqual($view->field['extension']->advancedRender($view->result[1]), 'tar');
$this->assertEqual($view->field['extension']->advancedRender($view->result[2]), 'gz');
$this->assertEqual($view->field['extension']->advancedRender($view->result[3]), '');
// Test with the tar option.
$renderer->executeInRenderContext(new RenderContext(), function () use ($view) {
$this->assertEqual($view->field['extension']->advancedRender($view->result[0]), 'png');
$this->assertEqual($view->field['extension']->advancedRender($view->result[1]), 'tar');
$this->assertEqual($view->field['extension']->advancedRender($view->result[2]), 'gz');
$this->assertEqual($view->field['extension']->advancedRender($view->result[3]), '');
});
// Test with the tar option.
$view = Views::getView('file_extension_view');
$view->setDisplay();
$view->initHandlers();
@ -87,10 +93,12 @@ class ExtensionViewsFieldTest extends ViewUnitTestBase {
$view->field['extension']->options['settings']['extension_detect_tar'] = TRUE;
$this->executeView($view);
$this->assertEqual($view->field['extension']->advancedRender($view->result[0]), 'png');
$this->assertEqual($view->field['extension']->advancedRender($view->result[1]), 'tar');
$this->assertEqual($view->field['extension']->advancedRender($view->result[2]), 'tar.gz');
$this->assertEqual($view->field['extension']->advancedRender($view->result[3]), '');
$renderer->executeInRenderContext(new RenderContext(), function () use ($view) {
$this->assertEqual($view->field['extension']->advancedRender($view->result[0]), 'png');
$this->assertEqual($view->field['extension']->advancedRender($view->result[1]), 'tar');
$this->assertEqual($view->field['extension']->advancedRender($view->result[2]), 'tar.gz');
$this->assertEqual($view->field['extension']->advancedRender($view->result[3]), '');
});
}
}

View File

@ -9,6 +9,7 @@ namespace Drupal\filter\Tests;
use Drupal\Component\Utility\Html;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Core\Render\RenderContext;
use Drupal\editor\EditorXssFilter\Standard;
use Drupal\filter\Entity\FilterFormat;
use Drupal\filter\FilterPluginCollection;
@ -101,10 +102,14 @@ class FilterUnitTest extends KernelTestBase {
* Tests the caption filter.
*/
function testCaptionFilter() {
/** @var \Drupal\Core\Render\RendererInterface $renderer */
$renderer = \Drupal::service('renderer');
$filter = $this->filters['filter_caption'];
$test = function($input) use ($filter) {
return $filter->process($input, 'und');
$test = function($input) use ($filter, $renderer) {
return $renderer->executeInRenderContext(new RenderContext(), function () use ($input, $filter) {
return $filter->process($input, 'und');
});
};
$attached_library = array(
@ -190,12 +195,14 @@ class FilterUnitTest extends KernelTestBase {
'filter_html_nofollow' => 0,
)
));
$test_with_html_filter = function ($input) use ($filter, $html_filter) {
// 1. Apply HTML filter's processing step.
$output = $html_filter->process($input, 'und');
// 2. Apply caption filter's processing step.
$output = $filter->process($output, 'und');
return $output->getProcessedText();
$test_with_html_filter = function ($input) use ($filter, $html_filter, $renderer) {
return $renderer->executeInRenderContext(new RenderContext(), function () use ($input, $filter, $html_filter) {
// 1. Apply HTML filter's processing step.
$output = $html_filter->process($input, 'und');
// 2. Apply caption filter's processing step.
$output = $filter->process($output, 'und');
return $output->getProcessedText();
});
};
// Editor XSS filter.
$test_editor_xss_filter = function ($input) {
@ -252,11 +259,15 @@ class FilterUnitTest extends KernelTestBase {
* Tests the combination of the align and caption filters.
*/
function testAlignAndCaptionFilters() {
/** @var \Drupal\Core\Render\RendererInterface $renderer */
$renderer = \Drupal::service('renderer');
$align_filter = $this->filters['filter_align'];
$caption_filter = $this->filters['filter_caption'];
$test = function($input) use ($align_filter, $caption_filter) {
return $caption_filter->process($align_filter->process($input, 'und'), 'und');
$test = function($input) use ($align_filter, $caption_filter, $renderer) {
return $renderer->executeInRenderContext(new RenderContext(), function () use ($input, $align_filter, $caption_filter) {
return $caption_filter->process($align_filter->process($input, 'und'), 'und');
});
};
$attached_library = array(

View File

@ -188,17 +188,20 @@ class NodeController extends ControllerBase implements ContainerInjectionInterfa
}
$row = [];
$row[] = [
$column = [
'data' => [
'#type' => 'inline_template',
'#template' => '{% trans %}{{ date }} by {{ username }}{% endtrans %}{% if message %}<p class="revision-log">{{ message }}</p>{% endif %}',
'#context' => [
'date' => $link,
'username' => $this->renderer->render($username),
'username' => $this->renderer->renderPlain($username),
'message' => Xss::filter($revision->revision_log->value),
],
],
];
// @todo Simplify once https://www.drupal.org/node/2334319 lands.
$this->renderer->addCacheableDependency($column['data'], $username);
$row[] = $column;
if ($vid == $node->getRevisionId()) {
$row[0]['class'] = ['revision-current'];

View File

@ -95,7 +95,7 @@ class NodeRevisionsUiTest extends NodeTestBase {
'#theme' => 'username',
'#account' => $this->editor,
];
$editor = \Drupal::service('renderer')->render($username);
$editor = \Drupal::service('renderer')->renderPlain($username);
// Get original node.
$nodes[] = clone $node;

View File

@ -10,9 +10,14 @@ namespace Drupal\rest\Plugin\views\display;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Cache\CacheableResponse;
use Drupal\Core\Render\RenderContext;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\Routing\RouteProviderInterface;
use Drupal\Core\State\StateInterface;
use Drupal\views\Plugin\views\display\ResponseDisplayPluginInterface;
use Drupal\views\ViewExecutable;
use Drupal\views\Plugin\views\display\PathPluginBase;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Routing\RouteCollection;
/**
@ -70,6 +75,48 @@ class RestExport extends PathPluginBase implements ResponseDisplayPluginInterfac
*/
protected $mimeType;
/**
* The renderer.
*
* @var \Drupal\Core\Render\RendererInterface
*/
protected $renderer;
/**
* Constructs a RestExport object.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Routing\RouteProviderInterface $route_provider
* The route provider.
* @param \Drupal\Core\State\StateInterface $state
* The state key value store.
* @param \Drupal\Core\Render\RendererInterface $renderer
* The renderer.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, RouteProviderInterface $route_provider, StateInterface $state, RendererInterface $renderer) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $route_provider, $state);
$this->renderer = $renderer;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('router.route_provider'),
$container->get('state'),
$container->get('renderer')
);
}
/**
* {@inheritdoc}
*/
@ -263,7 +310,9 @@ class RestExport extends PathPluginBase implements ResponseDisplayPluginInterfac
*/
public function render() {
$build = array();
$build['#markup'] = $this->view->style_plugin->render();
$build['#markup'] = $this->renderer->executeInRenderContext(new RenderContext(), function() {
return $this->view->style_plugin->render();
});
$this->view->element['#content_type'] = $this->getMimeType();
$this->view->element['#cache_properties'][] = '#content_type';

View File

@ -10,6 +10,7 @@ namespace Drupal\simpletest;
use Drupal\Component\Serialization\Json;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Component\Utility\Xss;
use Drupal\Core\Render\RenderContext;
use Symfony\Component\CssSelector\CssSelector;
/**
@ -808,7 +809,12 @@ trait AssertContentTrait {
* TRUE on pass, FALSE on fail.
*/
protected function assertThemeOutput($callback, array $variables = array(), $expected = '', $message = '', $group = 'Other') {
$output = \Drupal::theme()->render($callback, $variables);
/** @var \Drupal\Core\Render\RendererInterface $renderer */
$renderer = \Drupal::service('renderer');
$output = $renderer->executeInRenderContext(new RenderContext(), function() use ($callback, $variables) {
return \Drupal::theme()->render($callback, $variables);
});
$this->verbose(
'<hr />' . 'Result:' . '<pre>' . SafeMarkup::checkPlain(var_export($output, TRUE)) . '</pre>'
. '<hr />' . 'Expected:' . '<pre>' . SafeMarkup::checkPlain(var_export($expected, TRUE)) . '</pre>'

View File

@ -0,0 +1,106 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Common\EarlyRenderingControllerTest.
*/
namespace Drupal\system\Tests\Common;
use Drupal\Core\Render\Element;
use Drupal\Core\Url;
use Drupal\simpletest\WebTestBase;
/**
* Verifies that bubbleable metadata of early rendering is not lost.
*
* @group Common
*/
class EarlyRenderingControllerTest extends WebTestBase {
/**
* {@inheritdoc}
*/
protected $dumpHeaders = TRUE;
/**
* {@inheritdoc}
*/
public static $modules = ['system', 'early_rendering_controller_test'];
/**
* Tests theme preprocess functions being able to attach assets.
*/
function testEarlyRendering() {
// Render array: non-early & early.
$this->drupalGet(Url::fromRoute('early_rendering_controller_test.render_array'));
$this->assertResponse(200);
$this->assertRaw('Hello world!');
$this->assertCacheTag('foo');
$this->drupalGet(Url::fromRoute('early_rendering_controller_test.render_array.early'));
$this->assertResponse(200);
$this->assertRaw('Hello world!');
$this->assertCacheTag('foo');
// Basic Response object: non-early & early.
$this->drupalGet(Url::fromRoute('early_rendering_controller_test.response'));
$this->assertResponse(200);
$this->assertRaw('Hello world!');
$this->assertNoCacheTag('foo');
$this->drupalGet(Url::fromRoute('early_rendering_controller_test.response.early'));
$this->assertResponse(200);
$this->assertRaw('Hello world!');
$this->assertNoCacheTag('foo');
// Response object with attachments: non-early & early.
$this->drupalGet(Url::fromRoute('early_rendering_controller_test.response-with-attachments'));
$this->assertResponse(200);
$this->assertRaw('Hello world!');
$this->assertNoCacheTag('foo');
$this->drupalGet(Url::fromRoute('early_rendering_controller_test.response-with-attachments.early'));
$this->assertResponse(500);
$this->assertRaw('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: Drupal\early_rendering_controller_test\AttachmentsTestResponse.');
// Cacheable Response object: non-early & early.
$this->drupalGet(Url::fromRoute('early_rendering_controller_test.cacheable-response'));
$this->assertResponse(200);
$this->assertRaw('Hello world!');
$this->assertNoCacheTag('foo');
$this->drupalGet(Url::fromRoute('early_rendering_controller_test.cacheable-response.early'));
$this->assertResponse(500);
$this->assertRaw('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: Drupal\early_rendering_controller_test\CacheableTestResponse.');
// Basic domain object: non-early & early.
$this->drupalGet(Url::fromRoute('early_rendering_controller_test.domain-object'));
$this->assertResponse(200);
$this->assertRaw('TestDomainObject');
$this->assertNoCacheTag('foo');
$this->drupalGet(Url::fromRoute('early_rendering_controller_test.domain-object.early'));
$this->assertResponse(200);
$this->assertRaw('TestDomainObject');
$this->assertNoCacheTag('foo');
// Basic domain object with attachments: non-early & early.
$this->drupalGet(Url::fromRoute('early_rendering_controller_test.domain-object-with-attachments'));
$this->assertResponse(200);
$this->assertRaw('AttachmentsTestDomainObject');
$this->assertNoCacheTag('foo');
$this->drupalGet(Url::fromRoute('early_rendering_controller_test.domain-object-with-attachments.early'));
$this->assertResponse(500);
$this->assertRaw('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: Drupal\early_rendering_controller_test\AttachmentsTestDomainObject.');
// Cacheable Response object: non-early & early.
$this->drupalGet(Url::fromRoute('early_rendering_controller_test.cacheable-domain-object'));
$this->assertResponse(200);
$this->assertRaw('CacheableTestDomainObject');
$this->assertNoCacheTag('foo');
$this->drupalGet(Url::fromRoute('early_rendering_controller_test.cacheable-domain-object.early'));
$this->assertResponse(500);
$this->assertRaw('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: Drupal\early_rendering_controller_test\CacheableTestDomainObject.');
// The exceptions are expected. Do not interpret them as a test failure.
// Not using File API; a potential error must trigger a PHP warning.
unlink(\Drupal::root() . '/' . $this->siteDirectory . '/error.log');
}
}

View File

@ -10,6 +10,7 @@ namespace Drupal\system\Tests\Common;
use Drupal\Component\Utility\UrlHelper;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Language\Language;
use Drupal\Core\Render\RenderContext;
use Drupal\Core\Url;
use Drupal\simpletest\WebTestBase;
@ -168,9 +169,11 @@ class UrlTest extends WebTestBase {
$l = \Drupal::l('foo', Url::fromUri('https://www.drupal.org'));
// Test a renderable array passed to _l().
$renderable_text = array('#markup' => 'foo');
$l_renderable_text = \Drupal::l($renderable_text, Url::fromUri('https://www.drupal.org'));
$this->assertEqual($l_renderable_text, $l);
$renderer->executeInRenderContext(new RenderContext(), function() use ($renderer, $l) {
$renderable_text = array('#markup' => 'foo');
$l_renderable_text = \Drupal::l($renderable_text, Url::fromUri('https://www.drupal.org'));
$this->assertEqual($l_renderable_text, $l);
});
// Test a themed link with plain text 'text'.
$type_link_plain_array = array(

View File

@ -0,0 +1,6 @@
name: 'Early rendering controller test'
type: module
description: 'Support module for EarlyRenderingControllerTest.'
package: Testing
version: VERSION
core: 8.x

View File

@ -0,0 +1,97 @@
# Controller returning a render array.
early_rendering_controller_test.render_array:
path: '/early-rendering-controller-test/render-array'
defaults:
_controller: '\Drupal\early_rendering_controller_test\EarlyRenderingTestController::renderArray'
requirements:
_access: 'TRUE'
early_rendering_controller_test.render_array.early:
path: '/early-rendering-controller-test/render-array/early'
defaults:
_controller: '\Drupal\early_rendering_controller_test\EarlyRenderingTestController::renderArrayEarly'
requirements:
_access: 'TRUE'
# Controller returning a basic Response object.
early_rendering_controller_test.response:
path: '/early-rendering-controller-test/response'
defaults:
_controller: '\Drupal\early_rendering_controller_test\EarlyRenderingTestController::response'
requirements:
_access: 'TRUE'
early_rendering_controller_test.response.early:
path: '/early-rendering-controller-test/response/early'
defaults:
_controller: '\Drupal\early_rendering_controller_test\EarlyRenderingTestController::responseEarly'
requirements:
_access: 'TRUE'
# Controller returning a Response object with attachments.
early_rendering_controller_test.response-with-attachments:
path: '/early-rendering-controller-test/response-with-attachments'
defaults:
_controller: '\Drupal\early_rendering_controller_test\EarlyRenderingTestController::responseWithAttachments'
requirements:
_access: 'TRUE'
early_rendering_controller_test.response-with-attachments.early:
path: '/early-rendering-controller-test/response-with-attachments/early'
defaults:
_controller: '\Drupal\early_rendering_controller_test\EarlyRenderingTestController::responseWithAttachmentsEarly'
requirements:
_access: 'TRUE'
# Controller returning a cacheable Response object.
early_rendering_controller_test.cacheable-response:
path: '/early-rendering-controller-test/cacheable-response'
defaults:
_controller: '\Drupal\early_rendering_controller_test\EarlyRenderingTestController::cacheableResponse'
requirements:
_access: 'TRUE'
early_rendering_controller_test.cacheable-response.early:
path: '/early-rendering-controller-test/cacheable-response/early'
defaults:
_controller: '\Drupal\early_rendering_controller_test\EarlyRenderingTestController::cacheableResponseEarly'
requirements:
_access: 'TRUE'
# Controller returning a basic domain object.
early_rendering_controller_test.domain-object:
path: '/early-rendering-controller-test/domain-object'
defaults:
_controller: '\Drupal\early_rendering_controller_test\EarlyRenderingTestController::domainObject'
requirements:
_access: 'TRUE'
early_rendering_controller_test.domain-object.early:
path: '/early-rendering-controller-test/domain-object/early'
defaults:
_controller: '\Drupal\early_rendering_controller_test\EarlyRenderingTestController::domainObjectEarly'
requirements:
_access: 'TRUE'
# Controller returning a domain object with attachments.
early_rendering_controller_test.domain-object-with-attachments:
path: '/early-rendering-controller-test/domain-object-with-attachments'
defaults:
_controller: '\Drupal\early_rendering_controller_test\EarlyRenderingTestController::domainObjectWithAttachments'
requirements:
_access: 'TRUE'
early_rendering_controller_test.domain-object-with-attachments.early:
path: '/early-rendering-controller-test/domain-object-with-attachments/early'
defaults:
_controller: '\Drupal\early_rendering_controller_test\EarlyRenderingTestController::domainObjectWithAttachmentsEarly'
requirements:
_access: 'TRUE'
# Controller returning a cacheable domain object.
early_rendering_controller_test.cacheable-domain-object:
path: '/early-rendering-controller-test/cacheable-domain-object'
defaults:
_controller: '\Drupal\early_rendering_controller_test\EarlyRenderingTestController::cacheableDomainObject'
requirements:
_access: 'TRUE'
early_rendering_controller_test.cacheable-domain-object.early:
path: '/early-rendering-controller-test/cacheable-domain-object/early'
defaults:
_controller: '\Drupal\early_rendering_controller_test\EarlyRenderingTestController::cacheableDomainObjectEarly'
requirements:
_access: 'TRUE'

View File

@ -0,0 +1,5 @@
services:
test_domain_object.view_subscriber:
class: Drupal\early_rendering_controller_test\TestDomainObjectViewSubscriber
tags:
- { name: event_subscriber }

View File

@ -0,0 +1,17 @@
<?php
/**
* @file
* Contains \Drupal\early_rendering_controller_test\AttachmentsTestDomainObject.
*/
namespace Drupal\early_rendering_controller_test;
use Drupal\Core\Render\AttachmentsInterface;
use Drupal\Core\Render\AttachmentsTrait;
class AttachmentsTestDomainObject extends TestDomainObject implements AttachmentsInterface {
use AttachmentsTrait;
}

View File

@ -0,0 +1,18 @@
<?php
/**
* @file
* Contains \Drupal\early_rendering_controller_test\AttachmentsTestResponse.
*/
namespace Drupal\early_rendering_controller_test;
use Drupal\Core\Render\AttachmentsInterface;
use Drupal\Core\Render\AttachmentsTrait;
use Symfony\Component\HttpFoundation\Response;
class AttachmentsTestResponse extends Response implements AttachmentsInterface {
use AttachmentsTrait;
}

View File

@ -0,0 +1,35 @@
<?php
/**
* @file
* Contains \Drupal\early_rendering_controller_test\AttachmentsTestDomainObject.
*/
namespace Drupal\early_rendering_controller_test;
use Drupal\Core\Cache\CacheableDependencyInterface;
class CacheableTestDomainObject extends TestDomainObject implements CacheableDependencyInterface {
/**
* {@inheritdoc}
*/
public function getCacheContexts() {
return [];
}
/**
* {@inheritdoc}
*/
public function getCacheTags() {
return [];
}
/**
* {@inheritdoc}
*/
public function getCacheMaxAge() {
return 0;
}
}

View File

@ -0,0 +1,18 @@
<?php
/**
* @file
* Contains \Drupal\early_rendering_controller_test\CacheableTestResponse.
*/
namespace Drupal\early_rendering_controller_test;
use Drupal\Core\Cache\CacheableResponseInterface;
use Drupal\Core\Cache\CacheableResponseTrait;
use Symfony\Component\HttpFoundation\Response;
class CacheableTestResponse extends Response implements CacheableResponseInterface {
use CacheableResponseTrait;
}

View File

@ -0,0 +1,136 @@
<?php
/**
* @file
* Contains \Drupal\early_rendering_controller_test\EarlyRenderingTestController.
*/
namespace Drupal\early_rendering_controller_test;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Render\RendererInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Response;
/**
* Controller routines for early_rendering_test routes.
*
* The methods on this controller each correspond to a route for this module,
* each of which exist solely for test cases in EarlyRenderingControllerTest;
* see that test for documentation.
*
* @see core/modules/early_rendering_controller_test/early_rendering_controller_test.routing.yml
* @see \Drupal\system\Tests\Common\EarlyRenderingControllerTest::testEarlyRendering()
*/
class EarlyRenderingTestController extends ControllerBase {
/**
* The renderer.
*
* @var \Drupal\Core\Render\RendererInterface
*/
protected $renderer;
/**
* Constructs a EarlyRenderingTestController.
*
* @param \Drupal\Core\Render\RendererInterface $renderer
*/
public function __construct(RendererInterface $renderer) {
$this->renderer = $renderer;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('renderer')
);
}
protected function earlyRenderContent() {
return [
'#markup' => 'Hello world!',
'#cache' => [
'tags' => [
'foo',
],
],
];
}
public function renderArray() {
return [
'#pre_render' => [function () {
$elements = $this->earlyRenderContent();
return $elements;
}],
];
}
public function renderArrayEarly() {
$render_array = $this->earlyRenderContent();
return [
'#markup' => $this->renderer->render($render_array),
];
}
public function response() {
return new Response('Hello world!');
}
public function responseEarly() {
$render_array = $this->earlyRenderContent();
return new Response($this->renderer->render($render_array));
}
public function responseWithAttachments() {
return new AttachmentsTestResponse('Hello world!');
}
public function responseWithAttachmentsEarly() {
$render_array = $this->earlyRenderContent();
return new AttachmentsTestResponse($this->renderer->render($render_array));
}
public function cacheableResponse() {
return new CacheableTestResponse('Hello world!');
}
public function cacheableResponseEarly() {
$render_array = $this->earlyRenderContent();
return new CacheableTestResponse($this->renderer->render($render_array));
}
public function domainObject() {
return new TestDomainObject();
}
public function domainObjectEarly() {
$render_array = $this->earlyRenderContent();
$this->renderer->render($render_array);
return new TestDomainObject();
}
public function domainObjectWithAttachments() {
return new AttachmentsTestDomainObject();
}
public function domainObjectWithAttachmentsEarly() {
$render_array = $this->earlyRenderContent();
$this->renderer->render($render_array);
return new AttachmentsTestDomainObject();
}
public function cacheableDomainObject() {
return new CacheableTestDomainObject();
}
public function cacheableDomainObjectEarly() {
$render_array = $this->earlyRenderContent();
$this->renderer->render($render_array);
return new CacheableTestDomainObject();
}
}

View File

@ -0,0 +1,10 @@
<?php
/**
* @file
* Contains \Drupal\early_rendering_controller_test\TestDomainObject.
*/
namespace Drupal\early_rendering_controller_test;
class TestDomainObject { }

View File

@ -0,0 +1,51 @@
<?php
/**
* @file
* Contains \Drupal\early_rendering_controller_test\TestDomainObjectViewSubscriber.
*/
namespace Drupal\early_rendering_controller_test;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent;
use Symfony\Component\HttpKernel\KernelEvents;
/**
* View subscriber for turning TestDomainObject objects into Response objects.
*/
class TestDomainObjectViewSubscriber implements EventSubscriberInterface {
/**
* Sets a response given a TestDomainObject instance.
*
* @param \Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent $event
* The event to process.
*/
public function onViewTestDomainObject(GetResponseForControllerResultEvent $event) {
$result = $event->getControllerResult();
if ($result instanceof TestDomainObject) {
if ($result instanceof AttachmentsTestDomainObject) {
$event->setResponse(new AttachmentsTestResponse('AttachmentsTestDomainObject'));
}
elseif ($result instanceof CacheableTestDomainObject) {
$event->setResponse(new CacheableTestResponse('CacheableTestDomainObject'));
}
else {
$event->setResponse(new Response('TestDomainObject'));
}
}
}
/**
* {@inheritdoc}
*/
static function getSubscribedEvents() {
$events[KernelEvents::VIEW][] = ['onViewTestDomainObject'];
return $events;
}
}

View File

@ -7,6 +7,7 @@
namespace Drupal\taxonomy\Tests\Views;
use Drupal\Core\Render\RenderContext;
use Drupal\views\Views;
/**
@ -24,10 +25,15 @@ class TaxonomyFieldTidTest extends TaxonomyTestBase {
public static $testViews = array('test_taxonomy_tid_field');
function testViewsHandlerTidField() {
/** @var \Drupal\Core\Render\RendererInterface $renderer */
$renderer = \Drupal::service('renderer');
$view = Views::getView('test_taxonomy_tid_field');
$this->executeView($view);
$actual = $view->field['name']->advancedRender($view->result[0]);
$actual = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) {
return $view->field['name']->advancedRender($view->result[0]);
});
$expected = \Drupal::l($this->term1->label(), $this->term1->urlInfo());
$this->assertEqual($expected, $actual);

View File

@ -7,6 +7,7 @@
namespace Drupal\user\Tests\Views;
use Drupal\Core\Render\RenderContext;
use Drupal\views\Views;
/**
@ -25,6 +26,9 @@ class HandlerFieldUserNameTest extends UserTestBase {
public static $testViews = array('test_views_handler_field_user_name');
public function testUserName() {
/** @var \Drupal\Core\Render\RendererInterface $renderer */
$renderer = \Drupal::service('renderer');
$this->drupalLogin($this->drupalCreateUser(array('access user profiles')));
// Set defaults.
@ -37,13 +41,17 @@ class HandlerFieldUserNameTest extends UserTestBase {
$this->executeView($view);
$anon_name = $this->config('user.settings')->get('anonymous');
$render = $view->field['name']->advancedRender($view->result[0]);
$render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) {
return $view->field['name']->advancedRender($view->result[0]);
});
$this->assertTrue(strpos($render, $anon_name) !== FALSE, 'For user 0 it should use the default anonymous name by default.');
$username = $this->randomMachineName();
$view->result[0]->_entity->setUsername($username);
$view->result[0]->_entity->uid->value = 1;
$render = $view->field['name']->advancedRender($view->result[0]);
$render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) {
return $view->field['name']->advancedRender($view->result[0]);
});
$this->assertTrue(strpos($render, $username) !== FALSE, 'If link to user is checked the username should be part of the output.');
$this->assertTrue(strpos($render, 'user/1') !== FALSE, 'If link to user is checked the link to the user should appear as well.');
@ -52,7 +60,9 @@ class HandlerFieldUserNameTest extends UserTestBase {
$username = $this->randomMachineName();
$view->result[0]->_entity->setUsername($username);
$view->result[0]->_entity->uid->value = 1;
$render = $view->field['name']->advancedRender($view->result[0]);
$render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) {
return $view->field['name']->advancedRender($view->result[0]);
});
$this->assertIdentical($render, $username, 'If the user is not linked the username should be printed out for a normal user.');
}
@ -61,13 +71,18 @@ class HandlerFieldUserNameTest extends UserTestBase {
* Tests that the field handler works when no additional fields are added.
*/
public function testNoAdditionalFields() {
/** @var \Drupal\Core\Render\RendererInterface $renderer */
$renderer = \Drupal::service('renderer');
$view = Views::getView('test_views_handler_field_user_name');
$this->executeView($view);
$username = $this->randomMachineName();
$view->result[0]->_entity->setUsername($username);
$view->result[0]->_entity->uid->value = 1;
$render = $view->field['name']->advancedRender($view->result[0]);
$render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) {
return $view->field['name']->advancedRender($view->result[0]);
});
$this->assertTrue(strpos($render, $username) !== FALSE, 'If link to user is checked the username should be part of the output.');
}

View File

@ -8,6 +8,7 @@
namespace Drupal\views\Tests\Handler;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\Render\RenderContext;
use Drupal\views\Views;
/**
@ -67,6 +68,9 @@ class FieldGroupRowsTest extends HandlerTestBase {
* Testing the "Grouped rows" functionality.
*/
public function testGroupRows() {
/** @var \Drupal\Core\Render\RendererInterface $renderer */
$renderer = \Drupal::service('renderer');
$edit = array(
'title' => $this->randomMachineName(),
$this->fieldName => array('a', 'b', 'c'),
@ -77,7 +81,10 @@ class FieldGroupRowsTest extends HandlerTestBase {
// Test grouped rows.
$this->executeView($view);
$this->assertEqual($view->field[$this->fieldName]->advancedRender($view->result[0]), 'a, b, c');
$output = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) {
return $view->field[$this->fieldName]->advancedRender($view->result[0]);
});
$this->assertEqual($output, 'a, b, c');
// Change the group_rows checkbox to false.
$view = Views::getView('test_group_rows');
@ -88,11 +95,20 @@ class FieldGroupRowsTest extends HandlerTestBase {
$view->render();
$view->row_index = 0;
$this->assertEqual($view->field[$this->fieldName]->advancedRender($view->result[0]), 'a');
$output = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) {
return $view->field[$this->fieldName]->advancedRender($view->result[0]);
});
$this->assertEqual($output, 'a');
$view->row_index = 1;
$this->assertEqual($view->field[$this->fieldName]->advancedRender($view->result[1]), 'b');
$output = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) {
return $view->field[$this->fieldName]->advancedRender($view->result[1]);
});
$this->assertEqual($output, 'b');
$view->row_index = 2;
$this->assertEqual($view->field[$this->fieldName]->advancedRender($view->result[2]), 'c');
$output = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) {
return $view->field[$this->fieldName]->advancedRender($view->result[2]);
});
$this->assertEqual($output, 'c');
}
}

View File

@ -7,6 +7,7 @@
namespace Drupal\views\Tests\Handler;
use Drupal\Core\Render\RenderContext;
use Drupal\views\Tests\ViewUnitTestBase;
use Drupal\views\Plugin\views\field\FieldPluginBase;
use Drupal\views\Views;
@ -52,12 +53,18 @@ class FieldUnitTest extends ViewUnitTestBase {
* Tests that the render function is called.
*/
public function testRender() {
/** @var \Drupal\Core\Render\RendererInterface $renderer */
$renderer = \Drupal::service('renderer');
$view = Views::getView('test_field_tokens');
$this->executeView($view);
$random_text = $this->randomMachineName();
$view->field['job']->setTestValue($random_text);
$this->assertEqual($view->field['job']->theme($view->result[0]), $random_text, 'Make sure the render method rendered the manual set value.');
$output = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) {
return $view->field['job']->theme($view->result[0]);
});
$this->assertEqual($output, $random_text, 'Make sure the render method rendered the manual set value.');
}
/**
@ -141,6 +148,9 @@ class FieldUnitTest extends ViewUnitTestBase {
* Tests general rewriting of the output.
*/
public function testRewrite() {
/** @var \Drupal\Core\Render\RendererInterface $renderer */
$renderer = \Drupal::service('renderer');
$view = Views::getView('test_view');
$view->initHandlers();
$this->executeView($view);
@ -149,11 +159,15 @@ class FieldUnitTest extends ViewUnitTestBase {
// Don't check the rewrite checkbox, so the text shouldn't appear.
$id_field->options['alter']['text'] = $random_text = $this->randomMachineName();
$output = $id_field->theme($row);
$output = $renderer->executeInRenderContext(new RenderContext(), function () use ($id_field, $row) {
return $id_field->theme($row);
});
$this->assertNotSubString($output, $random_text);
$id_field->options['alter']['alter_text'] = TRUE;
$output = $id_field->theme($row);
$output = $renderer->executeInRenderContext(new RenderContext(), function () use ($id_field, $row) {
return $id_field->theme($row);
});
$this->assertSubString($output, $random_text);
}
@ -161,6 +175,9 @@ class FieldUnitTest extends ViewUnitTestBase {
* Tests the field tokens, row level and field level.
*/
public function testFieldTokens() {
/** @var \Drupal\Core\Render\RendererInterface $renderer */
$renderer = \Drupal::service('renderer');
$view = Views::getView('test_field_tokens');
$this->executeView($view);
$name_field_0 = $view->field['name'];
@ -182,19 +199,25 @@ class FieldUnitTest extends ViewUnitTestBase {
$expected_output_1 = "$row->views_test_data_name $row->views_test_data_name";
$expected_output_2 = "$row->views_test_data_name $row->views_test_data_name $row->views_test_data_name";
$output = $name_field_0->advancedRender($row);
$output = $renderer->executeInRenderContext(new RenderContext(), function () use ($name_field_0, $row) {
return $name_field_0->advancedRender($row);
});
$this->assertEqual($output, $expected_output_0, format_string('Test token replacement: "!token" gave "!output"', [
'!token' => $name_field_0->options['alter']['text'],
'!output' => $output,
]));
$output = $name_field_1->advancedRender($row);
$output = $renderer->executeInRenderContext(new RenderContext(), function () use ($name_field_1, $row) {
return $name_field_1->advancedRender($row);
});
$this->assertEqual($output, $expected_output_1, format_string('Test token replacement: "!token" gave "!output"', [
'!token' => $name_field_1->options['alter']['text'],
'!output' => $output,
]));
$output = $name_field_2->advancedRender($row);
$output = $renderer->executeInRenderContext(new RenderContext(), function () use ($name_field_2, $row) {
return $name_field_2->advancedRender($row);
});
$this->assertEqual($output, $expected_output_2, format_string('Test token replacement: "!token" gave "!output"', [
'!token' => $name_field_2->options['alter']['text'],
'!output' => $output,
@ -207,7 +230,9 @@ class FieldUnitTest extends ViewUnitTestBase {
$random_text = $this->randomMachineName();
$job_field->setTestValue($random_text);
$output = $job_field->advancedRender($row);
$output = $renderer->executeInRenderContext(new RenderContext(), function () use ($job_field, $row) {
return $job_field->advancedRender($row);
});
$this->assertSubString($output, $random_text, format_string('Make sure the self token (!token => !value) appears in the output (!output)', [
'!value' => $random_text,
'!output' => $output,
@ -219,7 +244,9 @@ class FieldUnitTest extends ViewUnitTestBase {
$job_field->options['alter']['text'] = $old_token;
$random_text = $this->randomMachineName();
$job_field->setTestValue($random_text);
$output = $job_field->advancedRender($row);
$output = $renderer->executeInRenderContext(new RenderContext(), function () use ($job_field, $row) {
return $job_field->advancedRender($row);
});
$this->assertSubString($output, $old_token, format_string('Make sure the old token style (!token => !value) is not changed in the output (!output)', [
'!value' => $random_text,
'!output' => $output,
@ -268,6 +295,9 @@ class FieldUnitTest extends ViewUnitTestBase {
* This tests alters the result to get easier and less coupled results.
*/
function _testHideIfEmpty() {
/** @var \Drupal\Core\Render\RendererInterface $renderer */
$renderer = \Drupal::service('renderer');
$view = Views::getView('test_view');
$view->initDisplay();
$this->executeView($view);
@ -284,22 +314,30 @@ class FieldUnitTest extends ViewUnitTestBase {
// Test a valid string.
$view->result[0]->{$column_map_reversed['name']} = $random_name;
$render = $view->field['name']->advancedRender($view->result[0]);
$render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) {
return $view->field['name']->advancedRender($view->result[0]);
});
$this->assertIdentical($render, $random_name, 'By default, a string should not be treated as empty.');
// Test an empty string.
$view->result[0]->{$column_map_reversed['name']} = "";
$render = $view->field['name']->advancedRender($view->result[0]);
$render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) {
return $view->field['name']->advancedRender($view->result[0]);
});
$this->assertIdentical($render, "", 'By default, "" should not be treated as empty.');
// Test zero as an integer.
$view->result[0]->{$column_map_reversed['name']} = 0;
$render = $view->field['name']->advancedRender($view->result[0]);
$render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) {
return $view->field['name']->advancedRender($view->result[0]);
});
$this->assertIdentical($render, '0', 'By default, 0 should not be treated as empty.');
// Test zero as a string.
$view->result[0]->{$column_map_reversed['name']} = "0";
$render = $view->field['name']->advancedRender($view->result[0]);
$render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) {
return $view->field['name']->advancedRender($view->result[0]);
});
$this->assertIdentical($render, "0", 'By default, "0" should not be treated as empty.');
// Test when results are not rewritten and non-zero empty values are hidden.
@ -309,22 +347,30 @@ class FieldUnitTest extends ViewUnitTestBase {
// Test a valid string.
$view->result[0]->{$column_map_reversed['name']} = $random_name;
$render = $view->field['name']->advancedRender($view->result[0]);
$render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) {
return $view->field['name']->advancedRender($view->result[0]);
});
$this->assertIdentical($render, $random_name, 'If hide_empty is checked, a string should not be treated as empty.');
// Test an empty string.
$view->result[0]->{$column_map_reversed['name']} = "";
$render = $view->field['name']->advancedRender($view->result[0]);
$render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) {
return $view->field['name']->advancedRender($view->result[0]);
});
$this->assertIdentical($render, "", 'If hide_empty is checked, "" should be treated as empty.');
// Test zero as an integer.
$view->result[0]->{$column_map_reversed['name']} = 0;
$render = $view->field['name']->advancedRender($view->result[0]);
$render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) {
return $view->field['name']->advancedRender($view->result[0]);
});
$this->assertIdentical($render, '0', 'If hide_empty is checked, but not empty_zero, 0 should not be treated as empty.');
// Test zero as a string.
$view->result[0]->{$column_map_reversed['name']} = "0";
$render = $view->field['name']->advancedRender($view->result[0]);
$render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) {
return $view->field['name']->advancedRender($view->result[0]);
});
$this->assertIdentical($render, "0", 'If hide_empty is checked, but not empty_zero, "0" should not be treated as empty.');
// Test when results are not rewritten and all empty values are hidden.
@ -334,12 +380,16 @@ class FieldUnitTest extends ViewUnitTestBase {
// Test zero as an integer.
$view->result[0]->{$column_map_reversed['name']} = 0;
$render = $view->field['name']->advancedRender($view->result[0]);
$render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) {
return $view->field['name']->advancedRender($view->result[0]);
});
$this->assertIdentical($render, "", 'If hide_empty and empty_zero are checked, 0 should be treated as empty.');
// Test zero as a string.
$view->result[0]->{$column_map_reversed['name']} = "0";
$render = $view->field['name']->advancedRender($view->result[0]);
$render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) {
return $view->field['name']->advancedRender($view->result[0]);
});
$this->assertIdentical($render, "", 'If hide_empty and empty_zero are checked, "0" should be treated as empty.');
// Test when results are rewritten to a valid string and non-zero empty
@ -352,22 +402,30 @@ class FieldUnitTest extends ViewUnitTestBase {
// Test a valid string.
$view->result[0]->{$column_map_reversed['name']} = $random_value;
$render = $view->field['name']->advancedRender($view->result[0]);
$render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) {
return $view->field['name']->advancedRender($view->result[0]);
});
$this->assertIdentical($render, $random_name, 'If the rewritten string is not empty, it should not be treated as empty.');
// Test an empty string.
$view->result[0]->{$column_map_reversed['name']} = "";
$render = $view->field['name']->advancedRender($view->result[0]);
$render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) {
return $view->field['name']->advancedRender($view->result[0]);
});
$this->assertIdentical($render, $random_name, 'If the rewritten string is not empty, "" should not be treated as empty.');
// Test zero as an integer.
$view->result[0]->{$column_map_reversed['name']} = 0;
$render = $view->field['name']->advancedRender($view->result[0]);
$render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) {
return $view->field['name']->advancedRender($view->result[0]);
});
$this->assertIdentical($render, $random_name, 'If the rewritten string is not empty, 0 should not be treated as empty.');
// Test zero as a string.
$view->result[0]->{$column_map_reversed['name']} = "0";
$render = $view->field['name']->advancedRender($view->result[0]);
$render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) {
return $view->field['name']->advancedRender($view->result[0]);
});
$this->assertIdentical($render, $random_name, 'If the rewritten string is not empty, "0" should not be treated as empty.');
// Test when results are rewritten to an empty string and non-zero empty results are hidden.
@ -379,22 +437,30 @@ class FieldUnitTest extends ViewUnitTestBase {
// Test a valid string.
$view->result[0]->{$column_map_reversed['name']} = $random_name;
$render = $view->field['name']->advancedRender($view->result[0]);
$render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) {
return $view->field['name']->advancedRender($view->result[0]);
});
$this->assertIdentical($render, $random_name, 'If the rewritten string is empty, it should not be treated as empty.');
// Test an empty string.
$view->result[0]->{$column_map_reversed['name']} = "";
$render = $view->field['name']->advancedRender($view->result[0]);
$render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) {
return $view->field['name']->advancedRender($view->result[0]);
});
$this->assertIdentical($render, "", 'If the rewritten string is empty, "" should be treated as empty.');
// Test zero as an integer.
$view->result[0]->{$column_map_reversed['name']} = 0;
$render = $view->field['name']->advancedRender($view->result[0]);
$render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) {
return $view->field['name']->advancedRender($view->result[0]);
});
$this->assertIdentical($render, '0', 'If the rewritten string is empty, 0 should not be treated as empty.');
// Test zero as a string.
$view->result[0]->{$column_map_reversed['name']} = "0";
$render = $view->field['name']->advancedRender($view->result[0]);
$render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) {
return $view->field['name']->advancedRender($view->result[0]);
});
$this->assertIdentical($render, "0", 'If the rewritten string is empty, "0" should not be treated as empty.');
// Test when results are rewritten to zero as a string and non-zero empty
@ -407,22 +473,30 @@ class FieldUnitTest extends ViewUnitTestBase {
// Test a valid string.
$view->result[0]->{$column_map_reversed['name']} = $random_name;
$render = $view->field['name']->advancedRender($view->result[0]);
$render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) {
return $view->field['name']->advancedRender($view->result[0]);
});
$this->assertIdentical($render, "0", 'If the rewritten string is zero and empty_zero is not checked, the string rewritten as 0 should not be treated as empty.');
// Test an empty string.
$view->result[0]->{$column_map_reversed['name']} = "";
$render = $view->field['name']->advancedRender($view->result[0]);
$render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) {
return $view->field['name']->advancedRender($view->result[0]);
});
$this->assertIdentical($render, "0", 'If the rewritten string is zero and empty_zero is not checked, "" rewritten as 0 should not be treated as empty.');
// Test zero as an integer.
$view->result[0]->{$column_map_reversed['name']} = 0;
$render = $view->field['name']->advancedRender($view->result[0]);
$render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) {
return $view->field['name']->advancedRender($view->result[0]);
});
$this->assertIdentical($render, "0", 'If the rewritten string is zero and empty_zero is not checked, 0 should not be treated as empty.');
// Test zero as a string.
$view->result[0]->{$column_map_reversed['name']} = "0";
$render = $view->field['name']->advancedRender($view->result[0]);
$render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) {
return $view->field['name']->advancedRender($view->result[0]);
});
$this->assertIdentical($render, "0", 'If the rewritten string is zero and empty_zero is not checked, "0" should not be treated as empty.');
// Test when results are rewritten to a valid string and non-zero empty
@ -435,22 +509,30 @@ class FieldUnitTest extends ViewUnitTestBase {
// Test a valid string.
$view->result[0]->{$column_map_reversed['name']} = $random_name;
$render = $view->field['name']->advancedRender($view->result[0]);
$render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) {
return $view->field['name']->advancedRender($view->result[0]);
});
$this->assertIdentical($render, $random_value, 'If the original and rewritten strings are valid, it should not be treated as empty.');
// Test an empty string.
$view->result[0]->{$column_map_reversed['name']} = "";
$render = $view->field['name']->advancedRender($view->result[0]);
$render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) {
return $view->field['name']->advancedRender($view->result[0]);
});
$this->assertIdentical($render, "", 'If either the original or rewritten string is invalid, "" should be treated as empty.');
// Test zero as an integer.
$view->result[0]->{$column_map_reversed['name']} = 0;
$render = $view->field['name']->advancedRender($view->result[0]);
$render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) {
return $view->field['name']->advancedRender($view->result[0]);
});
$this->assertIdentical($render, $random_value, 'If the original and rewritten strings are valid, 0 should not be treated as empty.');
// Test zero as a string.
$view->result[0]->{$column_map_reversed['name']} = "0";
$render = $view->field['name']->advancedRender($view->result[0]);
$render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) {
return $view->field['name']->advancedRender($view->result[0]);
});
$this->assertIdentical($render, $random_value, 'If the original and rewritten strings are valid, "0" should not be treated as empty.');
// Test when results are rewritten to zero as a string and all empty
@ -463,22 +545,30 @@ class FieldUnitTest extends ViewUnitTestBase {
// Test a valid string.
$view->result[0]->{$column_map_reversed['name']} = $random_name;
$render = $view->field['name']->advancedRender($view->result[0]);
$render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) {
return $view->field['name']->advancedRender($view->result[0]);
});
$this->assertIdentical($render, "", 'If the rewritten string is zero, it should be treated as empty.');
// Test an empty string.
$view->result[0]->{$column_map_reversed['name']} = "";
$render = $view->field['name']->advancedRender($view->result[0]);
$render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) {
return $view->field['name']->advancedRender($view->result[0]);
});
$this->assertIdentical($render, "", 'If the rewritten string is zero, "" should be treated as empty.');
// Test zero as an integer.
$view->result[0]->{$column_map_reversed['name']} = 0;
$render = $view->field['name']->advancedRender($view->result[0]);
$render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) {
return $view->field['name']->advancedRender($view->result[0]);
});
$this->assertIdentical($render, "", 'If the rewritten string is zero, 0 should not be treated as empty.');
// Test zero as a string.
$view->result[0]->{$column_map_reversed['name']} = "0";
$render = $view->field['name']->advancedRender($view->result[0]);
$render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) {
return $view->field['name']->advancedRender($view->result[0]);
});
$this->assertIdentical($render, "", 'If the rewritten string is zero, "0" should not be treated as empty.');
}
@ -486,6 +576,9 @@ class FieldUnitTest extends ViewUnitTestBase {
* Tests the usage of the empty text.
*/
function _testEmptyText() {
/** @var \Drupal\Core\Render\RendererInterface $renderer */
$renderer = \Drupal::service('renderer');
$view = Views::getView('test_view');
$view->initDisplay();
$this->executeView($view);
@ -495,27 +588,37 @@ class FieldUnitTest extends ViewUnitTestBase {
$empty_text = $view->field['name']->options['empty'] = $this->randomMachineName();
$view->result[0]->{$column_map_reversed['name']} = "";
$render = $view->field['name']->advancedRender($view->result[0]);
$render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) {
return $view->field['name']->advancedRender($view->result[0]);
});
$this->assertIdentical($render, $empty_text, 'If a field is empty, the empty text should be used for the output.');
$view->result[0]->{$column_map_reversed['name']} = "0";
$render = $view->field['name']->advancedRender($view->result[0]);
$render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) {
return $view->field['name']->advancedRender($view->result[0]);
});
$this->assertIdentical($render, "0", 'If a field is 0 and empty_zero is not checked, the empty text should not be used for the output.');
$view->result[0]->{$column_map_reversed['name']} = "0";
$view->field['name']->options['empty_zero'] = TRUE;
$render = $view->field['name']->advancedRender($view->result[0]);
$render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) {
return $view->field['name']->advancedRender($view->result[0]);
});
$this->assertIdentical($render, $empty_text, 'If a field is 0 and empty_zero is checked, the empty text should be used for the output.');
$view->result[0]->{$column_map_reversed['name']} = "";
$view->field['name']->options['alter']['alter_text'] = TRUE;
$alter_text = $view->field['name']->options['alter']['text'] = $this->randomMachineName();
$view->field['name']->options['hide_alter_empty'] = FALSE;
$render = $view->field['name']->advancedRender($view->result[0]);
$render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) {
return $view->field['name']->advancedRender($view->result[0]);
});
$this->assertIdentical($render, $alter_text, 'If a field is empty, some rewrite text exists, but hide_alter_empty is not checked, render the rewrite text.');
$view->field['name']->options['hide_alter_empty'] = TRUE;
$render = $view->field['name']->advancedRender($view->result[0]);
$render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) {
return $view->field['name']->advancedRender($view->result[0]);
});
$this->assertIdentical($render, $empty_text, 'If a field is empty, some rewrite text exists, and hide_alter_empty is checked, use the empty text.');
}

View File

@ -10,6 +10,7 @@ namespace Drupal\views\Tests\Handler;
use Drupal\Component\Utility\Html;
use Drupal\Component\Utility\Unicode;
use Drupal\Component\Utility\UrlHelper;
use Drupal\Core\Render\RenderContext;
use Drupal\system\Tests\Cache\AssertPageCacheContextsAndTagsTrait;
use Drupal\views\Views;
@ -197,6 +198,9 @@ class FieldWebTest extends HandlerTestBase {
* Tests rewriting the output to a link.
*/
public function testAlterUrl() {
/** @var \Drupal\Core\Render\RendererInterface $renderer */
$renderer = \Drupal::service('renderer');
$view = Views::getView('test_view');
$view->setDisplay();
$view->initHandlers();
@ -211,13 +215,17 @@ class FieldWebTest extends HandlerTestBase {
// Tests that the suffix/prefix appears on the output.
$id_field->options['alter']['prefix'] = $prefix = $this->randomMachineName();
$id_field->options['alter']['suffix'] = $suffix = $this->randomMachineName();
$output = $id_field->theme($row);
$output = $renderer->executeInRenderContext(new RenderContext(), function () use ($id_field, $row) {
return $id_field->theme($row);
});
$this->assertSubString($output, $prefix);
$this->assertSubString($output, $suffix);
unset($id_field->options['alter']['prefix']);
unset($id_field->options['alter']['suffix']);
$output = $id_field->theme($row);
$output = $renderer->executeInRenderContext(new RenderContext(), function () use ($id_field, $row) {
return $id_field->theme($row);
});
$this->assertSubString($output, $path, 'Make sure that the path is part of the output');
// Some generic test code adapted from the UrlTest class, which tests
@ -228,44 +236,60 @@ class FieldWebTest extends HandlerTestBase {
$expected_result = \Drupal::url('entity.node.canonical', ['node' => '123'], ['absolute' => $absolute]);
$alter['absolute'] = $absolute;
$result = $id_field->theme($row);
$result = $renderer->executeInRenderContext(new RenderContext(), function () use ($id_field, $row) {
return $id_field->theme($row);
});
$this->assertSubString($result, $expected_result);
$expected_result = \Drupal::url('entity.node.canonical', ['node' => '123'], ['fragment' => 'foo', 'absolute' => $absolute]);
$alter['path'] = 'node/123#foo';
$result = $id_field->theme($row);
$result = $renderer->executeInRenderContext(new RenderContext(), function () use ($id_field, $row) {
return $id_field->theme($row);
});
$this->assertSubString($result, $expected_result);
$expected_result = \Drupal::url('entity.node.canonical', ['node' => '123'], ['query' => ['foo' => NULL], 'absolute' => $absolute]);
$alter['path'] = 'node/123?foo';
$result = $id_field->theme($row);
$result = $renderer->executeInRenderContext(new RenderContext(), function () use ($id_field, $row) {
return $id_field->theme($row);
});
$this->assertSubString($result, $expected_result);
$expected_result = \Drupal::url('entity.node.canonical', ['node' => '123'], ['query' => ['foo' => 'bar', 'bar' => 'baz'], 'absolute' => $absolute]);
$alter['path'] = 'node/123?foo=bar&bar=baz';
$result = $id_field->theme($row);
$result = $renderer->executeInRenderContext(new RenderContext(), function () use ($id_field, $row) {
return $id_field->theme($row);
});
$this->assertSubString(Html::decodeEntities($result), Html::decodeEntities($expected_result));
// @todo The route-based URL generator strips out NULL attributes.
// $expected_result = \Drupal::url('entity.node.canonical', ['node' => '123'], ['query' => ['foo' => NULL], 'fragment' => 'bar', 'absolute' => $absolute]);
$expected_result = \Drupal::urlGenerator()->generateFromPath('node/123', array('query' => array('foo' => NULL), 'fragment' => 'bar', 'absolute' => $absolute));
$alter['path'] = 'node/123?foo#bar';
$result = $id_field->theme($row);
$result = $renderer->executeInRenderContext(new RenderContext(), function () use ($id_field, $row) {
return $id_field->theme($row);
});
$this->assertSubString(Html::decodeEntities($result), Html::decodeEntities($expected_result));
$expected_result = \Drupal::url('<front>', [], ['absolute' => $absolute]);
$alter['path'] = '<front>';
$result = $id_field->theme($row);
$result = $renderer->executeInRenderContext(new RenderContext(), function () use ($id_field, $row) {
return $id_field->theme($row);
});
$this->assertSubString($result, $expected_result);
}
// Tests the replace spaces with dashes feature.
$id_field->options['alter']['replace_spaces'] = TRUE;
$id_field->options['alter']['path'] = $path = $this->randomMachineName() . ' ' . $this->randomMachineName();
$output = $id_field->theme($row);
$output = $renderer->executeInRenderContext(new RenderContext(), function () use ($id_field, $row) {
return $id_field->theme($row);
});
$this->assertSubString($output, str_replace(' ', '-', $path));
$id_field->options['alter']['replace_spaces'] = FALSE;
$output = $id_field->theme($row);
$output = $renderer->executeInRenderContext(new RenderContext(), function () use ($id_field, $row) {
return $id_field->theme($row);
});
// The url has a space in it, so to check we have to decode the url output.
$this->assertSubString(urldecode($output), $path);
@ -273,44 +297,60 @@ class FieldWebTest extends HandlerTestBase {
// Switch on the external flag should output an external url as well.
$id_field->options['alter']['external'] = TRUE;
$id_field->options['alter']['path'] = $path = 'www.drupal.org';
$output = $id_field->theme($row);
$output = $renderer->executeInRenderContext(new RenderContext(), function () use ($id_field, $row) {
return $id_field->theme($row);
});
$this->assertSubString($output, 'http://www.drupal.org');
// Setup a not external url, which shouldn't lead to an external url.
$id_field->options['alter']['external'] = FALSE;
$id_field->options['alter']['path'] = $path = 'www.drupal.org';
$output = $id_field->theme($row);
$output = $renderer->executeInRenderContext(new RenderContext(), function () use ($id_field, $row) {
return $id_field->theme($row);
});
$this->assertNotSubString($output, 'http://www.drupal.org');
// Tests the transforming of the case setting.
$id_field->options['alter']['path'] = $path = $this->randomMachineName();
$id_field->options['alter']['path_case'] = 'none';
$output = $id_field->theme($row);
$output = $renderer->executeInRenderContext(new RenderContext(), function () use ($id_field, $row) {
return $id_field->theme($row);
});
$this->assertSubString($output, $path);
// Switch to uppercase and lowercase.
$id_field->options['alter']['path_case'] = 'upper';
$output = $id_field->theme($row);
$output = $renderer->executeInRenderContext(new RenderContext(), function () use ($id_field, $row) {
return $id_field->theme($row);
});
$this->assertSubString($output, strtoupper($path));
$id_field->options['alter']['path_case'] = 'lower';
$output = $id_field->theme($row);
$output = $renderer->executeInRenderContext(new RenderContext(), function () use ($id_field, $row) {
return $id_field->theme($row);
});
$this->assertSubString($output, strtolower($path));
// Switch to ucfirst and ucwords.
$id_field->options['alter']['path_case'] = 'ucfirst';
$id_field->options['alter']['path'] = 'drupal has a great community';
$output = $id_field->theme($row);
$output = $renderer->executeInRenderContext(new RenderContext(), function () use ($id_field, $row) {
return $id_field->theme($row);
});
$this->assertSubString($output, UrlHelper::encodePath('Drupal has a great community'));
$id_field->options['alter']['path_case'] = 'ucwords';
$output = $id_field->theme($row);
$output = $renderer->executeInRenderContext(new RenderContext(), function () use ($id_field, $row) {
return $id_field->theme($row);
});
$this->assertSubString($output, UrlHelper::encodePath('Drupal Has A Great Community'));
unset($id_field->options['alter']['path_case']);
// Tests the linkclass setting and see whether it actually exists in the
// output.
$id_field->options['alter']['link_class'] = $class = $this->randomMachineName();
$output = $id_field->theme($row);
$output = $renderer->executeInRenderContext(new RenderContext(), function () use ($id_field, $row) {
return $id_field->theme($row);
});
$elements = $this->xpathContent($output, '//a[contains(@class, :class)]', array(':class' => $class));
$this->assertTrue($elements);
// @fixme link_class, alt, rel cannot be unset, which should be fixed.
@ -318,21 +358,27 @@ class FieldWebTest extends HandlerTestBase {
// Tests the alt setting.
$id_field->options['alter']['alt'] = $rel = $this->randomMachineName();
$output = $id_field->theme($row);
$output = $renderer->executeInRenderContext(new RenderContext(), function () use ($id_field, $row) {
return $id_field->theme($row);
});
$elements = $this->xpathContent($output, '//a[contains(@title, :alt)]', array(':alt' => $rel));
$this->assertTrue($elements);
$id_field->options['alter']['alt'] = '';
// Tests the rel setting.
$id_field->options['alter']['rel'] = $rel = $this->randomMachineName();
$output = $id_field->theme($row);
$output = $renderer->executeInRenderContext(new RenderContext(), function () use ($id_field, $row) {
return $id_field->theme($row);
});
$elements = $this->xpathContent($output, '//a[contains(@rel, :rel)]', array(':rel' => $rel));
$this->assertTrue($elements);
$id_field->options['alter']['rel'] = '';
// Tests the target setting.
$id_field->options['alter']['target'] = $target = $this->randomMachineName();
$output = $id_field->theme($row);
$output = $renderer->executeInRenderContext(new RenderContext(), function () use ($id_field, $row) {
return $id_field->theme($row);
});
$elements = $this->xpathContent($output, '//a[contains(@target, :target)]', array(':target' => $target));
$this->assertTrue($elements);
unset($id_field->options['alter']['target']);
@ -453,6 +499,9 @@ class FieldWebTest extends HandlerTestBase {
* Tests trimming/read-more/ellipses.
*/
public function testTextRendering() {
/** @var \Drupal\Core\Render\RendererInterface $renderer */
$renderer = \Drupal::service('renderer');
$view = Views::getView('test_field_output');
$view->initHandlers();
$name_field = $view->field['name'];
@ -465,18 +514,24 @@ class FieldWebTest extends HandlerTestBase {
$row = $view->result[0];
$name_field->options['alter']['strip_tags'] = TRUE;
$output = $name_field->advancedRender($row);
$output = $renderer->executeInRenderContext(new RenderContext(), function () use ($name_field, $row) {
return $name_field->advancedRender($row);
});
$this->assertSubString($output, $random_text, 'Find text without html if stripping of views field output is enabled.');
$this->assertNotSubString($output, $html_text, 'Find no text with the html if stripping of views field output is enabled.');
// Tests preserving of html tags.
$name_field->options['alter']['preserve_tags'] = '<div>';
$output = $name_field->advancedRender($row);
$output = $renderer->executeInRenderContext(new RenderContext(), function () use ($name_field, $row) {
return $name_field->advancedRender($row);
});
$this->assertSubString($output, $random_text, 'Find text without html if stripping of views field output is enabled but a div is allowed.');
$this->assertSubString($output, $html_text, 'Find text with the html if stripping of views field output is enabled but a div is allowed.');
$name_field->options['alter']['strip_tags'] = FALSE;
$output = $name_field->advancedRender($row);
$output = $renderer->executeInRenderContext(new RenderContext(), function () use ($name_field, $row) {
return $name_field->advancedRender($row);
});
$this->assertSubString($output, $random_text, 'Find text without html if stripping of views field output is disabled.');
$this->assertSubString($output, $html_text, 'Find text with the html if stripping of views field output is disabled.');
@ -485,13 +540,17 @@ class FieldWebTest extends HandlerTestBase {
$views_test_data_name = $row->views_test_data_name;
$row->views_test_data_name = ' ' . $views_test_data_name . ' ';
$name_field->options['alter']['trim_whitespace'] = TRUE;
$output = $name_field->advancedRender($row);
$output = $renderer->executeInRenderContext(new RenderContext(), function () use ($name_field, $row) {
return $name_field->advancedRender($row);
});
$this->assertSubString($output, $views_test_data_name, 'Make sure the trimmed text can be found if trimming is enabled.');
$this->assertNotSubString($output, $row->views_test_data_name, 'Make sure the untrimmed text can be found if trimming is enabled.');
$name_field->options['alter']['trim_whitespace'] = FALSE;
$output = $name_field->advancedRender($row);
$output = $renderer->executeInRenderContext(new RenderContext(), function () use ($name_field, $row) {
return $name_field->advancedRender($row);
});
$this->assertSubString($output, $views_test_data_name, 'Make sure the trimmed text can be found if trimming is disabled.');
$this->assertSubString($output, $row->views_test_data_name, 'Make sure the untrimmed text can be found if trimming is disabled.');
@ -504,12 +563,16 @@ class FieldWebTest extends HandlerTestBase {
$name_field->options['alter']['max_length'] = 5;
$trimmed_name = Unicode::substr($row->views_test_data_name, 0, 5);
$output = $name_field->advancedRender($row);
$output = $renderer->executeInRenderContext(new RenderContext(), function () use ($name_field, $row) {
return $name_field->advancedRender($row);
});
$this->assertSubString($output, $trimmed_name, format_string('Make sure the trimmed output (!trimmed) appears in the rendered output (!output).', array('!trimmed' => $trimmed_name, '!output' => $output)));
$this->assertNotSubString($output, $row->views_test_data_name, format_string("Make sure the untrimmed value (!untrimmed) shouldn't appear in the rendered output (!output).", array('!untrimmed' => $row->views_test_data_name, '!output' => $output)));
$name_field->options['alter']['max_length'] = 9;
$output = $name_field->advancedRender($row);
$output = $renderer->executeInRenderContext(new RenderContext(), function () use ($name_field, $row) {
return $name_field->advancedRender($row);
});
$this->assertSubString($output, $trimmed_name, format_string('Make sure the untrimmed (!untrimmed) output appears in the rendered output (!output).', array('!trimmed' => $trimmed_name, '!output' => $output)));
// Take word_boundary into account for the tests.
@ -549,7 +612,9 @@ class FieldWebTest extends HandlerTestBase {
foreach ($tuples as $tuple) {
$row->views_test_data_name = $tuple['value'];
$output = $name_field->advancedRender($row);
$output = $renderer->executeInRenderContext(new RenderContext(), function () use ($name_field, $row) {
return $name_field->advancedRender($row);
});
if ($tuple['trimmed']) {
$this->assertNotSubString($output, $tuple['value'], format_string('The untrimmed value (!untrimmed) should not appear in the trimmed output (!output).', array('!untrimmed' => $tuple['value'], '!output' => $output)));
@ -566,22 +631,30 @@ class FieldWebTest extends HandlerTestBase {
$name_field->options['alter']['more_link_text'] = $more_text = $this->randomMachineName();
$name_field->options['alter']['more_link_path'] = $more_path = $this->randomMachineName();
$output = $name_field->advancedRender($row);
$output = $renderer->executeInRenderContext(new RenderContext(), function () use ($name_field, $row) {
return $name_field->advancedRender($row);
});
$this->assertSubString($output, $more_text, 'Make sure a read more text is displayed if the output got trimmed');
$this->assertTrue($this->xpathContent($output, '//a[contains(@href, :path)]', array(':path' => $more_path)), 'Make sure the read more link points to the right destination.');
$name_field->options['alter']['more_link'] = FALSE;
$output = $name_field->advancedRender($row);
$output = $renderer->executeInRenderContext(new RenderContext(), function () use ($name_field, $row) {
return $name_field->advancedRender($row);
});
$this->assertNotSubString($output, $more_text, 'Make sure no read more text appears.');
$this->assertFalse($this->xpathContent($output, '//a[contains(@href, :path)]', array(':path' => $more_path)), 'Make sure no read more link appears.');
// Check for the ellipses.
$row->views_test_data_name = $this->randomMachineName(8);
$name_field->options['alter']['max_length'] = 5;
$output = $name_field->advancedRender($row);
$output = $renderer->executeInRenderContext(new RenderContext(), function () use ($name_field, $row) {
return $name_field->advancedRender($row);
});
$this->assertSubString($output, '…', 'An ellipsis should appear if the output is trimmed');
$name_field->options['alter']['max_length'] = 10;
$output = $name_field->advancedRender($row);
$output = $renderer->executeInRenderContext(new RenderContext(), function () use ($name_field, $row) {
return $name_field->advancedRender($row);
});
$this->assertNotSubString($output, '…', 'No ellipsis should appear if the output is not trimmed');
}

View File

@ -7,6 +7,7 @@
namespace Drupal\views\Tests\Plugin;
use Drupal\Core\Render\RenderContext;
use Drupal\node\Entity\Node;
use Drupal\views\Tests\ViewUnitTestBase;
use Drupal\views\Views;
@ -282,14 +283,18 @@ class CacheTest extends ViewUnitTestBase {
$output = $view->buildRenderable();
/** @var \Drupal\Core\Render\RendererInterface $renderer */
$renderer = \Drupal::service('renderer');
$renderer->render($output);
$renderer->executeInRenderContext(new RenderContext(), function () use (&$output, $renderer) {
return $renderer->render($output);
});
unset($view->pre_render_called);
$view->destroy();
$view->setDisplay();
$output = $view->buildRenderable();
$renderer->render($output);
$renderer->executeInRenderContext(new RenderContext(), function () use (&$output, $renderer) {
return $renderer->render($output);
});
$this->assertTrue(in_array('views_test_data/test', $output['#attached']['library']), 'Make sure libraries are added for cached views.');
$this->assertEqual(['foo' => 'bar'], $output['#attached']['drupalSettings'], 'Make sure drupalSettings are added for cached views.');