В этой статье мы рассмотрим создание многошаговой формы в Drupal 8. Для краткости форма будет иметь только два шага в форме двух совершенно разных форм. Чтобы сохранить значения на всех этих этапах, мы будем использовать функциональность, предоставляемую ядром Drupal, для хранения временных и личных данных в нескольких запросах.
В Drupal 7 подобный подход может быть реализован с использованием кеша объектов cTools . В качестве альтернативы, существует опция сохранения данных через массив $form_state
как показано в этом руководстве .
Код, который мы пишем в этой статье, можно найти в этом репозитории вместе с большей частью работы над Drupal 8, которую мы выполняли до сих пор. Мы будем иметь дело с формами довольно много, поэтому я рекомендую проверить одну из предыдущих статей о Drupal 8, в которой мы говорим о формах.
План
Как я упоминал выше, наша многошаговая форма будет состоять из двух независимых форм с двумя простыми элементами в каждой. Пользователи смогут заполнить первый и перейти ко второй форме, где они могут либо вернуться к предыдущему шагу, либо заполнить его и нажать «Отправить». При перемещении между различными шагами ранее представленные значения сохраняются и используются для предварительного заполнения полей формы. Однако, если последняя форма отправлена, данные обрабатываются (не рассматриваются в этой статье) и удаляются из временного хранилища.
Технически, обе эти формы будут наследовать общие функциональные возможности от абстрактного класса форм, который мы будем называть MultistepFormBase
. Этот класс будет отвечать за внедрение необходимых зависимостей, создание форм, обработку конечного результата и всего остального, что необходимо и является общим для обоих.
Мы сгруппируем все классы форм вместе и поместим их в новую папку с именем Multistep
расположенную в каталоге плагинов Form
нашего demo
модуля (рядом со старой DemoForm
). Это просто для того, чтобы иметь чистую структуру и возможность быстро определить, какие формы являются частью нашего многоэтапного процесса форм.
Код
Начнем с формы базового класса. Я объясню, что здесь происходит после того, как мы увидим код.
MultistepFormBase.php:
/** * @file * Contains \Drupal\demo\Form\Multistep\MultistepFormBase. */ namespace Drupal\demo\Form\Multistep; use Drupal\Core\Form\FormBase; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Session\AccountInterface; use Drupal\Core\Session\SessionManagerInterface; use Drupal\user\PrivateTempStoreFactory; use Symfony\Component\DependencyInjection\ContainerInterface; abstract class MultistepFormBase extends FormBase { /** * @var \Drupal\user\PrivateTempStoreFactory */ protected $tempStoreFactory; /** * @var \Drupal\Core\Session\SessionManagerInterface */ private $sessionManager; /** * @var \Drupal\Core\Session\AccountInterface */ private $currentUser; /** * @var \Drupal\user\PrivateTempStore */ protected $store; /** * Constructs a \Drupal\demo\Form\Multistep\MultistepFormBase. * * @param \Drupal\user\PrivateTempStoreFactory $temp_store_factory * @param \Drupal\Core\Session\SessionManagerInterface $session_manager * @param \Drupal\Core\Session\AccountInterface $current_user */ public function __construct(PrivateTempStoreFactory $temp_store_factory, SessionManagerInterface $session_manager, AccountInterface $current_user) { $this->tempStoreFactory = $temp_store_factory; $this->sessionManager = $session_manager; $this->currentUser = $current_user; $this->store = $this->tempStoreFactory->get('multistep_data'); } /** * {@inheritdoc} */ public static function create(ContainerInterface $container) { return new static( $container->get('user.private_tempstore'), $container->get('session_manager'), $container->get('current_user') ); } /** * {@inheritdoc}. */ public function buildForm(array $form, FormStateInterface $form_state) { // Start a manual session for anonymous users. if ($this->currentUser->isAnonymous() && !isset($_SESSION['multistep_form_holds_session'])) { $_SESSION['multistep_form_holds_session'] = true; $this->sessionManager->start(); } $form = array(); $form['actions']['#type'] = 'actions'; $form['actions']['submit'] = array( '#type' => 'submit', '#value' => $this->t('Submit'), '#button_type' => 'primary', '#weight' => 10, ); return $form; } /** * Saves the data from the multistep form. */ protected function saveData() { // Logic for saving data goes here... $this->deleteStore(); drupal_set_message($this->t('The form has been saved.')); } /** * Helper method that removes all the keys from the store collection used for * the multistep form. */ protected function deleteStore() { $keys = ['name', 'email', 'age', 'location']; foreach ($keys as $key) { $this->store->delete($key); } } }
-/** * @file * Contains \Drupal\demo\Form\Multistep\MultistepFormBase. */ namespace Drupal\demo\Form\Multistep; use Drupal\Core\Form\FormBase; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Session\AccountInterface; use Drupal\Core\Session\SessionManagerInterface; use Drupal\user\PrivateTempStoreFactory; use Symfony\Component\DependencyInjection\ContainerInterface; abstract class MultistepFormBase extends FormBase { /** * @var \Drupal\user\PrivateTempStoreFactory */ protected $tempStoreFactory; /** * @var \Drupal\Core\Session\SessionManagerInterface */ private $sessionManager; /** * @var \Drupal\Core\Session\AccountInterface */ private $currentUser; /** * @var \Drupal\user\PrivateTempStore */ protected $store; /** * Constructs a \Drupal\demo\Form\Multistep\MultistepFormBase. * * @param \Drupal\user\PrivateTempStoreFactory $temp_store_factory * @param \Drupal\Core\Session\SessionManagerInterface $session_manager * @param \Drupal\Core\Session\AccountInterface $current_user */ public function __construct(PrivateTempStoreFactory $temp_store_factory, SessionManagerInterface $session_manager, AccountInterface $current_user) { $this->tempStoreFactory = $temp_store_factory; $this->sessionManager = $session_manager; $this->currentUser = $current_user; $this->store = $this->tempStoreFactory->get('multistep_data'); } /** * {@inheritdoc} */ public static function create(ContainerInterface $container) { return new static( $container->get('user.private_tempstore'), $container->get('session_manager'), $container->get('current_user') ); } /** * {@inheritdoc}. */ public function buildForm(array $form, FormStateInterface $form_state) { // Start a manual session for anonymous users. if ($this->currentUser->isAnonymous() && !isset($_SESSION['multistep_form_holds_session'])) { $_SESSION['multistep_form_holds_session'] = true; $this->sessionManager->start(); } $form = array(); $form['actions']['#type'] = 'actions'; $form['actions']['submit'] = array( '#type' => 'submit', '#value' => $this->t('Submit'), '#button_type' => 'primary', '#weight' => 10, ); return $form; } /** * Saves the data from the multistep form. */ protected function saveData() { // Logic for saving data goes here... $this->deleteStore(); drupal_set_message($this->t('The form has been saved.')); } /** * Helper method that removes all the keys from the store collection used for * the multistep form. */ protected function deleteStore() { $keys = ['name', 'email', 'age', 'location']; foreach ($keys as $key) { $this->store->delete($key); } } }
Наш абстрактный класс формы расширяется от стандартного класса Drupal FormBase
так что мы можем использовать некоторые функции, предоставляемые им, и черты, которые он использует. Мы используем внедрение зависимостей для внедрения некоторых необходимых сервисов:
-
PrivateTempStoreFactory
предоставляет нам временное хранилище, которое является приватным для текущего пользователя (PrivateTempStore
). Мы будем хранить все представленные данные из шагов формы в этом магазине. В конструкторе мы также немедленноstore
атрибутstore
который содержит ссылку на коллекцию ключей / значений multistep_data, которую мы будем использовать для этого процесса. Методget()
на фабрике либо создает хранилище, если оно не существует, либо извлекает его из хранилища. -
SessionManager
позволяет нам начать сеанс для анонимных пользователей. -
CurrentUser
позволяет нам проверить, является ли текущий пользователь анонимным.
Внутри buildForm()
мы делаем две основные вещи. Сначала мы запускаем сеанс для анонимных пользователей, если он еще не существует. Это потому, что без сеанса мы не можем передавать временные данные по нескольким запросам. Для этого мы используем менеджер сессий. Во-вторых, мы создаем кнопку базового действия отправки, которая будет присутствовать во всех формах реализации.
Метод saveData()
будет вызываться из одной или нескольких реализующих форм и отвечает за сохранение данных из временного хранилища после завершения многоступенчатого процесса. Мы не будем вдаваться в подробности этой реализации, поскольку она полностью зависит от вашего варианта использования (например, вы можете создать объект конфигурации из каждой отправки). Мы, однако, занимаемся удалением всех предметов в хранилище после сохранения данных. Имейте в виду, что эти типы логических проверок не должны выполняться в базовом классе. Вы должны отложить на выделенный класс обслуживания, как обычно, или использовать аналогичный подход.
Теперь пришло время для реальных форм, которые будут представлять шаги в процессе. Мы начнем с первого класса внутри файла с именем MultistepOneForm.php
:
/** * @file * Contains \Drupal\demo\Form\Multistep\MultistepOneForm. */ namespace Drupal\demo\Form\Multistep; use Drupal\Core\Form\FormStateInterface; class MultistepOneForm extends MultistepFormBase { /** * {@inheritdoc}. */ public function getFormId() { return 'multistep_form_one'; } /** * {@inheritdoc}. */ public function buildForm(array $form, FormStateInterface $form_state) { $form = parent::buildForm($form, $form_state); $form['name'] = array( '#type' => 'textfield', '#title' => $this->t('Your name'), '#default_value' => $this->store->get('name') ? $this->store->get('name') : '', ); $form['email'] = array( '#type' => 'email', '#title' => $this->t('Your email address'), '#default_value' => $this->store->get('email') ? $this->store->get('email') : '', ); $form['actions']['submit']['#value'] = $this->t('Next'); return $form; } /** * {@inheritdoc} */ public function submitForm(array &$form, FormStateInterface $form_state) { $this->store->set('email', $form_state->getValue('email')); $this->store->set('name', $form_state->getValue('name')); $form_state->setRedirect('demo.multistep_two'); } }
Эта форма будет выглядеть примерно так:
В buildForm()
мы определяем два фиктивных элемента формы. Обратите внимание, что мы сначала получаем существующее определение формы из родительского класса. Значения по умолчанию для этих полей устанавливаются как значения, найденные в хранилище для этих ключей (чтобы пользователи могли видеть значения, которые они заполнили на этом шаге, если вернутся к нему). Наконец, мы меняем значение кнопки действия на Next
(чтобы указать, что эта форма не является окончательной).
В submitForm()
мы сохраняем отправленные значения в хранилище, а затем перенаправляем во вторую форму (которую можно найти по маршруту demo.multistep_two
). Имейте в виду, что мы не проводим здесь никакой проверки, чтобы сохранить свет в коде. Но большинство сценариев использования требуют проверки входных данных.
Поскольку мы затронули вопрос маршрутов, давайте обновим файл маршрутов в нашем demo
модуле и создадим два новых маршрута для наших форм:
demo.routing.yml :
demo.multistep_one: path: '/demo/multistep-one' defaults: _form: '\Drupal\demo\Form\Multistep\MultistepOneForm' _title: 'First form' requirements: _permission: 'access content' demo.multistep_two: path: '/demo/multistep-two' defaults: _form: '\Drupal\demo\Form\Multistep\MultistepTwoForm' _title: 'Second form' requirements: _permission: 'access content'
поdemo.multistep_one: path: '/demo/multistep-one' defaults: _form: '\Drupal\demo\Form\Multistep\MultistepOneForm' _title: 'First form' requirements: _permission: 'access content' demo.multistep_two: path: '/demo/multistep-two' defaults: _form: '\Drupal\demo\Form\Multistep\MultistepTwoForm' _title: 'Second form' requirements: _permission: 'access content'
поdemo.multistep_one: path: '/demo/multistep-one' defaults: _form: '\Drupal\demo\Form\Multistep\MultistepOneForm' _title: 'First form' requirements: _permission: 'access content' demo.multistep_two: path: '/demo/multistep-two' defaults: _form: '\Drupal\demo\Form\Multistep\MultistepTwoForm' _title: 'Second form' requirements: _permission: 'access content'
Для получения дополнительной информации о том, что происходит в этом файле, вы можете прочитать одну из предыдущих статей о Drupal 8, которые также объясняют маршруты.
Наконец, мы можем создать нашу вторую форму (внутри файла с именем MultistepTwoForm
):
/** * @file * Contains \Drupal\demo\Form\Multistep\MultistepTwoForm. */ namespace Drupal\demo\Form\Multistep; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Url; class MultistepTwoForm extends MultistepFormBase { /** * {@inheritdoc}. */ public function getFormId() { return 'multistep_form_two'; } /** * {@inheritdoc}. */ public function buildForm(array $form, FormStateInterface $form_state) { $form = parent::buildForm($form, $form_state); $form['age'] = array( '#type' => 'textfield', '#title' => $this->t('Your age'), '#default_value' => $this->store->get('age') ? $this->store->get('age') : '', ); $form['location'] = array( '#type' => 'textfield', '#title' => $this->t('Your location'), '#default_value' => $this->store->get('location') ? $this->store->get('location') : '', ); $form['actions']['previous'] = array( '#type' => 'link', '#title' => $this->t('Previous'), '#attributes' => array( 'class' => array('button'), ), '#weight' => 0, '#url' => Url::fromRoute('demo.multistep_one'), ); return $form; } /** * {@inheritdoc} */ public function submitForm(array &$form, FormStateInterface $form_state) { $this->store->set('age', $form_state->getValue('age')); $this->store->set('location', $form_state->getValue('location')); // Save the data parent::saveData(); $form_state->setRedirect('some_route'); } }
Этот будет выглядеть так, опять же очень просто:
Опять же, мы расширяемся от нашего базового класса, как мы сделали с первой формой. На этот раз, однако, у нас есть разные элементы формы, и мы добавляем новую ссылку действия рядом с кнопкой отправки. Это позволит пользователям вернуться к первому шагу процесса формы.
Внутри submitForm()
мы снова сохраняем значения в хранилище и передаем родительский класс, чтобы сохранить эти данные любым способом, который он сочтет нужным. Затем мы перенаправляем на любую страницу, которую хотим (маршрут, который мы здесь используем, является фиктивным).
И это в значительной степени это. Теперь у нас должна быть рабочая многошаговая форма, которая использует PrivateTempStore
для обеспечения доступности данных для нескольких запросов. Если нам нужно больше шагов, все, что нам нужно сделать, это создать еще несколько форм, добавить их между существующими и внести пару корректировок. Конечно, вы можете сделать это гораздо более гибким, не задавая жестко названия маршрутов в ссылках и перенаправлениях, но я оставляю эту часть на ваше усмотрение.
Вывод
В этой статье мы рассмотрели простой способ создания многошаговой формы в Drupal 8. Разумеется, вы можете основываться на этом подходе и создавать очень сложные и динамические процессы, которые включают в себя не только формы, но и другие виды шагов, которые используют перекрестное взаимодействие. -запрос данных. Таким образом, целью этой статьи было столько же о многоэтапных формах, сколько о демонстрации возможностей PrivateTempStore
. И если вы, как и я, думаете, что кеш объектов cTools очень мощен в Drupal 7, вы будете очень заинтересованы в его аналоге Drupal 8.