CraueFormFlowBundle
Multi-step forms for your Symfony project.
Install / Use
/learn @craue/CraueFormFlowBundleREADME
Information
CraueFormFlowBundle provides a facility for building and handling multi-step forms in your Symfony project.
Features:
- navigation (next, back, start over)
- step labels
- skipping of steps
- different validation group for each step
- handling of file uploads
- dynamic step navigation (optional)
- redirect after submit (a.k.a. "Post/Redirect/Get", optional)
A live demo showcasing these features is available at http://craue.de/symfony-playground/en/CraueFormFlow/.
Installation
Get the bundle
Let Composer download and install the bundle by running
composer require craue/formflow-bundle
in a shell.
Enable the bundle
If you don't use Symfony Flex, register the bundle manually:
// in config/bundles.php
return [
// ...
Craue\FormFlowBundle\CraueFormFlowBundle::class => ['all' => true],
];
Or, for Symfony 3.4:
// in app/AppKernel.php
public function registerBundles() {
$bundles = [
// ...
new Craue\FormFlowBundle\CraueFormFlowBundle(),
];
// ...
}
Usage
This section shows how to create a 3-step form flow for creating a vehicle. You have to choose between two approaches on how to set up your flow.
Approach A: One form type for the entire flow
This approach makes it easy to turn an existing (common) form into a form flow.
Create a flow class
// 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',
],
];
}
}
Create a form type class
You only have to create one form type class for a flow.
There is an option called flow_step you can use to decide which fields will be added to the form
according to the step to render.
// 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';
}
}
Approach B: One form type per step
This approach makes it easy to reuse the form types to compose other forms.
Create a flow class
// 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',
],
];
}
}
Create form type classes
// 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';
}
}
Register your flow as a service
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
When not using autoconfiguration, you may let your flow inherit the required dependencies from a parent service.
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
Create a form template
You only need one template for a flow.
The instance of your flow class is passed to the template in a variable called flow so you can use it to render the
form according to the current step.
{# 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
Some CSS is needed to render the buttons correctly. Load the provided file in your base template:
<link type="text/css" rel="stylesheet" href="{{ asset('bundles/craueformflow/css/buttons.css') }}" />
...and install the assets in your project:
# in a shell
php bin/console assets:install --symlink web
Buttons
You can customize the default button look by using these variables to add one or more CSS classes to them:
craue_formflow_button_class_lastwill apply either to the next or finish buttoncraue_formflow_button_class_finishwill specifically apply to the finish buttoncraue_formflow_button_class_nextwill specifically apply to the next buttoncraue_formflow_button_class_backwill apply to the back buttoncraue_formflow_button_class_resetwill apply to the reset button
Example with Bootstrap button classes:
{% 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',
} %}
In the same manner you can customize the button labels:
craue_formflow_button_label_lastfor either the next or finish buttoncraue_formflow_button_label_finishfor the finish buttoncraue_formflow_button_label_nextfor the next buttoncraue_formflow_button_label_backfor the back buttoncraue_formflow_button_label_resetfor the reset button
Example:
{% include '@CraueFormFlow/FormFlow/buttons.html.twig' with {
craue_formflow_button_label_finish: 'submit',
craue_formflow_button_label_reset: 'reset the flow',
} %}
You can also remove the reset button by setting craue_formflow_button_render_reset to false.
Create an action
// 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,
]);
}
DoctrineStorage
You can configure CraueFormFlowBundle to use the DoctrineStorage instead of the SessionStorage. If a user then starts to fill out the fo
