Multi-step forms for your Symfony project.
Multi-step forms for your Symfony project.
CraueFormFlowBundle provides a facility for building and handling multi-step forms in your Symfony project.
Features:
A live demo showcasing these features is available at http://craue.de/symfony-playground/en/CraueFormFlow/.
Let Composer download and install the bundle by running
composer require craue/formflow-bundlein a shell.
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(),
];
// ...
}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.
This approach makes it easy to turn an existing (common) form into a form flow.
…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.
…This approach makes it easy to reuse the form types to compose other forms.
…// 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
YAML
services:
myCompany.form.flow.createVehicle:
class: MyCompany\MyBundle\Form\CreateVehicleFlow
autoconfigure: trueWhen not using autoconfiguration, you may let your flow inherit the required dependencies from a parent service.
XML
YAML
services:
myCompany.form.flow.createVehicle:
class: MyCompany\MyBundle\Form\CreateVehicleFlow
parent: craue.form.flowYou 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 #}
{{ form_start(form) }}
{{ form_errors(form) }}
{% if flow.getCurrentStepNumber() == 1 %}
{% endif %}
{{ form_rest(form) }}
{% include '@CraueFormFlow/FormFlow/buttons.html.twig' %}
{{ form_end(form) }}Some CSS is needed to render the buttons correctly. Load the provided file in your base template:
...and install the assets in your project:
# in a shell
php bin/console assets:install --symlink webYou can customize the default button look by using these variables to add one or more CSS classes to them:
craue_formflow_button_class_last will apply either to the next or finish buttoncraue_formflow_button_class_finish will specifically apply to the finish buttoncraue_formflow_button_class_next will specifically apply to the next buttoncraue_formflow_button_class_back will apply to the back buttoncraue_formflow_button_class_reset will apply to the reset buttonExample 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_last for either the next or finish buttoncraue_formflow_button_label_finish for the finish buttoncraue_formflow_button_label_next for the next button craue_formflow_button_label_back for the back buttoncraue_formflow_button_label_reset for the reset buttonExample:
{% 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.
…You can configure CraueFormFlowBundle to use the DoctrineStorage instead of the SessionStorage.
If a user then starts to fill out the form, the data will always be saved to the database instead of the session.
DoctrineStorage will use an extra table (craue_form_flow_storage) for this purpose.
You can use this example configuration as a starting point:
# config/packages/craue_form_flow.yaml
services:
Craue\FormFlowBundle\Storage\UserSessionStorageKeyGenerator:
arguments: [ '@security.token_storage', '@request_stack' ]
Craue\FormFlowBundle\Storage\DoctrineStorage:
arguments: [ '@doctrine.dbal.default_connection', '@Craue\FormFlowBundle\Storage\UserSessionStorageKeyGenerator' ]
myCompany.form.flow.storage.doctrine_storage:
class: 'Craue\FormFlowBundle\Storage\DataManager'
arguments: [ '@Craue\FormFlowBundle\Storage\DoctrineStorage' ]# config/services.yaml
services:
myCompany.form.flow.createVehicle:
autoconfigure: false
calls:
- [ setDataManager, [ '@myCompany.form.flow.storage.doctrine_storage'] ]
- [ setFormFactory, [ '@form.factory' ] ]
- [ setRequestStack, [ '@request_stack' ] ]
- [ setEventDispatcher, [ '@?event_dispatcher' ] ]PreBindEvent.GetStepsEvent.PostBindSavedDataEvent.PostBindFlowEvent.PostBindRequestEvent.PostValidateEvent.loadStepsConfigThe array returned by that method is used to create all steps of the flow. The first item will be the first step. You can, however, explicitly index the array for easier readability.
Valid options per step are:
label (string|StepLabel|null)label option for each step.StepLabel instance, it has to return a string value or null.messages domain when rendered in Twig.form_type (FormTypeInterface|string|null)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',
],
];
}To validate the form data class bound to the flow, a step-based validation group is passed to the form type.
By default, if the flow's getName method returns createVehicle, such a group is named flow_createVehicle_step1
for the first step. You can customize this name by setting the flow's property validationGroupPrefix explicitly.
The step number (1, 2, 3, etc.) will be appended by the flow.
Compared to standalone forms, setting the validation_groups option in your form type's configureOptions
method won't have any effect in the context of a flow. The value is just ignored, i.e. will be overwritten by the flow.
But there are other ways of defining custom validation groups:
getFormOptions method,form_options step option, orsetGenericFormOptions method.The generated step-based validation group will be added by the flow, unless the validation_groups option is set to false, a closure, or a GroupSequence.
In this case, it will not be added by the flow, so ensure the step forms are validated as expected.
Take a look at #98 for an example on why it's useful to revalidate previous steps by default. But if you want (or need) to avoid revalidating previous steps, add this to your flow class:
// in src/MyCompany/MyBundle/Form/CreateVehicleFlow.php
class CreateVehicleFlow extends FormFlow {
protected $revalidatePreviousSteps = false;
// ...
}To set options common for the form type(s) of all steps you can use method setGenericFormOptions:
// in src/MyCompany/MyBundle/Controller/VehicleController.php
public function createVehicleAction() {
// ...
$flow->setGenericFormOptions(['action' => 'targetUrl']);
$flow->bind($formData);
$form = $flow->createForm();
// ...
}To pass individual options to each step's form type you can use the step config option form_options:
// in src/MyCompany/MyBundle/Form/CreateVehicleFlow.php
protected function loadStepsConfig() {
return [
[
'label' => 'wheels',
'form_type' => CreateVehicleStep1Form:class,
'form_options' => [
'validation_groups' => ['Default'],
],
],
];
}Alternatively, to set options based on previous steps
No open issues yet, or sync has not completed.