Skip to content

Instantly share code, notes, and snippets.

@alexander-schranz
Created March 25, 2025 08:32
Show Gist options
  • Save alexander-schranz/15b5a3124ac8171f5676561eeefb08d2 to your computer and use it in GitHub Desktop.
Save alexander-schranz/15b5a3124ac8171f5676561eeefb08d2 to your computer and use it in GitHub Desktop.
Multi Step Form Type and Wizard
<?php
public function __invoke(Request $request): Response
{
$step = $request->query->getInt('step', 1);
$helper = new WizardHelper($request, clearExistingData: 1 === $step);
$data = $this->getData($helper->getAllData());
$form = $this->formFactory->create(ExampleFormType::class, $data, options: [
'activeStep' => $step,
]);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
/** @var array<string, mixed> $formData */
$formData = $form->getData();
$helper->saveStep($formData);
if ($helper->hasNext($form, $step)) {
return new RedirectResponse(
$this->urlGenerator->generate('my_route', ['step' => $step + 1]),
);
}
$allData = $helper->getAllData();
// TODO save
return new RedirectResponse(
$this->urlGenerator->generate('my_route', ['send' => 'true']),
);
}
$formView = $form->createView();
return new Response(
// TODO
);
}
}
<?php
class ExampleFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->add('details', StepFormType::class, [
'label' => 'app.details',
'activeStep' => $options['activeStep'],
'step' => ++$stepCounter,
'fields' => function (FormBuilderInterface $builder) {
// my fields
},
'block_prefix' => 'details',
]);
$builder->add('configuration', StepFormType::class, [
'label' => 'app.configuration',
'activeStep' => $options['activeStep'],
'step' => ++$stepCounter,
'fields' => function (FormBuilderInterface $builder) {
// my fields
},
'block_prefix' => 'details',
]);
}
}
<?php
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Webmozart\Assert\Assert;
class StepFormType extends AbstractType
{
final public const BLOCK_PREFIX = 'step';
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$fieldCallable = $options['fields'];
Assert::isCallable($fieldCallable);
$fieldCallable($builder);
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefault('isApi', false);
$resolver->setRequired('step');
$resolver->setAllowedTypes('step', ['int']);
$resolver->setRequired('activeStep');
$resolver->setAllowedTypes('activeStep', ['int']);
$resolver->setRequired('fields');
$resolver->setAllowedTypes('fields', ['callable']);
$resolver->setDefault('inherit_data', true);
$resolver->setNormalizer('disabled', fn (Options $options) => $options['step'] !== $options['activeStep']);
}
public function getBlockPrefix(): string
{
return self::BLOCK_PREFIX;
}
}
<?php
/**
* @experimental
*/
final class WizardHelper
{
private readonly string $key;
private readonly SessionInterface $session;
public function __construct(
Request $request,
bool $clearExistingData,
) {
$this->key = 'wizward_' . $request->attributes->get('_route', '');
$this->session = $request->getSession();
if ($clearExistingData) {
$this->clearAllData();
}
}
/**
* @return array<string, mixed>
*/
public function getAllData(): array
{
/** @var array<string, mixed> */
return $this->session->get($this->key, []);
}
public function clearAllData(): void
{
$this->session->remove($this->key);
}
/**
* @param array<string, mixed> $data
*/
public function saveStep(array $data): void
{
$data = \array_merge($this->getAllData(), $data);
$this->session->set($this->key, $data);
}
public function hasNext(FormInterface $form, int $step): bool
{
$allSteps = 0;
foreach ($form->all() as $key => $child) {
if ($child->getConfig()->getType()->getInnerType() instanceof StepType) {
++$allSteps;
}
}
return $allSteps > $step;
}
/**
* @return array<int, array{
* label: string,
* title: string,
* description: string,
* activeStep: bool,
* }>
*/
public function getProgressItems(FormInterface $form, int $step): array
{
$progressItems = [];
$i = 0;
foreach ($form->all() as $key => $child) {
if ($child->getConfig()->getType()->getInnerType() instanceof StepType) {
/** @var string $label */
$label = $child->getConfig()->getOption('label') ?? '';
/** @var string $contentTitle */
$contentTitle = $child->getConfig()->getOption('contentTitle') ?? '';
/** @var string $contentDescription */
$contentDescription = $child->getConfig()->getOption('contentDescription') ?? '';
$progressItems[] = [
'label' => $label,
'title' => $contentTitle,
'description' => $contentDescription,
'activeStep' => (++$i === $step),
];
}
}
return $progressItems;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment