Commit 287d1e19 authored by effulgentsia's avatar effulgentsia

Issue #2263569 by tim.plunkett, effulgentsia, Fabianx, dawehner, Wim Leers,...

Issue #2263569 by tim.plunkett, effulgentsia, Fabianx, dawehner, Wim Leers, larowlan: Bypass form caching by default for forms using #ajax.
parent d5d8b306
......@@ -818,6 +818,11 @@ services:
class: Drupal\Core\EventSubscriber\AjaxSubscriber
tags:
- { name: event_subscriber }
form_ajax_subscriber:
class: Drupal\Core\Form\EventSubscriber\FormAjaxSubscriber
arguments: ['@form_ajax_response_builder']
tags:
- { name: event_subscriber }
route_enhancer.lazy_collector:
class: Drupal\Core\Routing\LazyRouteEnhancer
tags:
......@@ -888,6 +893,9 @@ services:
controller.entity_form:
class: Drupal\Core\Entity\HtmlEntityFormController
arguments: ['@controller_resolver', '@form_builder', '@entity.manager']
form_ajax_response_builder:
class: Drupal\Core\Form\FormAjaxResponseBuilder
arguments: ['@main_content_renderer.ajax', '@current_route_match']
router_listener:
class: Symfony\Component\HttpKernel\EventListener\RouterListener
tags:
......
<?php
/**
* @file
* Contains \Drupal\Core\Form\EventSubscriber\FormAjaxSubscriber.
*/
namespace Drupal\Core\Form\EventSubscriber;
use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
use Drupal\Core\Form\FormAjaxException;
use Drupal\Core\Form\FormAjaxResponseBuilderInterface;
use Drupal\Core\Form\FormBuilderInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent;
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
use Symfony\Component\HttpKernel\KernelEvents;
/**
* Wraps AJAX form submissions that are triggered via an exception.
*/
class FormAjaxSubscriber implements EventSubscriberInterface {
/**
* The form AJAX response builder.
*
* @var \Drupal\Core\Form\FormAjaxResponseBuilderInterface
*/
protected $formAjaxResponseBuilder;
/**
* Constructs a new FormAjaxSubscriber.
*
* @param \Drupal\Core\Form\FormAjaxResponseBuilderInterface $form_ajax_response_builder
* The form AJAX response builder.
*/
public function __construct(FormAjaxResponseBuilderInterface $form_ajax_response_builder) {
$this->formAjaxResponseBuilder = $form_ajax_response_builder;
}
/**
* Alters the wrapper format if this is an AJAX form request.
*
* @param \Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent $event
* The event to process.
*/
public function onView(GetResponseForControllerResultEvent $event) {
// To support an AJAX form submission of a form within a block, make the
// later VIEW subscribers process the controller result as though for
// HTML display (i.e., add blocks). During that block building, when the
// submitted form gets processed, an exception gets thrown by
// \Drupal\Core\Form\FormBuilderInterface::buildForm(), allowing
// self::onException() to return an AJAX response instead of an HTML one.
$request = $event->getRequest();
if ($request->query->has(FormBuilderInterface::AJAX_FORM_REQUEST)) {
$request->query->set(MainContentViewSubscriber::WRAPPER_FORMAT, 'html');
}
}
/**
* Catches a form AJAX exception and build a response from it.
*
* @param \Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent $event
* The event to process.
*/
public function onException(GetResponseForExceptionEvent $event) {
// Extract the form AJAX exception (it may have been passed to another
// exception before reaching here).
if ($exception = $this->getFormAjaxException($event->getException())) {
$request = $event->getRequest();
$form = $exception->getForm();
$form_state = $exception->getFormState();
// Set the build ID from the request as the old build ID on the form.
$form['#build_id_old'] = $request->get('form_build_id');
try {
$response = $this->formAjaxResponseBuilder->buildResponse($request, $form, $form_state, []);
// Since this response is being set in place of an exception, explicitly
// mark this as a 200 status.
$response->headers->set('X-Status-Code', 200);
$event->setResponse($response);
}
catch (\Exception $e) {
// Otherwise, replace the existing exception with the new one.
$event->setException($e);
}
}
}
/**
* Extracts a form AJAX exception.
*
* @param \Exception $e
* A generic exception that might contain a form AJAX exception.
*
* @return \Drupal\Core\Form\FormAjaxException|null
* Either the form AJAX exception, or NULL if none could be found.
*/
protected function getFormAjaxException(\Exception $e) {
$exception = NULL;
while ($e) {
if ($e instanceof FormAjaxException) {
$exception = $e;
break;
}
$e = $e->getPrevious();
}
return $exception;
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
// Run before exception.logger.
$events[KernelEvents::EXCEPTION] = ['onException', 51];
// Run before main_content_view_subscriber.
$events[KernelEvents::VIEW][] = ['onView', 1];
return $events;
}
}
<?php
/**
* @file
* Contains \Drupal\Core\Form\FormAjaxException.
*/
namespace Drupal\Core\Form;
/**
* Custom exception to break out of AJAX form processing.
*/
class FormAjaxException extends \Exception {
/**
* The form definition.
*
* @var array
*/
protected $form;
/**
* The form state.
*
* @var \Drupal\Core\Form\FormStateInterface
*/
protected $formState;
/**
* Constructs a FormAjaxException object.
*
* @param array $form
* The form definition.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
* @param string $message
* (optional) The exception message.
* @param int $code
* (optional) A user defined exception code.
* @param \Exception $previous
* (optional) The previous exception for nested exceptions.
*/
public function __construct(array $form, FormStateInterface $form_state, $message = "", $code = 0, \Exception $previous = NULL) {
parent::__construct($message, $code, $previous);
$this->form = $form;
$this->formState = $form_state;
}
/**
* Gets the form definition.
*
* @return array
* The form structure.
*/
public function getForm() {
return $this->form;
}
/**
* Gets the form state.
*
* @return \Drupal\Core\Form\FormStateInterface
* The current state of the form.
*/
public function getFormState() {
return $this->formState;
}
}
<?php
/**
* @file
* Contains \Drupal\Core\Form\FormAjaxResponseBuilder.
*/
namespace Drupal\Core\Form;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\UpdateBuildIdCommand;
use Drupal\Core\Render\MainContent\MainContentRendererInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\HttpException;
/**
* Builds an AJAX form response.
*
* Given the current request, a form render array, its form state, and any AJAX
* commands to apply to the form, build a response object.
*/
class FormAjaxResponseBuilder implements FormAjaxResponseBuilderInterface {
/**
* The main content to AJAX Response renderer.
*
* @var \Drupal\Core\Render\MainContent\MainContentRendererInterface
*/
protected $ajaxRenderer;
/**
* The current route match.
*
* @var \Drupal\Core\Routing\RouteMatchInterface
*/
protected $routeMatch;
/**
* Constructs a new FormAjaxResponseBuilder.
*
* @param \Drupal\Core\Render\MainContent\MainContentRendererInterface $ajax_renderer
* The ajax renderer.
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The current route match.
*/
public function __construct(MainContentRendererInterface $ajax_renderer, RouteMatchInterface $route_match) {
$this->ajaxRenderer = $ajax_renderer;
$this->routeMatch = $route_match;
}
/**
* {@inheritdoc}
*/
public function buildResponse(Request $request, array $form, FormStateInterface $form_state, array $commands) {
// If the form build ID has changed, issue an Ajax command to update it.
if (isset($form['#build_id_old']) && $form['#build_id_old'] !== $form['#build_id']) {
$commands[] = new UpdateBuildIdCommand($form['#build_id_old'], $form['#build_id']);
}
// We need to return the part of the form (or some other content) that needs
// to be re-rendered so the browser can update the page with changed
// content. It is up to the #ajax['callback'] function of the element (may
// or may not be a button) that triggered the Ajax request to determine what
// needs to be rendered.
$callback = NULL;
if (($triggering_element = $form_state->getTriggeringElement()) && isset($triggering_element['#ajax']['callback'])) {
$callback = $triggering_element['#ajax']['callback'];
}
$callback = $form_state->prepareCallback($callback);
if (empty($callback) || !is_callable($callback)) {
throw new HttpException(500, 'The specified #ajax callback is empty or not callable.');
}
$result = call_user_func_array($callback, [&$form, &$form_state]);
// If the callback is an #ajax callback, the result is a render array, and
// we need to turn it into an AJAX response, so that we can add any commands
// we got earlier; typically the UpdateBuildIdCommand when handling an AJAX
// submit from a cached page.
if ($result instanceof AjaxResponse) {
$response = $result;
}
else {
/** @var \Drupal\Core\Ajax\AjaxResponse $response */
$response = $this->ajaxRenderer->renderResponse($result, $request, $this->routeMatch);
}
foreach ($commands as $command) {
$response->addCommand($command, TRUE);
}
return $response;
}
}
<?php
/**
* @file
* Contains \Drupal\Core\Form\FormAjaxResponseBuilderInterface.
*/
namespace Drupal\Core\Form;
use Symfony\Component\HttpFoundation\Request;
/**
* Provides an interface for building AJAX form responses.
*/
interface FormAjaxResponseBuilderInterface {
/**
* Builds a response for an AJAX form.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The current request.
* @param array $form
* An associative array containing the structure of the form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
* @param array $commands
* An array of AJAX commands to apply to the form.
*
* @return \Drupal\Core\Ajax\AjaxResponse
* An AJAX response representing the form and its AJAX commands.
*
* @throws \Symfony\Component\HttpKernel\Exception\HttpException
* Thrown if the AJAX callback is not a callable.
*/
public function buildResponse(Request $request, array $form, FormStateInterface $form_state, array $commands);
}
......@@ -14,6 +14,7 @@
use Drupal\Component\Utility\UrlHelper;
use Drupal\Core\Access\CsrfTokenGenerator;
use Drupal\Core\DependencyInjection\ClassResolverInterface;
use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\Render\ElementInfoManagerInterface;
......@@ -244,6 +245,12 @@ public function buildForm($form_id, FormStateInterface &$form_state) {
}
}
// If this form is an AJAX request, disable all form redirects.
$request = $this->requestStack->getCurrentRequest();
if ($ajax_form_request = $request->query->has(static::AJAX_FORM_REQUEST)) {
$form_state->disableRedirect();
}
// Now that we have a constructed form, process it. This is where:
// - Element #process functions get called to further refine $form.
// - User input, if any, gets incorporated in the #value property of the
......@@ -257,6 +264,17 @@ public function buildForm($form_id, FormStateInterface &$form_state) {
// can use it to know or update information about the state of the form.
$response = $this->processForm($form_id, $form, $form_state);
// After processing the form, if this is an AJAX form request, interrupt
// form rendering and return by throwing an exception that contains the
// processed form and form state. This exception will be caught by
// \Drupal\Core\Form\EventSubscriber\FormAjaxSubscriber::onException() and
// then passed through
// \Drupal\Core\Form\FormAjaxResponseBuilderInterface::buildResponse() to
// build a proper AJAX response.
if ($ajax_form_request && $form_state->isProcessingInput()) {
throw new FormAjaxException($form, $form_state);
}
// If the form returns a response, skip subsequent page construction by
// throwing an exception.
// @see Drupal\Core\EventSubscriber\EnforcedFormResponseSubscriber
......@@ -559,7 +577,7 @@ public function prepareForm($form_id, &$form, FormStateInterface &$form_state) {
// Only update the action if it is not already set.
if (!isset($form['#action'])) {
$form['#action'] = $this->requestStack->getMasterRequest()->getRequestUri();
$form['#action'] = $this->buildFormAction();
}
// Fix the form method, if it is 'get' in $form_state, but not in $form.
......@@ -659,6 +677,24 @@ public function prepareForm($form_id, &$form, FormStateInterface &$form_state) {
$this->themeManager->alter($hooks, $form, $form_state, $form_id);
}
/**
* Builds the $form['#action'].
*
* @return string
* The URL to be used as the $form['#action'].
*/
protected function buildFormAction() {
// @todo Use <current> instead of the master request in
// https://www.drupal.org/node/2505339.
$request_uri = $this->requestStack->getMasterRequest()->getRequestUri();
// @todo Remove this parsing once these are removed from the request in
// https://www.drupal.org/node/2504709.
$parsed = UrlHelper::parse($request_uri);
unset($parsed['query'][static::AJAX_FORM_REQUEST], $parsed['query'][MainContentViewSubscriber::WRAPPER_FORMAT]);
return $parsed['path'] . ($parsed['query'] ? ('?' . UrlHelper::buildQuery($parsed['query'])) : '');
}
/**
* {@inheritdoc}
*/
......
......@@ -12,6 +12,21 @@
*/
interface FormBuilderInterface {
/**
* Request key for AJAX forms that submit to the form's original route.
*
* This constant is distinct from a "drupal_ajax" value for
* \Drupal\Core\EventSubscriber\MainContentViewSubscriber::WRAPPER_FORMAT,
* because that one is set for all AJAX submissions, including ones with
* dedicated routes for which self::buildForm() should not exit early via a
* \Drupal\Core\Form\FormAjaxException.
*
* @todo Re-evaluate the need for this constant after
* https://www.drupal.org/node/2502785 and
* https://www.drupal.org/node/2503429.
*/
const AJAX_FORM_REQUEST = 'ajax_form';
/**
* Determines the ID of a form.
*
......@@ -69,6 +84,14 @@ public function getForm($form_arg);
* The rendered form. This function may also perform a redirect and hence
* may not return at all depending upon the $form_state flags that were set.
*
* @throws \Drupal\Core\Form\FormAjaxException
* Thrown when a form is triggered via an AJAX submission. It will be
* handled by \Drupal\Core\Form\EventSubscriber\FormAjaxSubscriber.
* @throws \Drupal\Core\Form\EnforcedResponseException
* Thrown when a form builder returns a response directly, usually a
* \Symfony\Component\HttpFoundation\RedirectResponse. It will be handled by
* \Drupal\Core\EventSubscriber\EnforcedFormResponseSubscriber.
*
* @see self::redirectForm()
*/
public function buildForm($form_id, FormStateInterface &$form_state);
......
......@@ -7,6 +7,7 @@
namespace Drupal\Core\Render\Element;
use Drupal\Core\Form\FormBuilderInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\PluginBase;
use Drupal\Core\Render\Element;
......@@ -128,7 +129,10 @@ public static function preRenderGroup($element) {
*/
public static function processAjaxForm(&$element, FormStateInterface $form_state, &$complete_form) {
$element = static::preRenderAjaxForm($element);
if (!empty($element['#ajax_processed'])) {
// If the element was processed as an #ajax element, and a custom URL was
// provided, set the form to be cached.
if (!empty($element['#ajax_processed']) && !empty($element['#ajax']['url'])) {
$form_state->setCached();
}
return $element;
......@@ -236,12 +240,22 @@ public static function preRenderAjaxForm($element) {
// to be substantially different for a JavaScript triggered submission.
// One such substantial difference is form elements that use
// #ajax['callback'] for determining which part of the form needs
// re-rendering. For that, we have a special 'system/ajax' route.
$settings += array(
'url' => isset($settings['callback']) ? Url::fromRoute('system.ajax') : NULL,
'options' => array(),
// re-rendering. For that, we have a special 'system.ajax' route which
// must be manually set.
$settings += [
'url' => NULL,
'options' => ['query' => []],
'dialogType' => 'ajax',
);
];
if (array_key_exists('callback', $settings) && !isset($settings['url'])) {
$settings['url'] = Url::fromRoute('<current>');
// Add all the current query parameters in order to ensure that we build
// the same form on the AJAX POST requests. For example,
// \Drupal\user\AccountForm takes query parameters into account in order
// to hide the password field dynamically.
$settings['options']['query'] += \Drupal::request()->query->all();
$settings['options']['query'][FormBuilderInterface::AJAX_FORM_REQUEST] = TRUE;
}
// @todo Legacy support. Remove in Drupal 8.
if (isset($settings['method']) && $settings['method'] == 'replace') {
......
......@@ -435,7 +435,7 @@ function testFieldFormJSAddMore() {
// Press 'add more' button through Ajax, and place the expected HTML result
// as the tested content.
$commands = $this->drupalPostAjaxForm(NULL, $edit, $field_name . '_add_more');
$this->setRawContent($commands[1]['data']);
$this->setRawContent($commands[2]['data']);
for ($delta = 0; $delta <= $delta_range; $delta++) {
$this->assertFieldByName("{$field_name}[$delta][value]", $values[$delta], "Widget $delta is displayed and has the right value");
......
......@@ -145,6 +145,7 @@ public static function processManagedFile(&$element, FormStateInterface $form_st
$element['#tree'] = TRUE;
$ajax_settings = [
// @todo Remove this in https://www.drupal.org/node/2500527.
'url' => Url::fromRoute('file.ajax_upload'),
'options' => [
'query' => [
......
......@@ -7,9 +7,8 @@
namespace Drupal\system\Controller;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\UpdateBuildIdCommand;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Form\FormAjaxResponseBuilderInterface;
use Drupal\Core\Form\FormState;
use Drupal\Core\Form\FormBuilderInterface;
use Drupal\Core\Render\MainContent\MainContentRendererInterface;
......@@ -20,7 +19,6 @@
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpKernel\Exception\HttpException;
/**
* Defines a controller to respond to form Ajax requests.
......@@ -62,6 +60,13 @@ class FormAjaxController implements ContainerInjectionInterface {
*/
protected $routeMatch;
/**
* The form AJAX response builder.
*
* @var \Drupal\Core\Form\FormAjaxResponseBuilderInterface
*/
protected $formAjaxResponseBuilder;
/**
* Constructs a FormAjaxController object.
*
......@@ -73,15 +78,18 @@ class FormAjaxController implements ContainerInjectionInterface {
* The renderer.
* @param \Drupal\Core\Render\MainContent\MainContentRendererInterface $ajax_renderer
* The main content to AJAX Response renderer.
* @param \Drupal\Core\Routing\RouteMatchInterface
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The current route match.
* @param \Drupal\Core\Form\FormAjaxResponseBuilderInterface $form_ajax_response_builder
* The form AJAX response builder.
*/
public function __construct(LoggerInterface $logger, FormBuilderInterface $form_builder, RendererInterface $renderer, MainContentRendererInterface $ajax_renderer, RouteMatchInterface $route_match) {
public function __construct(LoggerInterface $logger, FormBuilderInterface $form_builder, RendererInterface $renderer, MainContentRendererInterface $ajax_renderer, RouteMatchInterface $route_match, FormAjaxResponseBuilderInterface $form_ajax_response_builder) {
$this->logger = $logger;
$this->formBuilder = $form_builder;
$this->renderer = $renderer;
$this->ajaxRenderer = $ajax_renderer;
$this->routeMatch = $route_match;
$this->formAjaxResponseBuilder = $form_ajax_response_builder;
}
/**
......@@ -93,7 +101,8 @@ public static function create(ContainerInterface $container) {
$container->get('form_builder'),
$container->get('renderer'),
$container->get('main_content_renderer.ajax'),
$container->get('current_route_match')
$container->get('current_route_match'),
$container->get('form_ajax_response_builder')
);
}
......@@ -121,38 +130,7 @@ public function content(Request $request) {
$this->formBuilder->processForm($form['#form_id'], $form, $form_state);
// We need to return the part of the form (or some other content) that needs
// to be re-rendered so the browser can update the page with changed content.
// Since this is the generic menu callback used by many Ajax elements, it is
// up to the #ajax['callback'] function of the element (may or may not be a
// button) that triggered the Ajax request to determine what needs to be
// rendered.
$callback = NULL;
if ($triggering_element = $form_state->getTriggeringElement()) {
$callback = $triggering_element['#ajax']['callback'];
}
$callback = $form_state->prepareCallback($callback);
if (empty($callback) || !is_callable($callback)) {
throw new HttpException(500, 'The specified #ajax callback is empty or not callable.');
}
$result = call_user_func_array($callback, [&$form, &$form_state]);
// If the callback is an #ajax callback, the result is a render array, and
// we need to turn it into an AJAX response, so that we can add any commands
// we got earlier; typically the UpdateBuildIdCommand when handling an AJAX
// submit from a cached page.
if ($result instanceof AjaxResponse) {
$response = $result;
}
else {
/** @var \Drupal\Core\Ajax\AjaxResponse $response */
$response = $this->ajaxRenderer->renderResponse($result, $request, $this->routeMatch);
}
foreach ($commands as $command) {
$response->addCommand($command, TRUE);
}
return $response;
return $this->formAjaxResponseBuilder->buildResponse($request, $form, $form_state, $commands);
}
/**
......@@ -186,17 +164,6 @@ protected function getForm(Request $request) {
throw new BadRequestHttpException();
}
// When a page level cache is enabled, the form-build id might have been
// replaced from within \Drupal::formBuilder()->getCache(). If this is the
// case, it is also necessary to update it in the browser by issuing an
// appropriate Ajax command.
$commands = [];
if (isset($form['#build_id_old']) && $form['#build_id_old'] != $form['#build_id']) {
// If the form build ID has changed, issue an Ajax command to update it.
$commands[] = new UpdateBuildIdCommand($form['#build_id_old'], $form['#build_id']);
$form_build_id = $form['#build_id'];
}
// Since some of the submit handlers are run, redirects need to be disabled.
$form_state->disableRedirect();
......@@ -213,7 +180,7 @@ protected function getForm(Request $request) {
$form_state->setUserInput($request->request->all());
$form_id = $form['#form_id'];
return new FileAjaxForm($form, $form_state, $form_id, $form_build_id, $commands);
return new FileAjaxForm($form, $form_state, $form_id, $form['#build_id'], []);
}
}
<?php
/**
* @file
* Contains \Drupal\system\Tests\Ajax\AjaxFormCacheTest.
*/
namespace Drupal\system\Tests\Ajax;
use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
use Drupal\Core\Form\FormBuilderInterface;
use Drupal\Core\Url;
/**
* Tests the usage of form caching for AJAX forms.
*
* @group Ajax
*/
class AjaxFormCacheTest extends AjaxTestBase {
/**
* Tests the usage of form cache for AJAX forms.
*/
public function testFormCacheUsage() {
/** @var \Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface $key_value_expirable */
$key_value_expirable = \Drupal::service('keyvalue.expirable')->get('form');
$this->drupalLogin($this->rootUser);
// Ensure that the cache is empty.
$this->assertEqual(0, count($key_value_expirable->getAll()));
// Visit an AJAX form that is not cached, 3 times.
$uncached_form_url = Url::fromRoute('ajax_forms_test.commands_form');
$this->drupalGet($uncached_form_url);
$this->drupalGet($uncached_form_url);
$this->drupalGet($uncached_form_url);
// The number of cache entries should not have changed.
$this->assertEqual(0, count($key_value_expirable->getAll()));
// Visit a form that is explicitly cached, 3 times.
$cached_form_url = Url::fromRoute('ajax_forms_test.cached_form');
$this->drupalGet($cached_form_url);
$this->drupalGet($cached_form_url);
$this->drupalGet($cached_form_url);
// The number of cache entries should be exactly 3.
$this->assertEqual(3, count($key_value_expirable->getAll()));
}
/**
* Tests AJAX forms in blocks.
*/
public function testBlockForms() {
$this->container->get('module_installer')->install(['block', 'search']);
$this->rebuildContainer();
$this->container->get('router.builder')->rebuild();
$this->drupalLogin($this->rootUser);