Symfony Advent Calendar 2025 の17日目の記事です🎄
タイトルの通り、Symfony7.4からAbstractFlowTypeを使って簡単にマルチステップフォームを作成できるようになったようなので、使ってみました🤶
その方法と感想をまとめます!
参考記事はこちら👇
https://symfony.com/blog/new-in-symfony-7-4-multi-step-forms
目次
動作環境
- PHP 8.2.25
- Symfony 7.4.2
実装してみる!
$ symfony new multi-step-form --version=7.4 webapp
Symfony7.4からAbstractFlowTypが使用可能となっています。
DTO
<?php
declare(strict_types=1);
namespace App\Form\Data;
use Symfony\Component\Validator\Constraints as Assert;
class MultiStepDto
{
// Step 1:
#[Assert\NotBlank(groups: ['step1'])]
public ?string $username = null;
#[Assert\NotBlank(groups: ['step1'])]
#[Assert\Length(min: 3, groups: ['step1'])]
#[Assert\Email(groups: ['step1'])]
public ?string $email = null;
// Step 2:
#[Assert\NotBlank(groups: ['step2'])]
public ?int $age = null;
#[Assert\NotBlank(groups: ['step2'])]
public ?string $pref = null;
// Step 3:
#[Assert\NotBlank(groups: ['step3'])]
#[Assert\Length(min: 3, groups: ['step3'])]
public ?string $freeText = null;
#[Assert\IsTrue(groups: ['step3'])]
public ?bool $cookieUse = false;
// Current step value:
public ?string $currentStep = null;
}
FormType
<?php
declare(strict_types=1);
namespace App\Form\Type;
use App\Form\Data\MultiStepDto;
use App\Form\Type\Step\Step1Type;
use App\Form\Type\Step\Step2Type;
use App\Form\Type\Step\Step3Type;
use Symfony\Component\Form\Flow\AbstractFlowType;
use Symfony\Component\Form\Flow\FormFlowBuilderInterface;
use Symfony\Component\Form\Flow\Type\NavigatorFlowType;
use Symfony\Component\Form\Flow\Type\NextFlowType;
use Symfony\Component\Form\Flow\Type\PreviousFlowType;
use Symfony\Component\OptionsResolver\OptionsResolver;
class MultiStepType extends AbstractFlowType
{
public function buildFormFlow(FormFlowBuilderInterface $builder, array $options): void
{
$builder->addStep('step1', Step1Type::class);
$builder->addStep('step2', Step2Type::class);
$builder->addStep('step3', Step3Type::class);
$builder->add('navigator', OriginalNavigatorFlowType::class);
// ボタン等がデフォルトのままで良い場合は以下でok
// $builder->add('navigator', NavigatorFlowType::class);
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => MultiStepDto::class,
'step_property_path' => 'currentStep',
'translation_domain' => false,
]);
}
}
(Step2Type, Step3Typeは省略してます)
<?php
declare(strict_types=1);
namespace App\Form\Type\Step;
use App\Form\Data\MultiStepDto;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class Step1Type extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('username')
->add('email')
;
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => MultiStepDto::class,
'inherit_data' => true,
'label' => 'step1',
]);
}
}
ボタンのテキストを変更したり、装飾する場合はこんな感じ。
<?php
declare(strict_types=1);
namespace App\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Flow\FormFlowCursor;
use Symfony\Component\Form\Flow\Type\FinishFlowType;
use Symfony\Component\Form\Flow\Type\NextFlowType;
use Symfony\Component\Form\Flow\Type\PreviousFlowType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class OriginalNavigatorFlowType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('back', PreviousFlowType::class, [
'label' => 'Back',
'attr' => ['class' => 'btn btn-secondary'],
])
->add('next', NextFlowType::class, [
'label' => 'Next',
'include_if' => fn (FormFlowCursor $cursor): bool => $cursor->getCurrentStep() !== 'documents' && $cursor->canMoveNext(),
'attr' => ['class' => 'btn btn-primary'],
])
->add('finish', FinishFlowType::class, [
'label' => 'Submit',
'attr' => ['class' => 'btn btn-success'],
]);
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'label' => false,
'mapped' => false,
'priority' => -100,
]);
}
}
Controller
<?php
declare(strict_types=1);
namespace App\Controller;
use App\Form\Data\MultiStepDto;
use App\Form\Type\MultiStepType;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\Flow\FormFlowInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
class MultiStepFormController extends AbstractController
{
#[Route('/form', name: 'app_form')]
public function __invoke(Request $request): Response
{
/** @var FormFlowInterface $flow */
$flow = $this->createForm(MultiStepType::class, new MultiStepDto())
->handleRequest($request);
if ($flow->isSubmitted() && $flow->isValid() && $flow->isFinished()) {
return $this->redirectToRoute('app_success');
}
return $this->render('form.html.twig', [
'form' => $flow->getStepForm(),
], new Response(null, $flow->isSubmitted() ? 422 : 200));
}
#[Route('/success', name: 'app_success')]
public function success(): Response
{
return new Response('Success!');
}
}
Template
{% extends 'base.html.twig' %}
{% block body %}
<div class="container">
<h2>Multi Step Form</h2>
<div class="row text-center align-items-center">
{% for step in form.vars.visible_steps %}
{% if not loop.first and not loop.last %}<div class="col">➖</div>{% endif %}
<div class="col">
{% if step.is_current_step %}
🔵
{% elseif step.index > form.vars.cursor.stepIndex %}
⚪️
{% else %}
☑️
{% endif %}
<div>
{{ step.name|title }}
</div>
</div>
{% if not loop.first and not loop.last %}<div class="col">➖</div>{% endif %}
{% endfor %}
</div>
<div>
{{ form(form) }} //基本、これだけでok
</div>
</div>
{% endblock %}
実際の動き
感想
詰まったところとしては、Turbo Driveを使ったフォーム送信のようで、
data-turbo="false"にするか、以下のようにしないと次のステップへ進めない事象がありました。
ただSymfony6.2以降では以下のコード変更は不要とのことだったので、原因追及中です🤔
return $this->render('form.html.twig', [
'form' => $flow->getStepForm(),
- ]);
+ ], new Response(null, $flow->isSubmitted() ? 422 : 200));
参考記事:
http://symfony.com/bundles/ux-turbo/current/index.html#3-form-response-code-changes
