effiana/formflow-bundle

为您的Symfony项目提供多步表单。

安装: 14

依赖者: 1

建议者: 0

安全: 0

星标: 0

关注者: 1

分支: 118

类型:symfony-bundle

3.3.2 2020-05-07 08:50 UTC

README

Build Status Coverage Status

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,
	]);
}

说明

流程的工作方式

  1. 分发PreBindEvent
  2. 分发GetStepsEvent
  3. 更新表单数据类,包含所有步骤先前保存的数据。对于每个步骤,分发 PostBindSavedDataEvent
  4. 评估哪些步骤被跳过。确定当前步骤。
  5. 分发 PostBindFlowEvent
  6. 创建当前步骤的表单。
  7. 将请求绑定到该表单。
  8. 分发 PostBindRequestEvent
  9. 验证表单数据。
  10. 分发 PostValidateEvent
  11. 保存表单数据。
  12. 进行到下一个步骤。

方法 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) {
		// ...
	}

	// ...

}