Symfony forms are a battle-tested feature to create even the most complex forms you can imagine. They provide dozens of built-in field types, advanced validation, events for dynamic behavior, support for embedding collections of fields and other forms, and much more.
In Symfony 7.4, we're improving forms with multi-step forms, so you can split large forms into smaller, connected ones that users can navigate through until the process is completed.
These new multi-step forms are called "form flows" and follow the same concepts
and ideas as regular forms. Consider an application that defines a UserSignUp
class (a Value Object or a Doctrine entity) to store new user data. You can
create a three-step form as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
use Symfony\Component\Form\Flow\AbstractFlowType;
class UserSignUpType extends AbstractFlowType
{
public function buildFormFlow(FormFlowBuilderInterface $builder, array $options): void
{
$builder->addStep('personal', UserSignUpPersonalType::class);
$builder->addStep('professional', UserSignUpProfessionalType::class);
$builder->addStep('account', UserSignUpAccountType::class);
$builder->add('navigator', NavigatorFlowType::class);
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => UserSignUp::class,
// `$currentStep` is a property defined in the UserSignUp class
'step_property_path' => 'currentStep',
]);
}
}
Instead of extending AbstractType, you now extend AbstractFlowType; instead
of using the buildForm() method, you use buildFormFlow(), and so on.
The core ideas remain the same.
Each form step is defined as an independent, regular Symfony form. Then you call
addStep() to include it in the flow, giving each step a unique name as the
first argument (more on this later).
You can then create and handle this multi-step form in a controller just like a regular form:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
class UserSignUpController extends AbstractController
{
#[Route('/signup')]
public function __invoke(Request $request): Response
{
$flow = $this->createForm(UserSignUpType::class, new UserSignUp())
->handleRequest($request);
if ($flow->isSubmitted() && $flow->isValid() && $flow->isFinished()) {
// do something with $form->getData()
return $this->redirectToRoute('app_signup_success');
}
return $this->render('signup/flow.html.twig', [
'form' => $flow->getStepForm(),
]);
}
}
There are only two main differences:
- You must add a
$flow->isFinished()check to know when the form has been marked as finished (for example, when the user clicks the final submit button); - When rendering the form, you must call
$flow->getStepForm()to create the form for the current step.
Form flows introduce a useful convention for validation: they set the current step name as the active validation group. Use this in your validation constraints:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
class UserSignUp
{
public function __construct(
#[Valid(groups: ['personal'])]
public Personal $personal = new Personal(),
#[Valid(groups: ['professional'])]
public Professional $professional = new Professional(),
#[Valid(groups: ['account'])]
public Account $account = new Account(),
public string $currentStep = 'personal',
) {
}
}
Navigation through form steps is managed using the following button types, which
extend ButtonFlowType:
ResetFlowType: sends the multi-step form back to the initial stateNextFlowType: moves the form to the next stepPreviousFlowType: moves the form to the previous stepFinishFlowType: resets the form and marks it as finished
These buttons provide options like skip, back_to, include_if, and
clear_submission to enable conditional steps, advanced navigation, and more.
With these tools, you can build sophisticated multi-step forms like the one below:
We're finalizing the official documentation for this new feature. Meanwhile, you can check out the following resources:
- FormFlow: Build Stunning Multistep Forms presentation at SymfonyOnline June 2025
- The slides of that presentation
- A demo application showcasing form flows
Thanks to Yonel for his help creating this blog post, and to Silas Joisten (who is working on integrating multi-step forms in Symfony UX) and Christian Raue, who has maintained the popular CraueFormFlowBundle for many years. The work of Silas and Christian served as inspiration for this new feature.
That's fantastic! Well done @Yonel Ceruto, I can't wait to try it out!
Amazing feature
Wow, awesome feature! Thanks ❤️
Thanks Yonel!
Terrible ! merci