Created
March 25, 2025 08:32
-
-
Save alexander-schranz/15b5a3124ac8171f5676561eeefb08d2 to your computer and use it in GitHub Desktop.
Multi Step Form Type and Wizard
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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 | |
); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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', | |
]); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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