-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
[Form] Add FormFlow
for multistep forms management
#60212
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: 7.3
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
early raw reviews :)
{ | ||
public function buildForm(FormBuilderInterface $builder, array $options): void | ||
{ | ||
$builder->add('back', FormFlowActionType::class, [ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what bout "previous" ? generally it goes with "next"
or rename other "forward" ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don’t have a strong opinion on that. I personally prefer the shorter version, and users can change it using the label
option anyway. Let’s see what others think.
use Symfony\Component\PropertyAccess\PropertyPathInterface; | ||
|
||
/** | ||
* A multistep form. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
a question about naming:
you describe this as a form multistep
but the class is named form flow
its feels weird at first read
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
They mean the same to me... I’m just phrasing it differently to make the idea clearer. What is the weird part?
...ony/Component/Form/Extension/HttpFoundation/Type/FormFlowTypeSessionDataStorageExtension.php
Outdated
Show resolved
Hide resolved
->default(null) | ||
->allowedTypes('null', 'callable'); | ||
|
||
$resolver->define('include_if') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nice
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've improved it a bit more for better DX, so now we have three allowed values:
null
if you want to include the button in all defined steps,- an
array
with the names of the steps where the button will be included, (most used) - a
callable
for more advanced inclusion logic.
src/Symfony/Component/Form/Extension/Core/Type/FormFlowNavigatorType.php
Show resolved
Hide resolved
Great idea! I've looked at the previous PR and could use it! One question came up: how can I jump back to a specific step without having to press the back button many times? |
Hey! take a look at the |
2f81d34
to
cdac9e2
Compare
Hey @yceruto, awesome to see this may become a native part of Symfony Forms! I just had a quick look at the implementation and didn't find anything about It would be very helpful to have a default way the FormFlow can handle file uploads, but also to have some interface to interact with the full lifecycle of uploaded files, e.g. to decide where and how to store them, how to reference them inside form data, etc. I think the lifecycle consists of the following steps:
Maybe file uploads are not as important for an initial implementation, but it's definitely a use case which should be thought about. Feel free to ping me if you have any questions. Maybe I could even build a minimal demo of the current implementation we use in our project and share it with you. |
this would be incompatible with apps using a load balancer with several servers, as there is no guarantee that the next request goes to the same server behind the load balancer. Such case require storing uploads in a shared storage (for instance using a S3 bucket or similar storage) |
You're very right, thanks for pointing this out. So it's especially important to have the possibility of handling those different cases. Maybe a |
I haven’t checked the file upload yet because I knew it would be a complicated topic, but I think using a custom action button (as explained above) will give you the flexibility to do custom things on your own. One option is to create your own Imagine you have a $builder->add('upload', FormFlowActionType::class, [
'include_if' => ['documents'], // the steps where this button will appear
'handler' => function (MyDto $data, ActionButtonInterface $button, FormFlowInterface $flow) {
// handle files uploading here ... store them somewhere ... create references ...
// $data->uploadedFiles = ... save references if we go back ...
$flow->moveNext();
},
]); So, it’s up to you where to store the files, how to reference them in the DTO, and how to render them again if the user goes back to this step, just by looking into |
Think of FormFlow action buttons like mini-controllers with a focused job. The main controller handles the shared logic across all steps, while each action handler takes care of custom operations. You can even use them for inter-step operations (like in the demo preview, where I showed how to add or remove skill items from a This part feels a bit like magic, but it’s a helpful one given how complex multistep forms can be. |
1ef730f
to
4df48d8
Compare
7996e61
to
d8cbb5b
Compare
689f674
to
734a122
Compare
FormFlow
component for multistep forms managementFormFlow
for multistep forms management
Alternative to #59548, inspired on @silasjoisten's work and @craue's bundle, thank you!
FormFlow
This PR introduces
FormFlow
, a component built on top of the existingForm
architecture. It handles the definition, creation, and handling of multistep forms, including data management, actions (explained below), and validations across steps.AbstractFlowType
Just like
AbstractType
defines a single form based on theFormType
,AbstractFlowType
can be used to define a multistep form based onFormFlowType
.The step name comes from the first param of
addStep()
, which matches the form name, like this:personal
form of typeUserSignUpPersonalType
will be the steppersonal
,professional
form of typeUserSignUpProfessionalType
will be the stepprofessional
,When the form is created, the
currentStep
value determines which step form to build, only the matching one, from the steps defined above, will be built.Type Extension
FormFlowType
is a regular form type in the Form system, so you can useAbstractTypeExtension
to extend one or more of them:Controller
Use the existent
createForm()
in your controller to create aFormFlow
instance.This follows the classic form creation and handling pattern, with 2 key differences:
$flow->isFinished()
to know if the finish action button was clicked,$flow->getStepForm()
call, which creates the a new step form, when necessary, based on the current state.Don’t be misled by the
$flow
variable name, it’s just aForm
descendant withFormFlow
capabilities.Important
The form data will be stored across steps, meaning the initial data set during the FormFlow creation won't match the one returned by
$form->getData()
at the end. Therefore, always use$form->getData()
when the flow finishes.FormFlowActionType
A FormFlow action is a regular submit button with an action name (optional) and handler (a callable). It mainly handles step transitions but can also run custom logic tied to your form data.
The action name identifies the operation to perform and doesn’t need to match the button name. There are 4 built-in actions:
reset
: sends the FormFlow back to the initial state (will depend on the initial data),next
: moves to the next step,back
: goes to a previous step,finish
: same asreset
but also marks the FormFlow as finished.You can combine these actions with other options for different purposes, for example:
skip
button using thenext
action andclear_submission
moves the FormFlow forward while clearing the current step,back_to
button using theback
action and a view value (step name) returns to a specific previous step,Built-in actions have a default handler in the FormFlow class, but you can define custom action/handler for specific needs. Custom handlers use the following signature:
Important
By default, the action runs when the form is submitted, passes validation, and just before the next step form is created during
$flow->getStepForm()
. To control it manually, call$flow->handleAction()
after$flow->handleRequest($request)
where needed.FormFlowActionType
also comes with other 2 options:clear_submission
: If true, it clears the submitted data. This is especially handy forskip
andback
actions, or anytime you want to empty the current step form submission.include_if
:null
if you want to include the button in all steps (default), an array of steps, or a callable that’s triggered during form creation to decide whether the action button should be included in the current step form. This callable will receive theFormFlowCursor
instance as argument.FormFlowCursor
This immutable value object holds all defined steps and the current one. You can access it via
$flow->getCursor()
or as aFormView
variable in Twig to build a nice step progress UI.FormFlowNavigatorType
The built-in
FormFlowNavigatorType
provides 3 default action buttons:back
,next
, andfinish
. You can customize or add more if needed. Here’s an example of adding a “skip” button to theprofessional
step we defined earlier:Then use
UserSignUpNavigatorType
instead.Data Storage
FormFlow handles state across steps, so the final data includes everything collected throughout the flow. By default, it uses
SessionDataStorage
(unless you’ve configured a custom one). For testing,InMemoryDataStorage
is also available.You can also create custom data storage by implementing
DataStorageInterface
and passing it through thedata_storage
option inFormFlowType
.Step Accessor
The
step_accessor
option lets you control how the current step is read from or written to your data. By default,PropertyPathStepAccessor
handles this using the form’s bound data andPropertyAccess
component. If the step name is managed externally (e.g., by a workflow), you can create a customStepAccessorInterface
adapter and pass it through this option inFormFlowType
.Validation
FormFlow relies on the standard validation system but introduces a useful convention: it sets the current step as an active validation group. This allows step-specific validation rules without extra setup:
Allowing you to configure the validation
groups
in your constraints, like this:There’s a lot more to share about this feature, so feel free to ask if anything isn’t clear.
Cheers!
Demo preview:
