effiana / formflow-bundle
为您的Symfony项目提供多步表单。
Requires
- php: ~7.0
- symfony/config: ~3.4|~4.2|~5.0
- symfony/dependency-injection: ~3.4|~4.2|~5.0
- symfony/event-dispatcher: ~3.4|~4.2|~5.0
- symfony/form: ~3.4|~4.2|~5.0
- symfony/http-foundation: ~3.4|~4.2|~5.0
- symfony/http-kernel: ~3.4|~4.2|~5.0
- symfony/options-resolver: ~3.4|~4.2|~5.0
- symfony/security-core: ~3.4|~4.2|~5.0
- symfony/translation: ~3.4|~4.2|~5.0
- symfony/validator: ~3.4|~4.2|~5.0
- symfony/yaml: ~3.4|~4.2|~5.0
Requires (Dev)
- doctrine/common: ~2.7
- doctrine/doctrine-bundle: ~1.10|~2.0
- phpunit/phpunit: ^6.5.13|^7.5.1
- symfony/phpunit-bridge: ~5.0
- symfony/symfony: ~3.4|~4.2|~5.0
- dev-master / 3.4.x-dev
- 3.3.2
- 3.3.1
- 3.3.0
- 3.2.1
- 3.2.0
- 3.1.1
- 3.1.0
- 3.0.4
- 3.0.3
- 3.0.2
- 3.0.1
- 3.0.0
- 2.1.x-dev
- 2.1.10
- 2.1.9
- 2.1.8
- 2.1.7
- 2.1.6
- 2.1.5
- 2.1.4
- 2.1.3
- 2.1.2
- 2.1.1
- 2.1.0
- 2.0.1
- 2.0.0
- 1.1.3
- 1.1.2
- 1.1.1
- 1.1.0
- 1.0.0
- dev-prepare-subtree-split-for-library-keep-namespaces
- dev-prepare-subtree-split-for-library-without-proxies
- dev-prepare-subtree-split-for-library
- dev-test-issue-89
This package is auto-updated.
Last update: 2024-09-24 19:10:30 UTC
README
CraueFormFlowBundle为您的Symfony项目提供构建和处理多步表单的功能。
特性
- 导航(下一步、上一步、从头开始)
- 步骤标签
- 跳过步骤
- 每个步骤不同的验证组
- 处理文件上传
- 动态步骤导航(可选)
- 提交后重定向(也称为"Post/Redirect/Get",可选)
以下是一个展示这些功能的实时演示:http://craue.de/symfony-playground/en/CraueFormFlow/。
安装
获取包
在终端运行以下命令,让Composer下载并安装包:
composer require craue/formflow-bundle
在终端。
启用包
如果您不使用Symfony Flex,请手动注册包
// in config/bundles.php return [ // ... Craue\FormFlowBundle\CraueFormFlowBundle::class => ['all' => true], ];
或者,对于Symfony 3.4
// in app/AppKernel.php public function registerBundles() { $bundles = [ // ... new Craue\FormFlowBundle\CraueFormFlowBundle(), ]; // ... }
用法
本节展示了如何创建一个创建车辆的3步表单流程。您需要在两种设置流程的方法之间进行选择。
方法A:整个流程使用一个表单类型
这种方法使得将现有的(常见)表单转换为表单流程变得容易。
创建流程类
// src/MyCompany/MyBundle/Form/CreateVehicleFlow.php use Craue\FormFlowBundle\Form\FormFlow; use Craue\FormFlowBundle\Form\FormFlowInterface; use MyCompany\MyBundle\Form\CreateVehicleForm; class CreateVehicleFlow extends FormFlow { protected function loadStepsConfig() { return [ [ 'label' => 'wheels', 'form_type' => CreateVehicleForm::class, ], [ 'label' => 'engine', 'form_type' => CreateVehicleForm::class, 'skip' => function($estimatedCurrentStepNumber, FormFlowInterface $flow) { return $estimatedCurrentStepNumber > 1 && !$flow->getFormData()->canHaveEngine(); }, ], [ 'label' => 'confirmation', ], ]; } }
创建表单类型类
您只需为流程创建一个表单类型类。有一个名为flow_step
的选项,您可以使用它来决定根据要渲染的步骤将哪些字段添加到表单中。
// src/MyCompany/MyBundle/Form/CreateVehicleForm.php use MyCompany\MyBundle\Form\Type\VehicleEngineType; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\FormBuilderInterface; class CreateVehicleForm extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options) { switch ($options['flow_step']) { case 1: $validValues = [2, 4]; $builder->add('numberOfWheels', ChoiceType::class, [ 'choices' => array_combine($validValues, $validValues), 'placeholder' => '', ]); break; case 2: // This form type is not defined in the example. $builder->add('engine', VehicleEngineType::class, [ 'placeholder' => '', ]); break; } } public function getBlockPrefix() { return 'createVehicle'; } }
方法B:每个步骤一个表单类型
这种方法使得重用表单类型来组合其他表单变得容易。
创建流程类
// src/MyCompany/MyBundle/Form/CreateVehicleFlow.php use Craue\FormFlowBundle\Form\FormFlow; use Craue\FormFlowBundle\Form\FormFlowInterface; use MyCompany\MyBundle\Form\CreateVehicleStep1Form; use MyCompany\MyBundle\Form\CreateVehicleStep2Form; class CreateVehicleFlow extends FormFlow { protected function loadStepsConfig() { return [ [ 'label' => 'wheels', 'form_type' => CreateVehicleStep1Form::class, ], [ 'label' => 'engine', 'form_type' => CreateVehicleStep2Form::class, 'skip' => function($estimatedCurrentStepNumber, FormFlowInterface $flow) { return $estimatedCurrentStepNumber > 1 && !$flow->getFormData()->canHaveEngine(); }, ], [ 'label' => 'confirmation', ], ]; } }
创建表单类型类
// src/MyCompany/MyBundle/Form/CreateVehicleStep1Form.php use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\FormBuilderInterface; class CreateVehicleStep1Form extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options) { $validValues = [2, 4]; $builder->add('numberOfWheels', ChoiceType::class, [ 'choices' => array_combine($validValues, $validValues), 'placeholder' => '', ]); } public function getBlockPrefix() { return 'createVehicleStep1'; } }
// src/MyCompany/MyBundle/Form/CreateVehicleStep2Form.php use MyCompany\MyBundle\Form\Type\VehicleEngineType; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; class CreateVehicleStep2Form extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options) { $builder->add('engine', VehicleEngineType::class, [ 'placeholder' => '', ]); } public function getBlockPrefix() { return 'createVehicleStep2'; } }
将您的流程注册为一个服务
XML
<services> <service id="myCompany.form.flow.createVehicle" class="MyCompany\MyBundle\Form\CreateVehicleFlow" autoconfigure="true"> </service> </services>
YAML
services: myCompany.form.flow.createVehicle: class: MyCompany\MyBundle\Form\CreateVehicleFlow autoconfigure: true
当不使用自动配置时,您可以让您的流程从父服务继承所需的依赖。
XML
<services> <service id="myCompany.form.flow.createVehicle" class="MyCompany\MyBundle\Form\CreateVehicleFlow" parent="craue.form.flow"> </service> </services>
YAML
services: myCompany.form.flow.createVehicle: class: MyCompany\MyBundle\Form\CreateVehicleFlow parent: craue.form.flow
创建表单模板
您只需为流程创建一个模板。您的流程类实例通过一个名为flow
的变量传递给模板,您可以使用它来根据当前步骤渲染表单。
{# in src/MyCompany/MyBundle/Resources/views/Vehicle/createVehicle.html.twig #} <div> Steps: {% include '@CraueFormFlow/FormFlow/stepList.html.twig' %} </div> {{ form_start(form) }} {{ form_errors(form) }} {% if flow.getCurrentStepNumber() == 1 %} <div> When selecting four wheels you have to choose the engine in the next step.<br /> {{ form_row(form.numberOfWheels) }} </div> {% endif %} {{ form_rest(form) }} {% include '@CraueFormFlow/FormFlow/buttons.html.twig' %} {{ form_end(form) }}
CSS
需要一些CSS来正确渲染按钮。在您的基模板中加载提供的文件
<link type="text/css" rel="stylesheet" href="{{ asset('bundles/craueformflow/css/buttons.css') }}" />
...并在您的项目中安装资源
# in a shell
php bin/console assets:install --symlink web
按钮
您可以使用这些变量来添加一个或多个CSS类来自定义默认按钮的外观
craue_formflow_button_class_last
将应用于下一步
或完成
按钮craue_formflow_button_class_finish
将专门应用于完成
按钮craue_formflow_button_class_next
将专门应用于下一步
按钮craue_formflow_button_class_back
将应用于上一步
按钮craue_formflow_button_class_reset
将应用于重置
按钮
使用Bootstrap按钮类的示例
{% include '@CraueFormFlow/FormFlow/buttons.html.twig' with { craue_formflow_button_class_last: 'btn btn-primary', craue_formflow_button_class_back: 'btn', craue_formflow_button_class_reset: 'btn btn-warning', } %}
以同样的方式,您可以自定义按钮标签
craue_formflow_button_label_last
适用于下一步
或完成
按钮craue_formflow_button_label_finish
适用于完成
按钮craue_formflow_button_label_next
适用于下一步
按钮craue_formflow_button_label_back
适用于上一步
按钮craue_formflow_button_label_reset
适用于重置
按钮
示例
{% include '@CraueFormFlow/FormFlow/buttons.html.twig' with { craue_formflow_button_label_finish: 'submit', craue_formflow_button_label_reset: 'reset the flow', } %}
您还可以通过将craue_formflow_button_render_reset
设置为false
来移除重置按钮。
创建操作
// in src/MyCompany/MyBundle/Controller/VehicleController.php public function createVehicleAction() { $formData = new Vehicle(); // Your form data class. Has to be an object, won't work properly with an array. $flow = $this->get('myCompany.form.flow.createVehicle'); // must match the flow's service id $flow->bind($formData); // form of the current step $form = $flow->createForm(); if ($flow->isValid($form)) { $flow->saveCurrentStepData($form); if ($flow->nextStep()) { // form for the next step $form = $flow->createForm(); } else { // flow finished $em = $this->getDoctrine()->getManager(); $em->persist($formData); $em->flush(); $flow->reset(); // remove step data from the session return $this->redirect($this->generateUrl('home')); // redirect when done } } return $this->render('@MyCompanyMy/Vehicle/createVehicle.html.twig', [ 'form' => $form->createView(), 'flow' => $flow, ]); }
说明
流程的工作方式
- 分发
PreBindEvent
。 - 分发
GetStepsEvent
。 - 更新表单数据类,包含所有步骤先前保存的数据。对于每个步骤,分发
PostBindSavedDataEvent
。 - 评估哪些步骤被跳过。确定当前步骤。
- 分发
PostBindFlowEvent
。 - 创建当前步骤的表单。
- 将请求绑定到该表单。
- 分发
PostBindRequestEvent
。 - 验证表单数据。
- 分发
PostValidateEvent
。 - 保存表单数据。
- 进行到下一个步骤。
方法 loadStepsConfig
该方法返回的数组用于创建流程的所有步骤。第一个项目将是第一个步骤。然而,您可以显式索引数组以提高可读性。
每个步骤的有效选项
label
(string
|StepLabel
|null
)- 如果您想渲染所有步骤的概述,您必须为每个步骤设置
label
选项。 - 如果在一个
StepLabel
实例上使用可调用函数,它必须返回一个字符串值或null
。 - 默认情况下,标签将在 Twig 中使用
messages
区域进行翻译。
- 如果您想渲染所有步骤的概述,您必须为每个步骤设置
form_type
(FormTypeInterface
|string
|null
)- 用于构建该步骤表单的表单类型。
- 此值传递给 Symfony 的表单工厂,因此创建常见表单时适用相同的规则。如果使用字符串,它必须是表单类型的 FQCN。
form_options
(array
)- 传递给该步骤表单类型的选项。
skip
(callable
|bool
)- 决定步骤是否被跳过。
- 如果使用可调用函数...
- 它将接收估计的当前步骤编号和流程作为参数;
- 它必须返回一个布尔值;
- 它可能会多次调用,直到确定实际的当前步骤编号。
示例
protected function loadStepsConfig() { return [ [ 'form_type' => CreateVehicleStep1Form::class, ], [ 'form_type' => CreateVehicleStep2Form::class, 'skip' => true, ], [ ], ]; }
protected function loadStepsConfig() { return [ 1 =>[ 'label' => 'wheels', 'form_type' => CreateVehicleStep1Form::class, ], 2 => [ 'label' => StepLabel::createCallableLabel(function() { return 'engine'; }) 'form_type' => CreateVehicleStep2Form::class, 'form_options' => [ 'validation_groups' => ['Default'], ], 'skip' => function($estimatedCurrentStepNumber, FormFlowInterface $flow) { return $estimatedCurrentStepNumber > 1 && !$flow->getFormData()->canHaveEngine(); }, ], 3 => [ 'label' => 'confirmation', ], ]; }
高级功能
验证分组
为了验证绑定到流程的表单数据类,将基于步骤的验证分组传递给表单类型。默认情况下,如果流程的 getName
方法返回 createVehicle
,则此类分组命名为 flow_createVehicle_step1
用于第一个步骤。您可以通过显式设置流程的属性 validationGroupPrefix
来自定义此名称。步骤编号(1、2、3 等)将由流程附加。
与独立表单相比,在您的表单类型的 configureOptions
方法中设置 validation_groups
选项在流程的上下文中没有任何效果。此值将被忽略,即被流程覆盖。但还有其他定义自定义验证分组的方法
- 重写流程的
getFormOptions
方法; - 使用
form_options
步骤选项;或 - 使用流程的
setGenericFormOptions
方法。
生成的基于步骤的验证分组将由流程添加,除非 validation_groups
选项设置为 false
、闭包或 GroupSequence。在这种情况下,它将 不会 由流程添加,因此请确保步骤表单按预期进行验证。
禁用先前步骤的重新验证
请参阅 #98 了解为什么默认情况下重新验证先前步骤很有用。但如果您想(或需要)避免重新验证先前步骤,请将以下内容添加到您的流程类中
// in src/MyCompany/MyBundle/Form/CreateVehicleFlow.php class CreateVehicleFlow extends FormFlow { protected $revalidatePreviousSteps = false; // ... }
将通用选项传递给表单类型
要设置所有步骤表单类型的通用选项,请使用方法 setGenericFormOptions
// in src/MyCompany/MyBundle/Controller/VehicleController.php public function createVehicleAction() { // ... $flow->setGenericFormOptions(['action' => 'targetUrl']); $flow->bind($formData); $form = $flow->createForm(); // ... }
将基于步骤的选项传递给表单类型
要为每个步骤的表单类型传递单独的选项,请使用步骤配置选项 form_options
// in src/MyCompany/MyBundle/Form/CreateVehicleFlow.php protected function loadStepsConfig() { return [ [ 'label' => 'wheels', 'form_type' => CreateVehicleStep1Form:class, 'form_options' => [ 'validation_groups' => ['Default'], ], ], ]; }
或者,要基于先前步骤设置选项(例如,根据提交的数据渲染字段)可以重写您的流程类中的 getFormOptions
方法
// in src/MyCompany/MyBundle/Form/CreateVehicleFlow.php public function getFormOptions($step, array $options = []) { $options = parent::getFormOptions($step, $options); $formData = $this->getFormData(); if ($step === 2) { $options['numberOfWheels'] = $formData->getNumberOfWheels(); } return $options; }
启用动态步骤导航
动态步骤导航意味着渲染的步骤列表将包含直接跳转到特定步骤(已完成的步骤)的链接。要启用它,请将以下内容添加到您的流程类中
// in src/MyCompany/MyBundle/Form/CreateVehicleFlow.php class CreateVehicleFlow extends FormFlow { protected $allowDynamicStepNavigation = true; // ... }
如果您想在提交表单时删除(通过使用此类直接链接添加的)参数,应修改模板中打开表单标签的动作,如下所示
{{ form_start(form, {'action': path(app.request.attributes.get('_route'), app.request.query.all | craue_removeDynamicStepNavigationParameters(flow))}) }}
文件上传处理
文件上传通过Base64编码内容并存储在会话中透明处理,可能会影响性能。为了方便起见,此功能默认启用,但可以在流程类中禁用,如下所示
// in src/MyCompany/MyBundle/Form/CreateVehicleFlow.php class CreateVehicleFlow extends FormFlow { protected $handleFileUploads = false; // ... }
默认情况下,系统目录将用于在加载步骤数据时从会话恢复的文件。您可以设置一个自定义的目录
// in src/MyCompany/MyBundle/Form/CreateVehicleFlow.php class CreateVehicleFlow extends FormFlow { protected $handleFileUploadsTempDir = '/path/for/flow/uploads'; // ... }
启用提交后重定向
此功能将允许在提交步骤后执行重定向,使用GET请求加载包含下一步的页面。要启用它,请将以下内容添加到您的流程类中
// in src/MyCompany/MyBundle/Form/CreateVehicleFlow.php class CreateVehicleFlow extends FormFlow { protected $allowRedirectAfterSubmit = true; // ... }
但是您仍然需要自己执行重定向,因此更新您的动作如下所示
// in src/MyCompany/MyBundle/Controller/VehicleController.php public function createVehicleAction() { // ... $flow->bind($formData); $form = $submittedForm = $flow->createForm(); if ($flow->isValid($submittedForm)) { $flow->saveCurrentStepData($submittedForm); // ... } if ($flow->redirectAfterSubmit($submittedForm)) { $request = $this->getRequest(); $params = $this->get('craue_formflow_util')->addRouteParameters(array_merge($request->query->all(), $request->attributes->get('_route_params')), $flow); return $this->redirect($this->generateUrl($request->attributes->get('_route'), $params)); } // ... // return ... }
使用事件
有一些事件您可以订阅。在您的流程类中直接使用所有这些事件可能看起来像这样
// in src/MyCompany/MyBundle/Form/CreateVehicleFlow.php use Craue\FormFlowBundle\Event\GetStepsEvent; use Craue\FormFlowBundle\Event\PostBindFlowEvent; use Craue\FormFlowBundle\Event\PostBindRequestEvent; use Craue\FormFlowBundle\Event\PostBindSavedDataEvent; use Craue\FormFlowBundle\Event\PostValidateEvent; use Craue\FormFlowBundle\Event\PreBindEvent; use Craue\FormFlowBundle\Form\FormFlowEvents; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; class CreateVehicleFlow extends FormFlow implements EventSubscriberInterface { /** * This method is only needed when _not_ using autoconfiguration. If it's there even with autoconfiguration enabled, * the `removeSubscriber` call ensures that subscribed events won't occur twice. * (You can remove the `removeSubscriber` call if you'll definitely never use autoconfiguration for that flow.) */ public function setEventDispatcher(EventDispatcherInterface $dispatcher) { parent::setEventDispatcher($dispatcher); $dispatcher->removeSubscriber($this); $dispatcher->addSubscriber($this); } public static function getSubscribedEvents() { return [ FormFlowEvents::PRE_BIND => 'onPreBind', FormFlowEvents::GET_STEPS => 'onGetSteps', FormFlowEvents::POST_BIND_SAVED_DATA => 'onPostBindSavedData', FormFlowEvents::POST_BIND_FLOW => 'onPostBindFlow', FormFlowEvents::POST_BIND_REQUEST => 'onPostBindRequest', FormFlowEvents::POST_VALIDATE => 'onPostValidate', ]; } public function onPreBind(PreBindEvent $event) { // ... } public function onGetSteps(GetStepsEvent $event) { // ... } public function onPostBindSavedData(PostBindSavedDataEvent $event) { // ... } public function onPostBindFlow(PostBindFlowEvent $event) { // ... } public function onPostBindRequest(PostBindRequestEvent $event) { // ... } public function onPostValidate(PostValidateEvent $event) { // ... } // ... }