Commit 42ac4e50 authored by webchick's avatar webchick

Issue #2147669 by tim.plunkett, dawehner: Convert exposed forms to use FormInterface.

parent d9f2ba2c
......@@ -192,10 +192,13 @@ public function getForm($form_arg) {
/**
* {@inheritdoc}
*/
public function buildForm($form_id, &$form_state) {
public function buildForm($form_id, array &$form_state) {
// Ensure some defaults; if already set they will not be overridden.
$form_state += $this->getFormStateDefaults();
// Ensure the form ID is prepared.
$form_id = $this->getFormId($form_id, $form_state);
if (!isset($form_state['input'])) {
$form_state['input'] = $form_state['method'] == 'get' ? $_GET : $_POST;
}
......
......@@ -73,7 +73,7 @@ public function getForm($form_arg);
* can implement hook_forms(), which maps different $form_id values to the
* proper form constructor function. Examples may be found in node_forms(),
* and search_forms().
* @param $form_state
* @param array $form_state
* An array which stores information about the form. This is passed as a
* reference so that the caller can use it to examine what in the form
* changed when the form submission process is complete. Furthermore, it may
......@@ -230,7 +230,7 @@ public function getForm($form_arg);
*
* @see self::redirectForm()
*/
public function buildForm($form_id, &$form_state);
public function buildForm($form_id, array &$form_state);
/**
* Retrieves default values for the $form_state array.
......
<?php
/**
* @file
* Contains \Drupal\views\ExposedFormCache.
*/
namespace Drupal\views;
/**
* Caches exposed forms, as they are heavy to generate.
*
* @see \Drupal\views\Form\ViewsExposedForm
*/
class ExposedFormCache {
/**
* Stores the exposed form data.
*
* @var array
*/
protected $cache = array();
/**
* Save the Views exposed form for later use.
*
* @param string $view_id
* The views ID.
* @param string $display_id
* The current view display name.
* @param array $form_output
* The form structure. Only needed when inserting the value.
*/
public function setForm($view_id, $display_id, array $form_output) {
// Save the form output.
$views_exposed[$view_id][$display_id] = $form_output;
}
/**
* Retrieves the views exposed form from cache.
*
* @param string $view_id
* The views ID.
* @param string $display_id
* The current view display name.
*
* @return array|bool
* The form structure, if any, otherwise FALSE.
*/
public function getForm($view_id, $display_id) {
// Return the form output, if any.
if (empty($this->cache[$view_id][$display_id])) {
return FALSE;
}
else {
return $this->cache[$view_id][$display_id];
}
}
/**
* Rests the form cache.
*/
public function reset() {
$this->cache = array();
}
}
<?php
/**
* @file
* Contains \Drupal\views\Form\ViewsExposedForm.
*/
namespace Drupal\views\Form;
use Drupal\Component\Utility\String;
use Drupal\Core\Form\FormBase;
use Drupal\views\ExposedFormCache;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides the views exposed form.
*/
class ViewsExposedForm extends FormBase {
/**
* The exposed form cache.
*
* @var \Drupal\views\ExposedFormCache
*/
protected $exposedFormCache;
/**
* Constructs a new ViewsExposedForm
*
* @param \Drupal\views\ExposedFormCache $exposed_form_cache
* The exposed form cache.
*/
public function __construct(ExposedFormCache $exposed_form_cache) {
$this->exposedFormCache = $exposed_form_cache;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static($container->get('views.exposed_form_cache'));
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'views_exposed_form';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, array &$form_state) {
// Don't show the form when batch operations are in progress.
if ($batch = batch_get() && isset($batch['current_set'])) {
return array(
// Set the theme callback to be nothing to avoid errors in template_preprocess_views_exposed_form().
'#theme' => '',
);
}
// Make sure that we validate because this form might be submitted
// multiple times per page.
$form_state['must_validate'] = TRUE;
/** @var \Drupal\views\ViewExecutable $view */
$view = $form_state['view'];
$display = &$form_state['display'];
$form_state['input'] = $view->getExposedInput();
// Let form plugins know this is for exposed widgets.
$form_state['exposed'] = TRUE;
// Check if the form was already created
if ($cache = $this->exposedFormCache->getForm($view->storage->id(), $view->current_display)) {
return $cache;
}
$form['#info'] = array();
// Go through each handler and let it generate its exposed widget.
foreach ($view->display_handler->handlers as $type => $value) {
/** @var \Drupal\views\Plugin\views\HandlerBase $handler */
foreach ($view->$type as $id => $handler) {
if ($handler->canExpose() && $handler->isExposed()) {
// Grouped exposed filters have their own forms.
// Instead of render the standard exposed form, a new Select or
// Radio form field is rendered with the available groups.
// When an user choose an option the selected value is split
// into the operator and value that the item represents.
if ($handler->isAGroup()) {
$handler->groupForm($form, $form_state);
$id = $handler->options['group_info']['identifier'];
}
else {
$handler->buildExposedForm($form, $form_state);
}
if ($info = $handler->exposedInfo()) {
$form['#info']["$type-$id"] = $info;
}
}
}
}
$form['actions'] = array(
'#type' => 'actions'
);
$form['actions']['submit'] = array(
// Prevent from showing up in \Drupal::request()->query.
'#name' => '',
'#type' => 'submit',
'#value' => $this->t('Apply'),
'#id' => drupal_html_id('edit-submit-' . $view->storage->id()),
);
$form['#action'] = url($view->display_handler->getUrl());
$form['#theme'] = $view->buildThemeFunctions('views_exposed_form');
$form['#id'] = drupal_clean_css_identifier('views_exposed_form-' . String::checkPlain($view->storage->id()) . '-' . String::checkPlain($display['id']));
// $form['#attributes']['class'] = array('views-exposed-form');
/** @var \Drupal\views\Plugin\views\exposed_form\ExposedFormPluginBase $exposed_form_plugin */
$exposed_form_plugin = $form_state['exposed_form_plugin'];
$exposed_form_plugin->exposedFormAlter($form, $form_state);
// Save the form.
$this->exposedFormCache->setForm($view->storage->id(), $view->current_display, $form);
return $form;
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, array &$form_state) {
foreach (array('field', 'filter') as $type) {
/** @var \Drupal\views\Plugin\views\HandlerBase[] $handlers */
$handlers = &$form_state['view']->$type;
foreach ($handlers as $key => $handler) {
$handlers[$key]->validateExposed($form, $form_state);
}
}
/** @var \Drupal\views\Plugin\views\exposed_form\ExposedFormPluginBase $exposed_form_plugin */
$exposed_form_plugin = $form_state['exposed_form_plugin'];
$exposed_form_plugin->exposedFormValidate($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, array &$form_state) {
foreach (array('field', 'filter') as $type) {
/** @var \Drupal\views\Plugin\views\HandlerBase[] $handlers */
$handlers = &$form_state['view']->$type;
foreach ($handlers as $key => $info) {
$handlers[$key]->submitExposed($form, $form_state);
}
}
$form_state['view']->exposed_data = $form_state['values'];
$form_state['view']->exposed_raw_input = array();
$exclude = array('submit', 'form_build_id', 'form_id', 'form_token', 'exposed_form_plugin', '', 'reset');
/** @var \Drupal\views\Plugin\views\exposed_form\ExposedFormPluginBase $exposed_form_plugin */
$exposed_form_plugin = $form_state['exposed_form_plugin'];
$exposed_form_plugin->exposedFormSubmit($form, $form_state, $exclude);
foreach ($form_state['values'] as $key => $value) {
if (!in_array($key, $exclude)) {
$form_state['view']->exposed_raw_input[$key] = $value;
}
}
}
}
......@@ -7,6 +7,7 @@
namespace Drupal\views\Plugin\views\exposed_form;
use Drupal\views\Form\ViewsExposedForm;
use Drupal\views\ViewExecutable;
use Drupal\views\Plugin\views\display\DisplayPluginBase;
use Drupal\views\Plugin\views\PluginBase;
......@@ -138,7 +139,7 @@ public function renderExposedForm($block = FALSE) {
}
$form_state['exposed_form_plugin'] = $this;
$form = drupal_build_form('views_exposed_form', $form_state);
$form = \Drupal::formBuilder()->buildForm('\Drupal\views\Form\ViewsExposedForm', $form_state);
if (!$this->view->display_handler->displaysExposed() || (!$block && $this->view->display_handler->getOption('exposed_block'))) {
return array();
......
......@@ -86,7 +86,7 @@ protected function getBasicPageView() {
// In order to test exposed filters, we have to disable
// the exposed forms cache.
drupal_static_reset('views_exposed_form_cache');
\Drupal::service('views.exposed_form_cache')->reset();
$view->newDisplay('page', 'Page', 'page_1');
return $view;
......
......@@ -1052,83 +1052,6 @@ function views_pre_render_views_form_views_form($element) {
return $element;
}
/**
* Form builder for the exposed widgets form.
*
* Be sure that $display is a reference.
*/
function views_exposed_form($form, &$form_state) {
// Don't show the form when batch operations are in progress.
if ($batch = batch_get() && isset($batch['current_set'])) {
return array(
// Set the theme callback to be nothing to avoid errors in template_preprocess_views_exposed_form().
'#theme' => '',
);
}
// Make sure that we validate because this form might be submitted
// multiple times per page.
$form_state['must_validate'] = TRUE;
$view = $form_state['view'];
$display = &$form_state['display'];
$form_state['input'] = $view->getExposedInput();
// Let form plugins know this is for exposed widgets.
$form_state['exposed'] = TRUE;
// Check if the form was already created
if ($cache = views_exposed_form_cache($view->storage->id(), $view->current_display)) {
return $cache;
}
$form['#info'] = array();
// Go through each handler and let it generate its exposed widget.
foreach ($view->display_handler->handlers as $type => $value) {
foreach ($view->$type as $id => $handler) {
if ($handler->canExpose() && $handler->isExposed()) {
// Grouped exposed filters have their own forms.
// Instead of render the standard exposed form, a new Select or
// Radio form field is rendered with the available groups.
// When an user choose an option the selected value is split
// into the operator and value that the item represents.
if ($handler->isAGroup()) {
$handler->groupForm($form, $form_state);
$id = $handler->options['group_info']['identifier'];
}
else {
$handler->buildExposedForm($form, $form_state);
}
if ($info = $handler->exposedInfo()) {
$form['#info']["$type-$id"] = $info;
}
}
}
}
$form['actions'] = array('#type' => 'actions');
$form['actions']['submit'] = array(
// Prevent from showing up in \Drupal::request()->query.
'#name' => '',
'#type' => 'submit',
'#value' => t('Apply'),
'#id' => drupal_html_id('edit-submit-' . $view->storage->id()),
);
$form['#action'] = url($view->display_handler->getUrl());
$form['#theme'] = $view->buildThemeFunctions('views_exposed_form');
$form['#id'] = drupal_clean_css_identifier('views_exposed_form-' . check_plain($view->storage->id()) . '-' . check_plain($display['id']));
// $form['#attributes']['class'] = array('views-exposed-form');
$exposed_form_plugin = $form_state['exposed_form_plugin'];
$exposed_form_plugin->exposedFormAlter($form, $form_state);
// Save the form
views_exposed_form_cache($view->storage->id(), $view->current_display, $form);
return $form;
}
/**
* Implement hook_form_alter for the exposed form.
*
......@@ -1141,71 +1064,6 @@ function views_form_views_exposed_form_alter(&$form, &$form_state) {
$form['form_id']['#access'] = FALSE;
}
/**
* Validate handler for exposed filters
*/
function views_exposed_form_validate(&$form, &$form_state) {
foreach (array('field', 'filter') as $type) {
$handlers = &$form_state['view']->$type;
foreach ($handlers as $key => $handler) {
$handlers[$key]->validateExposed($form, $form_state);
}
}
$exposed_form_plugin = $form_state['exposed_form_plugin'];
$exposed_form_plugin->exposedFormValidate($form, $form_state);
}
/**
* Submit handler for exposed filters
*/
function views_exposed_form_submit(&$form, &$form_state) {
foreach (array('field', 'filter') as $type) {
$handlers = &$form_state['view']->$type;
foreach ($handlers as $key => $info) {
$handlers[$key]->submitExposed($form, $form_state);
}
}
$form_state['view']->exposed_data = $form_state['values'];
$form_state['view']->exposed_raw_input = array();
$exclude = array('submit', 'form_build_id', 'form_id', 'form_token', 'exposed_form_plugin', '', 'reset');
$exposed_form_plugin = $form_state['exposed_form_plugin'];
$exposed_form_plugin->exposedFormSubmit($form, $form_state, $exclude);
foreach ($form_state['values'] as $key => $value) {
if (!in_array($key, $exclude)) {
$form_state['view']->exposed_raw_input[$key] = $value;
}
}
}
/**
* Save the Views exposed form for later use.
*
* @param $views_name
* String. The views name.
* @param $display_name
* String. The current view display name.
* @param $form_output
* Array (optional). The form structure. Only needed when inserting the value.
* @return
* Array. The form structure, if any. Otherwise, return FALSE.
*/
function views_exposed_form_cache($views_name, $display_name, $form_output = NULL) {
// When running tests for exposed filters, this cache should
// be cleared between each test.
$views_exposed = &drupal_static(__FUNCTION__);
// Save the form output
if (!empty($form_output)) {
$views_exposed[$views_name][$display_name] = $form_output;
return;
}
// Return the form output, if any
return empty($views_exposed[$views_name][$display_name]) ? FALSE : $views_exposed[$views_name][$display_name];
}
/**
* Implements hook_query_TAG_alter().
*
......
......@@ -92,3 +92,5 @@ services:
class: Drupal\views\ViewsAccessCheck
tags:
- { name: 'access_check' }
views.exposed_form_cache:
class: Drupal\views\ExposedFormCache
......@@ -25,7 +25,7 @@ class FormBuilderTest extends UnitTestCase {
/**
* The form builder being tested.
*
* @var \Drupal\Core\Form\FormBuilderInterface
* @var \Drupal\Core\Form\FormBuilder
*/
protected $formBuilder;
......@@ -354,19 +354,59 @@ public function testGetFormWithObject() {
}
/**
* Tests the buildForm() method with a form object.
* Tests the getForm() method with a class name based form ID.
*/
public function testBuildFormWithObject() {
public function testGetFormWithClassString() {
$form_id = '\Drupal\Tests\Core\Form\TestForm';
$object = new TestForm();
$form = array();
$form_state = array();
$expected_form = $object->buildForm($form, $form_state);
$form = $this->formBuilder->getForm($form_id);
$this->assertFormElement($expected_form, $form, 'test');
$this->assertSame('test_form', $form['#id']);
}
/**
* Tests the buildForm() method with a string based form ID.
*/
public function testBuildFormWithString() {
$form_id = 'test_form_id';
$expected_form = $form_id();
$form_arg = $this->getMockForm(NULL, $expected_form);
$form = $this->formBuilder->getForm($form_id);
$this->assertFormElement($expected_form, $form, 'test');
$this->assertSame($form_id, $form['#id']);
}
$form_state['build_info']['callback_object'] = $form_arg;
$form_state['build_info']['args'] = array();
/**
* Tests the buildForm() method with a class name based form ID.
*/
public function testBuildFormWithClassString() {
$form_id = '\Drupal\Tests\Core\Form\TestForm';
$object = new TestForm();
$form = array();
$form_state = array();
$expected_form = $object->buildForm($form, $form_state);
$form = $this->formBuilder->buildForm($form_id, $form_state);
$this->assertFormElement($expected_form, $form, 'test');
$this->assertSame('test_form', $form['#id']);
}
/**
* Tests the buildForm() method with a form object.
*/
public function testBuildFormWithObject() {
$form_id = 'test_form_id';
$expected_form = $form_id();
$form_arg = $this->getMockForm($form_id, $expected_form);
$form_state = array();
$form = $this->formBuilder->buildForm($form_arg, $form_state);
$this->assertFormElement($expected_form, $form, 'test');
$this->assertSame($form_id, $form_state['build_info']['form_id']);
$this->assertSame($form_id, $form['#id']);
}
......@@ -388,8 +428,7 @@ public function testBuildFormWithHookForms() {
),
)));
$form_state['build_info']['args'] = array();
$form_state = array();
$form = $this->formBuilder->buildForm($form_id, $form_state);
$this->assertFormElement($expected_form, $form, 'test');
$this->assertSame($form_id, $form_state['build_info']['form_id']);
......@@ -405,13 +444,17 @@ public function testRebuildForm() {
$expected_form = $form_id();
// The form will be built four times.
$form_arg = $this->getMockForm(NULL, $expected_form, 4);
$form_arg = $this->getMock('Drupal\Core\Form\FormInterface');
$form_arg->expects($this->exactly(2))
->method('getFormId')
->will($this->returnValue($form_id));
$form_arg->expects($this->exactly(4))
->method('buildForm')
->will($this->returnValue($expected_form));
// Do an initial build of the form and track the build ID.
$form_state = array();
$form_state['build_info']['callback_object'] = $form_arg;
$form_state['build_info']['args'] = array();
$form = $this->formBuilder->buildForm($form_id, $form_state);
$form = $this->formBuilder->buildForm($form_arg, $form_state);
$original_build_id = $form['#build_id'];
// Rebuild the form, and assert that the build ID has not changed.
......@@ -423,7 +466,7 @@ public function testRebuildForm() {
// Rebuild the form again, and assert that there is a new build ID.
$form_state['rebuild_info'] = array();
$form = $this->formBuilder->buildForm($form_id, $form_state);
$form = $this->formBuilder->buildForm($form_arg, $form_state);
$this->assertNotSame($original_build_id, $form['#build_id']);
}
......@@ -615,52 +658,54 @@ public function providerTestGetError() {
public function testGetCache() {
$form_id = 'test_form_id';
$expected_form = $form_id();
$expected_form['#token'] = FALSE;
// FormBuilder::buildForm() will be called 3 times, but the form object will
// only be called twice due to caching.
$form_arg = $this->getMockForm(NULL, $expected_form, 2);
// FormBuilder::buildForm() will be called twice, but the form object will
// only be called once due to caching.
$form_arg = $this->getMockForm($form_id, $expected_form, 1);
// The CSRF token and the user authentication are checked each time.
$this->csrfToken->expects($this->exactly(3))
// The CSRF token is checked each time.
$this->csrfToken->expects($this->exactly(2))
->method('get')
->will($this->returnValue('csrf_token'));
// The CSRF token is validated only when retrieving from the cache.
$this->csrfToken->expects($this->once())
->method('validate')
->with('csrf_token')
->will($this->returnValue(TRUE));
// The user is checked for authentication once for the form building and
// twice for each cache set.
$this->account->expects($this->exactly(3))
->method('isAuthenticated')
->will($this->returnValue(TRUE));
// Do an initial build of the form and track the build ID.
$form_state = array();
$form_state['build_info']['callback_object'] = $form_arg;
$form_state['build_info']['args'] = array();
$form_state['build_info']['files'] = array(array('module' => 'node', 'type' => 'pages.inc'));
$form_state['cache'] = TRUE;
$form = $this->formBuilder->buildForm($form_id, $form_state);
// Rebuild the form, this time setting it up to be cached.
$form_state['rebuild'] = TRUE;
$form_state['rebuild_info']['copy']['#build_id'] = TRUE;
$form_state['input']['form_token'] = $form['#token'];
$form_state['input']['form_id'] = $form_id;
$form_state['input']['form_build_id'] = $form['#build_id'];
$form = $this->formBuilder->buildForm($form_id, $form_state);
$form = $this->formBuilder->buildForm($form_arg, $form_state);
$cached_form = $form;
$cached_form['#cache_token'] = 'csrf_token';
// The form cache, form_state cache, and CSRF token validation will only be
// called on the cached form.
$this->formCache->expects($this->once())
->method('setWithExpire');
$this->formCache->expects($this->once())
->method('get')
->will($this->returnValue($cached_form));
$this->formStateCache->expects($this->once())
->method('get')
->will($this->returnValue($form_state));
$this->csrfToken->expects($this->once())
->method('validate')
->will($this->returnValue(TRUE));
// The final form build will not trigger any actual form building, but will
// use the form cache.
$form_state['input']['form_id'] = $form_id;
$form_state['input']['form_build_id'] = $form['#build_id'];
$this->formBuilder->buildForm($form_id, $form_state);
$errors = $this->formBuilder->getErrors($form_state);
$this->assertEmpty($errors);
}
/**
......@@ -677,13 +722,11 @@ public function testSendResponse() {
->method('prepare')
->will($this->returnValue($expected_form));
$form_arg = $this->getMockForm(NULL, $expected_form);
$form_arg = $this->getMockForm($form_id, $expected_form);
// Do an initial build of the form and track the build ID.
$form_state = array();
$form_state['build_info']['callback_object'] = $form_arg;
$form_state['build_info']['args'] = array();
$this->formBuilder->buildForm($form_id, $form_state);
$this->formBuilder->buildForm($form_arg, $form_state);
}
/**
......@@ -691,7 +734,7 @@ public function testSendResponse() {
*
* @param string $form_id
* (optional) The form ID to be used. If none is provided, the form will be
* set to expect that getFormId() will never be called.
* set with no expectation about getFormId().
* @param mixed $expected_form
* (optional) If provided, the expected form response for buildForm() to
* return. Defaults to NULL.
......@@ -702,17 +745,11 @@ public function testSendResponse() {
* @return \PHPUnit_Framework_MockObject_MockObject|\Drupal\Core\Form\FormInterface
* The mocked form object.
*/
protected function getMockForm($form_id = NULL, $expected_form = NULL, $count = 1) {
protected function getMockForm($form_id, $expected_form = NULL, $count = 1) {
$form = $this->getMock('Drupal\Core\Form\FormInterface');
if ($form_id) {
$form->expects($this->once())
->method('getFormId')
->will($this->returnValue($form_id));
}
else {
$form->expects($this->never())
->method('getFormId');
}
$form->expects($this->once())
->method('getFormId')
->will($this->returnValue($form_id));
if ($expected_form) {
$form->expects($this->exactly($count))
......@@ -840,7 +877,9 @@ public function getFormId() {
return 'test_form';
}
public function buildForm(array $form, array &$form_state) { }
public function buildForm(array $form, array &$form_state) {
return test_form_id();
}
public function validateForm(array &$form, array &$form_state) { }
public function submitForm(array &$form, array &$form_state) { }