CraueFormFlowBundle

repository·master·Indexed 20 days ago

https://github.com/craue/craueformflowbundle

A Symfony bundle for creating and managing multi-step forms. It provides tools for navigation (next, back, reset), step skipping, data persistence across steps via SessionStorage or DoctrineStorage, and support for both single-form-type or multi-form-type flow implementations.

Tokens
7.4K
Snippets
23
Records
30
Agent score
71%

What's inside CraueFormFlowBundle

  1. Configure step-based validation groups

    master

    By default, the bundle automatically generates step-based validation groups to validate the form data class bound to the flow.

    • Naming Convention: If your flow's getName() returns createVehicle, the first step's group will be flow_createVehicle_step1.
    • Customization: You can change the prefix by setting the validationGroupPrefix property on your flow class.
    • Overriding: Setting validation_groups in a form type's configureOptions is ignored because the flow overwrites it. To use custom groups, you must:
      • Override the flow's getFormOptions method.
      • Use the form_options key in the step configuration.
      • Use the setGenericFormOptions method.

    Note: If you set validation_groups to false, a closure, or a GroupSequence, the flow will not automatically append the step number to the group.

  2. Handle concurrent flow instances using `instance` parameter

    master

    Starting from version 3.0, the bundle supports concurrent instances of the same flow. Previously, a GET request without parameters would reuse the existing session-based flow. Now, a new flow instance is started by default.

    To link to a specific existing flow instance (for example, when navigating to a specific step), you must now include the instance parameter using the flow's instance ID.

    To continue an existing flow: Pass flow.getInstanceId() as the instance parameter in your URL.

    To start a fresh flow: Simply navigate to the flow's base URL without any additional parameters. You no longer need a dedicated 'reset' action to start a clean flow.

    {# Link to a specific step in the current flow instance #}
    <a href="{{ path('createTopic', {'instance': flow.getInstanceId(), 'step': 2}) }}">continue creating a topic</a>
  3. Implement a multi-step form flow

    master

    To create a form flow, you must implement a class that extends Craue\FormFlowBundle\Form\FormFlow. The core of your configuration happens in the loadStepsConfig() method, where you define an array of steps. Each step can have a label, a form_type, and an optional skip callback.

    Step Configuration Options

    • label: A string label for the step.
    • form_type: The class name of the Symfony Form Type to use for this step.
    • skip: A callback function function($estimatedCurrentStepNumber, FormFlowInterface $flow): bool that returns true if the step should be skipped.
    use Craue\FormFlowBundle\Form\FormFlow;
    use Craue\FormFlowBundle\Form\FormFlowInterface;
    
    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',
    			],
    		];
    	}
    }
  4. Use PostBindFlowEvent instead of PostBindSavedDataEvent

    master
    In versions 2.0 and later, the current step number is not yet determined by the time PostBindSavedDataEvent is dispatched. If your code needs to access the current step number during the binding process, you must use PostBindFlowEvent instead.
  5. Reset flow data using `reset()`

    master

    To clear saved step data from the session once a flow has been completed (e.g., after persisting data to a database), call $flow->reset() at the end of your controller action.

    public function createTopicAction() {
    	// ...
    	// flow finished
    	// persist data to the DB or whatever...
    
    	$flow->reset();
    
    	// redirect when done...
    }
  6. Pass generic and step-specific options to form types

    master

    You can pass options to the form types used in your flow at different levels of granularity.

    Generic Options (All Steps)

    Use setGenericFormOptions on the flow instance (usually in your controller) to apply options to every step's form type.

    $flow->setGenericFormOptions(['action' => 'targetUrl']);

    Step-Specific Options

    Define options for a specific step within the loadStepsConfig method using the form_options key.

    'form_options' => ['validation_groups' => ['Default']],

    Dynamic Step Options

    To pass options based on data submitted in previous steps, override the getFormOptions method in your flow class.

    public function getFormOptions($step, array $options = []) {
        $options = parent::getFormOptions($step, $options);
        $formData = $this->getFormData();
    
        if ($step === 2) {
            $options['numberOfWheels'] = $formData->getNumberOfWheels();
        }
    
        return $options;
    }
    // In your Controller
    $flow->setGenericFormOptions(['action' => 'targetUrl']);
    
    // In your Flow class
    protected function loadStepsConfig() {
        return [
            [
                'label' => 'wheels',
                'form_type' => CreateVehicleStep1Form::class,
                'form_options' => [
                    'validation_groups' => ['Default'],
                ],
            ],
        ];
    }
  7. Update Twig templates for Flow step labels

    master

    If you are overriding Flow templates, note that the block and variable names for step descriptions have changed.

    • Rename the block craue_flow_stepDescription to craue_flow_stepLabel.
    • Rename the variable stepDescription to stepLabel.
    {# Before #}
    {{ block('craue_flow_stepDescription') }}
    
    {% block craue_flow_stepDescription %}
        <span>{{ stepDescription | trans }}</span>
    {% endblock %}
    
    {# After #}
    {{ block('craue_flow_stepLabel') }}
    
    {% block craue_flow_stepLabel %}
        <span>{{ stepLabel | trans }}</span>
    {% endblock %}
  8. Configure Flow steps using setFormType and loadStepsConfig

    master

    The way steps are defined in a Flow class has changed. Instead of using a maxSteps property and loadStepDescriptions(), you now define a form type and a configuration array that maps labels to form types.

    1. Implement getName() to return the same value as your form type (this ensures validation groups continue to work).
    2. Use setFormType(FormTypeInterface $formType) to set the primary form type.
    3. Implement loadStepsConfig() to return an array of arrays, where each sub-array contains a label and a type (the form type for that step).
    use Symfony//Component/Form/FormTypeInterface;
    
    public function getName() {
        return 'createVehicle';
    }
    
    protected $formType;
    
    public function setFormType(FormTypeInterface $formType) {
        $this->formType = $formType;
    }
    
    protected function loadStepsConfig() {
        return array(
            array('label' => 'wheels', 'type' => $this->formType),
            array('label' => 'engine', 'type' => $this->formType),
            array('label' => 'confirmation', 'type' => $this->formType),
        );
    }
  9. Enable dynamic step navigation

    master

    To allow users to navigate directly to specific steps via links in the step list, set the $allowDynamicStepNavigation property to true in your flow class.

    To prevent the navigation parameters from being sent as query parameters during form submission, wrap your form_start in Twig using the craue_removeDynamicStepNavigationParameters filter:

    {{ form_start(form, {'action': path(app.request.attributes.get('_route'),
        app.request.query.all | craue_removeDynamicStepNavigationParameters(flow))}) }}
    class CreateVehicleFlow extends FormFlow {
        protected $allowDynamicStepNavigation = true;
    }
  10. Enable redirect after step submission

    master

    To perform a GET request redirect to the next step after a successful POST submission, follow these two steps:

    1. Enable the feature in your flow class by setting $allowRedirectAfterSubmit = true;.
    2. Implement the redirect logic in your controller using the redirectAfterSubmit method.
    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);
    }
    class CreateVehicleFlow extends FormFlow {
        protected $allowRedirectAfterSubmit = true;
    }
  11. Handle Form Flow in a Controller

    master

    To process a flow in a controller action:

    1. Instantiate your data object (must be an object, not an array).
    2. Retrieve the flow service and call $flow->bind($formData).
    3. Create the form using $flow->createForm().
    4. Validate using $flow->isValid($form).
    5. If valid, call $flow->saveCurrentStepData($form).
    6. Check $flow->nextStep():
      • If true, create the form for the next step.
      • If false, the flow is finished. Persist your data, call $flow->reset(), and redirect.
    7. Pass both form (the view) and flow to your template.
    public function createVehicleAction() {
    	$formData = new Vehicle();
    	$flow = $this->get('myCompany.form.flow.createVehicle');
    	$flow->bind($formData);
    
    	$form = $flow->createForm();
    	if ($flow->isValid($form)) {
    		$flow->saveCurrentStepData($form);
    
    		if ($flow->nextStep()) {
    			$form = $flow->createForm();
    		} else {
    			// flow finished
    			$em = $this->getDoctrine()->getManager();
    			$em->persist($formData);
    			$em->flush();
    
    			$flow->reset();
    			return $this->redirectToRoute('home');
    		}
    	}
    
    	return $this->render('@MyCompanyMy/Vehicle/createVehicle.html.twig', [
    		'form' => $form->createView(),
    		'flow' => $flow,
    	]);
    }
  12. Approach A: Use one form type for the entire flow

    master

    This approach is ideal for converting an existing single form into a flow. You use a single Form Type class and use the flow_step option (passed in the $options array of buildForm) to conditionally add fields based on the current step number.

    class CreateVehicleForm extends AbstractType {
    	public function buildForm(FormBuilderInterface $builder, array $options) {
    		switch ($options['flow_step']) {
    			case 1:
    				$builder->add('numberOfWheels', ChoiceType::class, [...]);
    				break;
    			case 2:
    				$builder->add('engine', VehicleEngineType::class, [...]);
    				break;
    			case 3: // No fields added
    				break;
    		}
    	}
    }