В этом разделе мы приведем инструкции по реализации многошаговой формы с помощью ZF3. Многошаговая форма - это форма, имеющая много полей, которая отображается в несколько шагов. Для хранения текущего шага и введенных пользователем данных между запросами страниц, используются сессии PHP.
Возьмем в качестве примера регистрацию пользователя. Она может быть осуществлена в несколько шагов: на первом шаге вы отображаете страницу, где пользователь вводит логин и пароль, на втором - страницу, где он может ввести личную информацию, а на третьем - страницу для ввода платежной информации.
Другой пример многошаговой формы - форма опроса. Такая форма будет отображать вопрос и несколько вариантов ответа на него. Шагов у этой формы будет столько же, сколько вопросов будет содержать опрос.
В этом разделе мы реализует форму регистрации User Registration, позволяющую собрать информацию о регистрируемом пользователе.
Рабочий пример этой формы можно посмотреть в приложении Form Demo, которое идет вместе с этой книгой.
Если вы не знакомы с PHP-сессиями, пожалуйста, обратитесь к Работа с сессиями перед прочтением этого раздела.
Поддержка сессий реализована в компоненте Zend\Session
, так что вам нужно установить его, если вы еще этого не сделали.
Затем измените файл конфигурации APP_DIR/config/global.php следующим образом:
<?php
use Zend\Session\Storage\SessionArrayStorage;
use Zend\Session\Validator\RemoteAddr;
use Zend\Session\Validator\HttpUserAgent;
return [
// Настройка сессии.
'session_config' => [
// Срок действия cookie истечет через 1 час.
'cookie_lifetime' => 60*60*1,
// Данные сессии хранятся на сервере до 30 дней.
'gc_maxlifetime' => 60*60*24*30,
],
// Настройка менеджера сессии.
'session_manager' => [
// Валидаторы сессии (используются для безопасности).
'validators' => [
RemoteAddr::class,
HttpUserAgent::class,
]
],
// Настройка хранилища сессии.
'session_storage' => [
'type' => SessionArrayStorage::class
],
// ...
];
Затем добавьте следующий фрагмент кода в module.config.php для регистрации контейнера сессии UserRegistration:
<?php
return [
// ...
'session_containers' => [
'UserRegistration'
],
];
Готово! Теперь мы можем использовать контейнер сессии в нашем коде. Далее мы реализуем
модель формы RegistrationForm
.
Модель формы RegistrationForm
будет использоваться для сбора данных о пользователе (эл. адрес,
полное имя, персональная информация и платежная информация). Мы добавим элементы к этой форме в виде
трех частей, что позволит использовать ее как многошаговую форму.
Чтобы добавить модель формы, создайте файл RegistrationForm.php в каталоге Form под корневым каталогом модуля Application.
<?php
namespace Application\Form;
use Zend\Form\Form;
use Zend\InputFilter\InputFilter;
use Application\Validator\PhoneValidator;
/**
* Эта форма используется для сбора данных о регистрации пользователя. Она является многошаговой.
* Форма определяет, какие создавать поля, в зависимости от аргумента $step, который вы передаете
* ее конструктору.
*/
class RegistrationForm extends Form
{
/**
* Конструктор.
*/
public function __construct($step)
{
// Проверяем входные данные.
if (!is_int($step) || $step<1 || $step>3)
throw new \Exception('Step is invalid');
// Определяем имя формы
parent::__construct('registration-form');
// Задаем метод POST для этой формы
$this->setAttribute('method', 'post');
$this->addElements($step);
$this->addInputFilter($step);
}
/**
* Этот метод добавляет элементы к форме (поля ввода и кнопку отправки формы).
*/
protected function addElements($step)
{
if ($step==1) {
// Добавляем поле "email"
$this->add([
'type' => 'text',
'name' => 'email',
'attributes' => [
'id' => 'email'
],
'options' => [
'label' => 'Ваш E-mail',
],
]);
// Добавляем поле "full_name"
$this->add([
'type' => 'text',
'name' => 'full_name',
'attributes' => [
'id' => 'full_name'
],
'options' => [
'label' => 'Полное имя',
],
]);
// Добавляем поле "password"
$this->add([
'type' => 'password',
'name' => 'password',
'attributes' => [
'id' => 'password'
],
'options' => [
'label' => 'Пароль',
],
]);
// Добавляем поле "confirm_password"
$this->add([
'type' => 'password',
'name' => 'confirm_password',
'attributes' => [
'id' => 'confirm_password'
],
'options' => [
'label' => 'Подтвердите пароль',
],
]);
} else if ($step==2) {
// Добавляем поле "phone"
$this->add([
'type' => 'text',
'name' => 'phone',
'attributes' => [
'id' => 'phone'
],
'options' => [
'label' => 'Мобильный телефон',
],
]);
// Добавляем поле "street_address"
$this->add([
'type' => 'text',
'name' => 'street_address',
'attributes' => [
'id' => 'street_address'
],
'options' => [
'label' => 'Адрес',
],
]);
// Добавляем поле "city"
$this->add([
'type' => 'text',
'name' => 'city',
'attributes' => [
'id' => 'city'
],
'options' => [
'label' => 'Город',
],
]);
// Добавляем поле "state"
$this->add([
'type' => 'text',
'name' => 'state',
'attributes' => [
'id' => 'state'
],
'options' => [
'label' => 'Штат',
],
]);
// Добавляем поле "post_code"
$this->add([
'type' => 'text',
'name' => 'post_code',
'attributes' => [
'id' => 'post_code'
],
'options' => [
'label' => 'Почтовый индекс',
],
]);
// Добавляем поле "country"
$this->add([
'type' => 'select',
'name' => 'country',
'attributes' => [
'id' => 'country',
],
'options' => [
'label' => 'Страна',
'empty_option' => '-- Пожалуйста, выберите --',
'value_options' => [
'US' => 'United States',
'CA' => 'Canada',
'BR' => 'Brazil',
'GB' => 'Great Britain',
'FR' => 'France',
'IT' => 'Italy',
'DE' => 'Germany',
'RU' => 'Russia',
'IN' => 'India',
'CN' => 'China',
'AU' => 'Australia',
'JP' => 'Japan'
],
],
]);
} else if ($step==3) {
// Добавляем поле "billing_plan"
$this->add([
'type' => 'select',
'name' => 'billing_plan',
'attributes' => [
'id' => 'billing_plan',
],
'options' => [
'label' => 'Тарифный план',
'empty_option' => '-- Пожалуйста, выберите --',
'value_options' => [
'Free' => 'Free',
'Bronze' => 'Bronze',
'Silver' => 'Silver',
'Gold' => 'Gold',
'Platinum' => 'Platinum'
],
],
]);
// Добавляем поле "payment_method"
$this->add([
'type' => 'select',
'name' => 'payment_method',
'attributes' => [
'id' => 'payment_method',
],
'options' => [
'label' => 'Способ оплаты',
'empty_option' => '-- Пожалуйста, выберите --',
'value_options' => [
'Visa' => 'Visa',
'MasterCard' => 'Master Card',
'PayPal' => 'PayPal'
],
],
]);
}
// Добавляем поле CSRF
$this->add([
'type' => 'csrf',
'name' => 'csrf',
'attributes' => [],
'options' => [
'csrf_options' => [
'timeout' => 600
]
],
]);
// Добавляем кнопку отправки формы
$this->add([
'type' => 'submit',
'name' => 'submit',
'attributes' => [
'value' => 'Следующий шаг',
'id' => 'submitbutton',
],
]);
}
/**
* Этот метод создает фильтр входных данных (используется для фильтрации/валидации).
*/
private function addInputFilter($step)
{
$inputFilter = new InputFilter();
$this->setInputFilter($inputFilter);
if ($step==1) {
$inputFilter->add([
'name' => 'email',
'required' => true,
'filters' => [
['name' => 'StringTrim'],
],
'validators' => [
[
'name' => 'EmailAddress',
'options' => [
'allow' => \Zend\Validator\Hostname::ALLOW_DNS,
'useMxCheck' => false,
],
],
],
]);
$inputFilter->add([
'name' => 'full_name',
'required' => true,
'filters' => [
['name' => 'StringTrim'],
['name' => 'StripTags'],
['name' => 'StripNewlines'],
],
'validators' => [
[
'name' => 'StringLength',
'options' => [
'min' => 1,
'max' => 128
],
],
],
]);
// Добавляем вход для поля "password"
$inputFilter->add([
'name' => 'password',
'required' => true,
'filters' => [
],
'validators' => [
[
'name' => 'StringLength',
'options' => [
'min' => 6,
'max' => 64
],
],
],
]);
// Добавляем вход для поля "confirm_password"
$inputFilter->add([
'name' => 'confirm_password',
'required' => true,
'filters' => [
],
'validators' => [
[
'name' => 'Identical',
'options' => [
'token' => 'password',
],
],
],
]);
} else if ($step==2) {
$inputFilter->add([
'name' => 'phone',
'required' => true,
'filters' => [
],
'validators' => [
[
'name' => 'StringLength',
'options' => [
'min' => 3,
'max' => 32
],
],
[
'name' => PhoneValidator::class,
'options' => [
'format' => PhoneValidator::PHONE_FORMAT_INTL
]
],
],
]);
// Добавляем вход для поля "street_address"
$inputFilter->add([
'name' => 'street_address',
'required' => true,
'filters' => [
['name' => 'StringTrim'],
],
'validators' => [
['name'=>'StringLength', 'options'=>['min'=>1, 'max'=>255]]
],
]);
// Добавляем вход для поля "city"
$inputFilter->add([
'name' => 'city',
'required' => true,
'filters' => [
['name' => 'StringTrim'],
],
'validators' => [
['name'=>'StringLength', 'options'=>['min'=>1, 'max'=>255]]
],
]);
// Добавляем вход для поля "state"
$inputFilter->add([
'name' => 'state',
'required' => true,
'filters' => [
['name' => 'StringTrim'],
],
'validators' => [
['name'=>'StringLength', 'options'=>['min'=>1, 'max'=>32]]
],
]);
// Добавляем вход для поля "post_code"
$inputFilter->add([
'name' => 'post_code',
'required' => true,
'filters' => [
],
'validators' => [
['name' => 'IsInt'],
['name'=>'Between', 'options'=>['min'=>0, 'max'=>999999]]
],
]);
// Добавляем вход для поля "country"
$inputFilter->add([
'name' => 'country',
'required' => false,
'filters' => [
['name' => 'Alpha'],
['name' => 'StringTrim'],
['name' => 'StringToUpper'],
],
'validators' => [
['name'=>'StringLength', 'options'=>['min'=>2, 'max'=>2]]
],
]);
} else if ($step==3) {
// Добавляем вход для поля "billing_plan"
$inputFilter->add([
'name' => 'billing_plan',
'required' => true,
'filters' => [
],
'validators' => [
[
'name' => 'InArray',
'options' => [
'haystack'=>[
'Free',
'Bronze',
'Silver',
'Gold',
'Platinum'
]
]
]
],
]);
// Добавляем вход для поля "payment_method"
$inputFilter->add([
'name' => 'payment_method',
'required' => true,
'filters' => [
],
'validators' => [
[
'name' => 'InArray',
'options' => [
'haystack'=>[
'PayPal',
'Visa',
'MasterCard',
]
]
]
],
]);
}
}
}
Как видите из этого фрагмента кода, RegistrationForm
- это обычная модель формы, однако она принимает в своем
конструктора аргумент $step
, позволяющий указать, какие элементы формы использовать на текущем шаге.
Далее, мы добавим класс контроллера RegistrationController
. Для этого создадим файл RegistrationController.php
под каталогом Controller и добавим в него следующий код:
<?php
namespace Application\Controller;
use Zend\Mvc\Controller\AbstractActionController;
use Zend\View\Model\ViewModel;
use Application\Form\RegistrationForm;
use Zend\Session\Container;
/**
* Это класс контроллера, отображающий страницу с формой регистрации пользователя.
* Регистрация состоит из нескольких шагов, так что для каждого шага мы будем отображать
* разные элементы формы. Мы используем контейнер сессии для запоминания выбора пользователя
* на предыдущих шагах.
*/
class RegistrationController extends AbstractActionController
{
/**
* Контейнер сессии.
* @var Zend\Session\Container
*/
private $sessionContainer;
/**
* Конструктор. Его целью является внедрение зависимостей в контроллер.
*/
public function __construct($sessionContainer)
{
$this->sessionContainer = $sessionContainer;
}
/**
* Действие по умолчанию "index". Оно отображает
* страницу регистрации.
*/
public function indexAction()
{
// Определяем текущий шаг.
$step = 1;
if (isset($this->sessionContainer->step)) {
$step = $this->sessionContainer->step;
}
// Проверка номера шага на корректность (от 1 до 3).
if ($step<1 || $step>3)
$step = 1;
if ($step==1) {
// Инициализируем выборы пользователя.
$this->sessionContainer->userChoices = [];
}
$form = new RegistrationForm($step);
// Проверяем, отправил ли пользователь форму.
if($this->getRequest()->isPost()) {
// Заполняем форму POST-данными.
$data = $this->params()->fromPost();
$form->setData($data);
// Валидируем форму.
if($form->isValid()) {
// Получаем отфильтрованные и валидированные данные.
$data = $form->getData();
// Сохраняем выборы пользователя в сессии.
$this->sessionContainer->userChoices["step$step"] = $data;
// Увеличиваем шаг.
$step ++;
$this->sessionContainer->step = $step;
// Если мы прошли все 3 шага, перенаправляем на страницу проверки данных Review.
if ($step>3) {
return $this->redirect()->toRoute('registration',
['action'=>'review']);
}
// Переходим на следующий шаг.
return $this->redirect()->toRoute('registration');
}
}
$viewModel = new ViewModel([
'form' => $form
]);
$viewModel->setTemplate("application/registration/step$step");
return $viewModel;
}
/**
* Действие "review" отображает страницу, позволяющую проверить данные, введенные на
* предыдущих шагах.
*/
public function reviewAction()
{
// Валидируем данные сессии.
if(!isset($this->sessionContainer->step) ||
$this->sessionContainer->step<=3 ||
!isset($this->sessionContainer->userChoices)) {
throw new \Exception('Извините, данные пока не доступны для проверки');
}
// Извлекаем из сессии выборы пользователя.
$userChoices = $this->sessionContainer->userChoices;
return new ViewModel([
'userChoices' => $userChoices
]);
}
}
Описанный выше класс имеет три метода:
Конструктор __construct()
используется для внедрения зависимости - контейнера сессии - в контроллер.
Метод действия indexAction()
извлекает из сессии текущий шаг и инициализирует модель формы.
Если пользователь отправил форму, мы извлекаем из формы данные и сохраняем их в сессии, инкрементируя шаг.
Если шаг больше 3, мы перенаправляем пользователя на страницу "Review".
Метод действия reviewAction()
извлекает данные, введенные пользователем на всех трех шагах и передаете
их представлению для визуализации.
После этого добавим фабрику для RegistrationController
. Для этого добавьте файл RegistrationControllerFactory.php
в каталог Controller/Form под корневым каталогом модуля. Поместите в него следующий код:
<?php
namespace Application\Controller\Factory;
use Interop\Container\ContainerInterface;
use Zend\ServiceManager\Factory\FactoryInterface;
use Application\Controller\RegistrationController;
/**
* Это фабрика для RegistrationController. Ее целью является инстанцирование
* контроллера и внедрение в него зависимостей.
*/
class RegistrationControllerFactory implements FactoryInterface
{
public function __invoke(ContainerInterface $container,
$requestedName, array $options = null)
{
$sessionContainer = $container->get('UserRegistration');
// Инстанцируем контроллер и внедряем зависимости.
return new RegistrationController($sessionContainer);
}
}
Не забудьте зарегистрировать контроллер в файле module.config.php file!
Теперь давайте добавим шаблоны представлений для действий контроллера. У нас есть четыре шаблона представлений:
step1.phtml, step2.phtml, step3.phtml и review.phtml. Первые три используются indexAction()
, а последний
- reviewAction()
.
Добавьте файл step1.phtml в каталог application/registration и поместите в него следующий код:
<?php
$form->get('email')->setAttributes([
'class'=>'form-control',
'placeholder'=>'name@yourcompany.com'
]);
$form->get('full_name')->setAttributes([
'class'=>'form-control',
'placeholder'=>'John Doe'
]);
$form->get('password')->setAttributes([
'class'=>'form-control',
'placeholder'=>'Введите пароль (минимум 6 символов)'
]);
$form->get('confirm_password')->setAttributes([
'class'=>'form-control',
'placeholder'=>'Подтвердите пароль'
]);
$form->get('submit')->setAttributes(array('class'=>'btn btn-primary'));
$form->prepare();
?>
<h1>Регистрация пользователя - Шаг 1</h1>
<div class="row">
<div class="col-md-6">
<?= $this->form()->openTag($form); ?>
<div class="form-group">
<?= $this->formLabel($form->get('email')); ?>
<?= $this->formElement($form->get('email')); ?>
<?= $this->formElementErrors($form->get('email')); ?>
</div>
<div class="form-group">
<?= $this->formLabel($form->get('full_name')); ?>
<?= $this->formElement($form->get('full_name')); ?>
<?= $this->formElementErrors($form->get('full_name')); ?>
</div>
<div class="form-group">
<?= $this->formLabel($form->get('password')); ?>
<?= $this->formElement($form->get('password')); ?>
<?= $this->formElementErrors($form->get('password')); ?>
</div>
<div class="form-group">
<?= $this->formLabel($form->get('confirm_password')); ?>
<?= $this->formElement($form->get('confirm_password')); ?>
<?= $this->formElementErrors($form->get('confirm_password')); ?>
</div>
<div class="form-group">
<?= $this->formElement($form->get('submit')); ?>
</div>
<?= $this->formElement($form->get('csrf')); ?>
<?= $this->form()->closeTag(); ?>
</div>
</div>
Затем добавьте файл step2.phtml в каталог application/registration и поместите в него следующий код:
<?php
$form->get('phone')->setAttributes([
'class'=>'form-control',
'placeholder'=>'Номер телефона в международном формате'
]);
$form->get('street_address')->setAttributes([
'class'=>'form-control',
]);
$form->get('city')->setAttributes([
'class'=>'form-control',
]);
$form->get('state')->setAttributes([
'class'=>'form-control',
]);
$form->get('post_code')->setAttributes([
'class'=>'form-control',
]);
$form->get('country')->setAttributes([
'class'=>'form-control'
]);
$form->get('submit')->setAttributes(array('class'=>'btn btn-primary'));
$form->prepare();
?>
<h1>Регистрация пользователя - Шаг 2 - Персональная информация</h1>
<div class="row">
<div class="col-md-6">
<?= $this->form()->openTag($form); ?>
<div class="form-group">
<?= $this->formLabel($form->get('phone')); ?>
<?= $this->formElement($form->get('phone')); ?>
<?= $this->formElementErrors($form->get('phone')); ?>
</div>
<div class="form-group">
<?= $this->formLabel($form->get('street_address')); ?>
<?= $this->formElement($form->get('street_address')); ?>
<?= $this->formElementErrors($form->get('street_address')); ?>
</div>
<div class="form-group">
<?= $this->formLabel($form->get('city')); ?>
<?= $this->formElement($form->get('city')); ?>
<?= $this->formElementErrors($form->get('city')); ?>
</div>
<div class="form-group">
<?= $this->formLabel($form->get('state')); ?>
<?= $this->formElement($form->get('state')); ?>
<?= $this->formElementErrors($form->get('state')); ?>
</div>
<div class="form-group">
<?= $this->formLabel($form->get('post_code')); ?>
<?= $this->formElement($form->get('post_code')); ?>
<?= $this->formElementErrors($form->get('post_code')); ?>
</div>
<div class="form-group">
<?= $this->formLabel($form->get('country')); ?>
<?= $this->formElement($form->get('country')); ?>
<?= $this->formElementErrors($form->get('country')); ?>
</div>
<div class="form-group">
<?= $this->formElement($form->get('submit')); ?>
</div>
<?= $this->formElement($form->get('csrf')); ?>
<?= $this->form()->closeTag(); ?>
</div>
</div>
После этого добавьте файл step3.phtml в каталог application/registration и поместите в него следующий код:
<?php
$form->get('billing_plan')->setAttributes([
'class'=>'form-control',
]);
$form->get('payment_method')->setAttributes([
'class'=>'form-control',
]);
$form->get('submit')->setAttributes(array('class'=>'btn btn-primary'));
$form->prepare();
?>
<h1>Регистрация пользователя - Шаг 3 - Платежная информация</h1>
<div class="row">
<div class="col-md-6">
<?= $this->form()->openTag($form); ?>
<div class="form-group">
<?= $this->formLabel($form->get('billing_plan')); ?>
<?= $this->formElement($form->get('billing_plan')); ?>
<?= $this->formElementErrors($form->get('billing_plan')); ?>
</div>
<div class="form-group">
<?= $this->formLabel($form->get('payment_method')); ?>
<?= $this->formElement($form->get('payment_method')); ?>
<?= $this->formElementErrors($form->get('payment_method')); ?>
</div>
<div class="form-group">
<?= $this->formElement($form->get('submit')); ?>
</div>
<?= $this->formElement($form->get('csrf')); ?>
<?= $this->form()->closeTag(); ?>
</div>
</div>
И наконец, добавьте файл review.phtml в каталог application/registration и поместите в него следующий код:
<h1>Регистрация пользователя - Проверка</h1>
<p>Спасибо! Теперь, пожалуйста, проверьте данные, которые вы ввели на предыдуших шагах.</p>
<pre>
<?php print_r($userChoices); ?>
</pre>
Добавьте следующий маршрут в файл конфигурации module.config.php:
'registration' => [
'type' => Segment::class,
'options' => [
'route' => '/registration[/:action]',
'constraints' => [
'action' => '[a-zA-Z][a-zA-Z0-9_-]*'
],
'defaults' => [
'controller' => Controller\RegistrationController::class,
'action' => 'index',
],
],
],
Отлично! Теперь все готово для того, чтобы увидеть результат!
Чтобы увидеть нашу многошаговую форму в действии введите URL "http://localhost/registration" в адресной строке своего браузера. Появится страница User Registration - Step 1 (см. рисунок 11.6):
Как только пользователь введет свой адрес электронной почты, полное имя и пароль, и нажмет кнопку Next, он перенаправляется на следующий шаг (см. рисунок 11.7):
И последний шаг показан на рисунке 11.8 ниже:
Нажатие Next приводит к отображению страницы проверки Review, позволяющей просмотреть введенные на предыдущих шагах данные:
Этот пример можно посмотреть в приложении Form Demo, которое идет вместе с этой книгой.