Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

[Form] Add MultiStepType #59548

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

Closed
wants to merge 16 commits into from
5 changes: 5 additions & 0 deletions 5 src/Symfony/Component/Form/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
CHANGELOG
=========

7.3
---

* Add `MultiStepType` to create multistep forms

7.2
---

Expand Down
174 changes: 174 additions & 0 deletions 174 src/Symfony/Component/Form/Extension/Core/Type/MultiStepType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Form\Extension\Core\Type;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;

/**
* @author Silas Joisten <silasjoisten@proton.me>
* @author Patrick Reimers <preimers@pm.me>
* @author Jules Pietri <jules@heahprod.com>
*/
final class MultiStepType extends AbstractType
{
public function configureOptions(OptionsResolver $resolver): void
{
$resolver
->setRequired('steps')
silasjoisten marked this conversation as resolved.
Show resolved Hide resolved
->setAllowedTypes('steps', 'array')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

array -> (string|callable)[] this is a fresh feature already merged in 7.3

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should all current_step / next_step etc allow callable ? That would ease integration with Stepper or Navigator or any future WizardStepPathFinder-like.. wdyt ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea but i dont know what i should do here. Maybe i dont fully understand the approach

->setAllowedValues('steps', static function (array $steps): bool {
foreach ($steps as $key => $step) {
if (!\is_string($key)) {
return false;
}

if ((!\is_string($step) || !is_subclass_of($step, AbstractType::class)) && !\is_callable($step)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A $step object that implements FormTypeInterface should be allowed as well

Also, I have the feeling that parenthesis should be added here (mixing || and &&).

return false;
}
}

return true;
})
->setRequired('current_step')
->setAllowedTypes('current_step', 'string')
->setNormalizer('current_step', static function (Options $options, string $value): string {
if (!\array_key_exists($value, $options['steps'])) {
throw new InvalidOptionsException(\sprintf('The current step "%s" does not exist.', $value));
}

return $value;
})
->setDefault('current_step', static function (Options $options): string {
/** @var string $firstStep */
$firstStep = array_key_first($options['steps']);

return $firstStep;
})
->setRequired('next_step')
->setAllowedTypes('next_step', ['string', 'null'])
->setDefault('next_step', function (Options $options): ?string {
return array_keys($options['steps'])[$this->currentStepIndex($options['current_step'], $options['steps']) + 1] ?? null;
})
->setNormalizer('next_step', static function (Options $options, ?string $value): ?string {
if (null === $value) {
return null;
}

if (!\array_key_exists($value, $options['steps'])) {
throw new InvalidOptionsException(\sprintf('The next step "%s" does not exist.', $value));
}

return $value;
})
->setRequired('previous_step')
->setAllowedTypes('previous_step', ['string', 'null'])
->setDefault('previous_step', function (Options $options): ?string {
return array_keys($options['steps'])[$this->currentStepIndex($options['current_step'], $options['steps']) - 1] ?? null;
})
->setNormalizer('previous_step', static function (Options $options, ?string $value): ?string {
if (null === $value) {
return null;
}

if (!\array_key_exists($value, $options['steps'])) {
throw new InvalidOptionsException(\sprintf('The previous step "%s" does not exist.', $value));
}

return $value;
});

$resolver->setDefaults([
'hide_back_button_on_first_step' => false,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

display_ with default true instead of hide_ default false?
Seems easier to read the positive way not the négative

'button_back_options' => [
'label' => 'Back',
],
'button_next_options' => [
'label' => 'Next',
],
'button_submit_options' => [
'label' => 'Finish',
],
]);

$resolver->setAllowedTypes('hide_back_button_on_first_step', 'bool');
$resolver->setAllowedTypes('button_back_options', 'array');
$resolver->setAllowedTypes('button_submit_options', 'array');
}

public function buildForm(FormBuilderInterface $builder, array $options): void
{
$currentStep = $options['steps'][$options['current_step']];
silasjoisten marked this conversation as resolved.
Show resolved Hide resolved

if (\is_callable($currentStep)) {
$currentStep($builder, $options);
} elseif (\is_string($currentStep)) {
$builder->add($options['current_step'], $currentStep);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the step type may require specific options that differ from those in the root form.

we need to consider this, as it's currently a limitation

}

$builder->add('back', SubmitType::class, [
Copy link
Contributor

@Spomky Spomky Mar 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not a fan of buttons within form type objects. I prefer their integration into the template.
There should be a way for the template to know when each button has to be displayed or not e.g. with the help of the isFirstStep or isLastStep methods.
Also, this will remove the need of the hide_back_button_on_first_step, button_back_options, button_next_options and button_submit_options options. WDYT?

'disabled' => $this->isFirstStep($options['current_step'], $options['steps']),
'validate' => false,
...$options['button_back_options'],
]);

if ($this->isFirstStep($options['current_step'], $options['steps']) && true === $options['hide_back_button_on_first_step']) {
$builder->remove('back');
}

$builder->add('submit', SubmitType::class, $this->isLastStep($options['current_step'], $options['steps']) ? $options['button_submit_options'] : $options['button_next_options']);
}

public function buildView(FormView $view, FormInterface $form, array $options): void
{
$view->vars['current_step'] = $options['current_step'];
$view->vars['steps'] = array_keys($options['steps']);
yceruto marked this conversation as resolved.
Show resolved Hide resolved
$view->vars['total_steps_count'] = \count($options['steps']);
$view->vars['current_step_number'] = $this->currentStepIndex($options['current_step'], $options['steps']) + 1;
$view->vars['is_first_step'] = $this->isFirstStep($options['current_step'], $options['steps']);
$view->vars['is_last_step'] = $this->isLastStep($options['current_step'], $options['steps']);
$view->vars['previous_step'] = $options['previous_step'];
$view->vars['next_step'] = $options['next_step'];
}

/**
* @param array<string, mixed> $steps
*/
private function currentStepIndex(string $currentStep, array $steps): int
{
/** @var int $currentStep */
$currentStep = array_search($currentStep, array_keys($steps), true);

return $currentStep;
}

/**
* @param array<string, mixed> $steps
*/
private function isLastStep(string $currentStep, array $steps): bool
{
return array_key_last(array_keys($steps)) === $this->currentStepIndex($currentStep, $steps);
}

/**
* @param array<string, mixed> $steps
*/
private function isFirstStep(string $currentStep, array $steps): bool
{
return 0 === $this->currentStepIndex($currentStep, $steps);
}
}
Loading
Loading
Morty Proxy This is a proxified and sanitized view of the page, visit original site.