megawilddaddy/formflow-bundle

适用于您的 Symfony 项目的多步骤表单。

安装次数: 1,931

依赖项: 0

建议者: 0

安全: 0

星标: 0

关注者: 0

分支: 118

类型:symfony-bundle

v10.0.2 2021-03-01 10:47 UTC

README

Build Status Coverage Status

CraueFormFlowBundle 提供了一种在您的 Symfony 项目中构建和处理多步骤表单的功能。

功能

  • 导航(下一步、上一步、从头开始)
  • 步骤标签
  • 跳过步骤
  • 每个步骤不同的验证组
  • 处理文件上传
  • 动态步骤导航(可选)
  • 提交后重定向(也称为 "Post/Redirect/Get",可选)

一个展示这些功能的实时演示可以在 http://craue.de/symfony-playground/en/CraueFormFlow/ 查看。

安装

获取包

通过在 shell 中运行以下命令,让 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 来正确渲染按钮。在您的 base 模板中加载提供的文件

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

	// ...

}