Пошаговая форма на Symfony2 с SyliusFlowBundle

Иногда при разработке веб-приложений отличных от примитивного блога или сайта-визитки можно столкнуться с необходимостью создания громоздких форм — информация о пользователе с несколькими адресами, личными даннми, кличками питомцев и т.д. Создание пошаговой формы способно упростить жизнь пользователям, но далеко не всегда разработчикам. В попытке помочь последним и был написан этот пост.

Мы рассмотрим бандл, уже упоминавшейся на просторах хабра e-commerce платформы SyliusSyliusFlowBundle. Почему именно он? Рекомендация коллеги, поимевшего боли из-за негибкости CraueFormFlowBundle'a, документация проекта и няшное оформление sylius.org склонили чашу внутренних весов на сторону описываемого решения. В общем поехали.

Для начала нужно подтянуть непосредственно сам бандл:
composer require "sylius/flow-bundle"

Далее редактируем AppKernel.php:
<?php

// app/AppKernel.php

public function registerBundles()
{
    $bundles = array(
        new Sylius\Bundle\FlowBundle\SyliusFlowBundle(),

        // Other bundles...
    );
}

Для того чтобы наша форма завелась нужно произвести следующие действия:
  1. Создать нужные шаги — объекты, имлементирующие интерфейс StepInterface, с шаблоном для каждого из них.
  2. Связать шаги в сценарий — интерфейс ProcessScenarioInterface и объявить его как сервис
  3. Импортировать роутинг из бандла

Вот собственно и все что нужно, чтобы получить работающую пошаговую форму. Теперь рассмотрим каждый из шагов подробнее.

Создание шагов


Как было сказано выше, SyliusFlowBundle требует от нас реализации своего StepInterface, для этого удобно воспользоваться любезно нам предоставленной абстракцией ControllerStep, который, как видно из имени, в свою очередь наследует симфоновский контроллер. Решение предлагает нам разбить шаг на два действия — displayAction и forwardAction. Первое, очевидно, отображает форму, второе — обрабатывает POST-запрос и перенаправляет пользователя на следующий шаг. Скорей всего Ваши шаги будут иметь много общего кода, который можно вынести в собственный абстрактный класс. Например так:
BaseStep
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 валидиция данных. И наконец взглянем на реализацию шага:
MainStep
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 с привычными для него методами.
UserScenario
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: Hospect\AppBundle\Process\Scenario\UserScenario
        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: /


Ссылки


Similar posts

AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 5

    0
    А какие проблемы были с CraueFormFlowBundle?
      0
      Проблем с ним не было, т.к. не пробовал его использовать. Просмотрел доку, заглянул в исходники обоих бандлов, и сайлиусовский вариант мне показался более понятным и, опять же имхо, более гибким. Кроме того, в команде есть желание использовать сам Sylius на других проектах, так что возможность хоть одним глазом взглянуть на этого зверя подкупила.
      0
      поимевшего боли из-за негибкости CraueFormFlowBundle'a


      Какая именно боль?
        0
        Вроде как гибкости на хватало — деталей не знаю.
        0
        А при возврате на предыдущий шаг, введенные ранее данные, должны отобразиться, правильно? У меня что-то в приведенном примере, при возврате, чистая формы :(

        Only users with full accounts can post comments. Log in, please.