craue/formflow-bundle

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

安装数: 3,280,745

依赖关系: 17

建议者: 7

安全性: 0

星标: 736

关注者: 30

分支: 118

开放问题: 73

类型:symfony-bundle

3.7.0 2024-01-11 01:03 UTC

README

Tests 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来正确渲染按钮。在您的基模板中加载提供的文件

<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->redirectToRoute('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;

	// ...

}

将通用选项传递给表单类型

要设置适用于所有步骤表单类型(s)的通用选项,可以使用方法 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->redirectToRoute($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) {
		// ...
	}

	// ...

}