Иногда при разработке веб-приложений отличных от примитивного блога или сайта-визитки можно столкнуться с необходимостью создания громоздких форм — информация о пользователе с несколькими адресами, личными даннми, кличками питомцев и т.д. Создание пошаговой формы способно упростить жизнь пользователям, но далеко не всегда разработчикам. В попытке помочь последним и был написан этот пост.
Мы рассмотрим бандл, уже упоминавшейся на просторах хабра e-commerce платформы Sylius — SyliusFlowBundle. Почему именно он? Рекомендация коллеги, поимевшего боли из-за негибкости CraueFormFlowBundle'a, документация проекта и няшное оформление sylius.org склонили чашу внутренних весов на сторону описываемого решения. В общем поехали.
Для начала нужно подтянуть непосредственно сам бандл:
composer require "sylius/flow-bundle"
Далее редактируем AppKernel.php:
<?php
// app/AppKernel.php
public function registerBundles()
{
$bundles = array(
new SyliusBundleFlowBundleSyliusFlowBundle(),
// Other bundles...
);
}
Для того чтобы наша форма завелась нужно произвести следующие действия:
- Создать нужные шаги — объекты, имлементирующие интерфейс StepInterface, с шаблоном для каждого из них.
- Связать шаги в сценарий — интерфейс ProcessScenarioInterface и объявить его как сервис
- Импортировать роутинг из бандла
Вот собственно и все что нужно, чтобы получить работающую пошаговую форму. Теперь рассмотрим каждый из шагов подробнее.
Создание шагов
Как было сказано выше, SyliusFlowBundle требует от нас реализации своего StepInterface, для этого удобно воспользоваться любезно нам предоставленной абстракцией ControllerStep, который, как видно из имени, в свою очередь наследует симфоновский контроллер. Решение предлагает нам разбить шаг на два действия — displayAction и forwardAction. Первое, очевидно, отображает форму, второе — обрабатывает POST-запрос и перенаправляет пользователя на следующий шаг. Скорей всего Ваши шаги будут иметь много общего кода, который можно вынести в собственный абстрактный класс. Например так:
abstract class BaseStep extends ControllerStep
{
const USER = 'user';
/** {@inheritdoc} */
public function displayAction(ProcessContextInterface $context)
{
return $this->createView(
$this->getStepForm($context->getStorage()->get(self::USER)),
$context
);
}
/** {@inheritdoc} */
public function forwardAction(ProcessContextInterface $context)
{
$form = $this->getStepForm($context->getStorage()->get(self::USER));
$form->handleRequest($context->getRequest());
if ($context->getRequest()->isMethod('POST') && $form->isValid()) {
return $this->onFormValid($form, $context);
}
return $this->createView($form, $context);
}
/**
* @param Form $form
* @param ProcessContextInterface $context
*
* @return Response
*/
abstract protected function createView(Form $form, ProcessContextInterface $context);
/**
* @param mixed $data
*
* @return Form
*/
abstract protected function getStepForm($data = null);
/**
* @param Form $form
* @param ProcessContextInterface $context
*
* @return mixed
*/
abstract protected function onFormValid(Form $form, ProcessContextInterface $context);
}
Здесь мы в displayAction достаем из хранилща (по умолчанию сессия) обьект, получаем форму в зависимости от конкретного шага и рендерим страницу (реализацию также нужно будет предоставить на конкретном шаге). Единственная задача forwardAction валидиция данных. И наконец взглянем на реализацию шага:
class MainStep extends BaseStep
{
/** {@inheritdoc} */
public function displayAction(ProcessContextInterface $context)
{
$context->getStorage()->remove(self::USER);
return parent::displayAction($context);
}
/** {@inheritdoc} */
protected function createView(Form $form, ProcessContextInterface $context)
{
return $this->render('HospectAppBundle:Process:step.html.twig', [
'form' => $form->createView(),
'context' => $context,
]);
}
/** {@inheritdoc} */
protected function getStepForm($data = null)
{
return $this->createForm(new MainInfoType(), $data);
}
/** {@inheritdoc} */
protected function onFormValid(Form $form, ProcessContextInterface $context)
{
$context->getStorage()->set(self::USER, $form->getData());
return $this->complete();
}
}
Здесь предсказуемо создается нужная форма, рендерится специфическое для шага представление и, в случае валидности данных, происходит сохранение в сессию. Последний шаг должен, естественно, сохранять многострадальный обьект в ваше любимое постояное хранилище данных.
Связывание шагов в сценарий
На данном этапе у нас есть несколько сферический шагов, отрезанных от приложения. Чтобы это изменить необходимо создать сценарий. Сценарий является связующим звеном общей формы и отвечает за такие вещи как: последовательность шагов, установку общих для всех шагов параметров роута, перенаправление после успешного окончания всех шагов и т.д. Однако на самом деле работы здесь не так много — интерфейс требует реализовать всего один метод — build, принимающий на вход всего один параметр — builder с привычными для него методами.
class UserScenario extends ContainerAware implements ProcessScenarioInterface
{
/** {@inheritdoc} */
public function build(ProcessBuilderInterface $builder)
{
$builder
->add('main', new MainStep())
->add('address', new AddressStep())
->setRedirect('hospect_app_homepage');
}
}
Чтобы система «узнала» о нашем сценарии его нужно обьявить как сервис с тегом 'sylius.process.scenario':
sylius.scenario.flow:
class: HospectAppBundleProcessScenarioUserScenario
calls:
- [ setContainer, [@service_container] ]
tags:
- { name: sylius.process.scenario, alias: user }
Конфигурация роутинга
Нам предлагается всего три маршрута для всей формы, сколько бы шагов в ней не было. Все они имеют обязательный параметр scenarioAlias, ориентируясь на который подтягивается нужный сценарий. Маршруты 'sylius_flow_display' и 'sylius_flow_forward' требуют наличие второго параметра под названием 'stepName'. Вот собственно и все. Импорт выглядит так:
sylius_flow:
resource: @SyliusFlowBundle/Resources/config/routing.yml
prefix: /
Ссылки
Автор: hospect