From e83ec2a6ea8cfb69d486d92c49cbcc98fd4d848f Mon Sep 17 00:00:00 2001 From: Silas Joisten Date: Sat, 18 Jan 2025 13:15:06 +0200 Subject: [PATCH 01/15] Enhancement: Introduce `MultiStepType` --- .../Extension/Core/Type/MultiStepType.php | 59 +++++++ .../Extension/Core/Type/MultiStepTypeTest.php | 145 ++++++++++++++++++ 2 files changed, 204 insertions(+) create mode 100644 src/Symfony/Component/Form/Extension/Core/Type/MultiStepType.php create mode 100644 src/Symfony/Component/Form/Tests/Extension/Core/Type/MultiStepTypeTest.php diff --git a/src/Symfony/Component/Form/Extension/Core/Type/MultiStepType.php b/src/Symfony/Component/Form/Extension/Core/Type/MultiStepType.php new file mode 100644 index 0000000000000..0e2f29ac65489 --- /dev/null +++ b/src/Symfony/Component/Form/Extension/Core/Type/MultiStepType.php @@ -0,0 +1,59 @@ + + * + * 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\Options; +use Symfony\Component\OptionsResolver\OptionsResolver; + +/** + * @author Silas Joisten + * @author Patrick Reimers + * @author Jules Pietri + */ +final class MultiStepType extends AbstractType +{ + public function configureOptions(OptionsResolver $resolver): void + { + $resolver + ->setDefault('current_step_name', static function (Options $options): string { + return array_key_first($options['steps']); + }) + ->setRequired('steps'); + } + + public function buildForm(FormBuilderInterface $builder, array $options): void + { + $currentStep = $options['steps'][$options['current_step_name']]; + + if (\is_callable($currentStep)) { + $currentStep($builder, $options); + } elseif(\is_string($currentStep)) { + if (!class_exists($currentStep) || !is_a($currentStep, AbstractType::class)) { + throw new \InvalidArgumentException(sprintf('The form class "%s" does not exist.', $currentStep)); + } + + $builder->add($options['current_step_name'], $currentStep, $options); + } + } + + public function buildView(FormView $view, FormInterface $form, array $options): void + { + $view->vars['current_step_name'] = $options['current_step_name']; + $view->vars['steps_names'] = array_keys($options['steps']); + } +} diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/Type/MultiStepTypeTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/Type/MultiStepTypeTest.php new file mode 100644 index 0000000000000..920c615f117e6 --- /dev/null +++ b/src/Symfony/Component/Form/Tests/Extension/Core/Type/MultiStepTypeTest.php @@ -0,0 +1,145 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Tests\Extension\Core\Type; + +use Symfony\Component\Form\Extension\Core\Type\MultiStepType; +use Symfony\Component\Form\Extension\Core\Type\TextType; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\Form\Test\TypeTestCase; +use Symfony\Component\Form\Tests\Fixtures\AuthorType; +use Symfony\Component\OptionsResolver\Exception\MissingOptionsException; + +/** + * @author Silas Joisten + */ +final class MultiStepTypeTest extends TypeTestCase +{ + public function testConfigureOptionsWithoutStepsThrowsException(): void + { + self::expectException(MissingOptionsException::class); + + $this->factory->create(MultiStepType::class); + } + + public function testConfigureOptionsWithStepsSetsDefaultForCurrentStepName(): void + { + $form = $this->factory->create(MultiStepType::class, [], [ + 'steps' => [ + 'general' => static function (): void {}, + 'contact' => static function (): void {}, + 'newsletter' => static function (): void {}, + ], + ]); + + self::assertSame('general', $form->createView()->vars['current_step_name']); + } + + public function testBuildViewHasStepNames(): void + { + $form = $this->factory->create(MultiStepType::class, [], [ + 'steps' => [ + 'general' => static function (): void {}, + 'contact' => static function (): void {}, + 'newsletter' => static function (): void {}, + ], + ]); + + self::assertSame(['general', 'contact', 'newsletter'], $form->createView()->vars['steps_names']); + } + + public function testFormOnlyHasCurrentStepForm(): void + { + $form = $this->factory->create(MultiStepType::class, [], [ + 'steps' => [ + 'general' => static function (FormBuilderInterface $builder): void { + $builder + ->add('firstName', TextType::class) + ->add('lastName', TextType::class); + }, + 'contact' => static function (FormBuilderInterface $builder): void { + $builder + ->add('address', TextType::class) + ->add('city', TextType::class); + }, + 'newsletter' => static function (): void {}, + ], + ]); + + self::assertArrayHasKey('firstName', $form->createView()->children); + self::assertArrayHasKey('lastName', $form->createView()->children); + self::assertArrayNotHasKey('address', $form->createView()->children); + self::assertArrayNotHasKey('city', $form->createView()->children); + } + + public function testFormStepCanBeClassString(): void + { + $form = $this->factory->create(MultiStepType::class, ['current_step_name' => 'author'], [ + 'steps' => [ + 'general' => static function (FormBuilderInterface $builder): void { + $builder + ->add('firstName', TextType::class) + ->add('lastName', TextType::class); + }, + 'contact' => static function (FormBuilderInterface $builder): void { + $builder + ->add('address', TextType::class) + ->add('city', TextType::class); + }, + 'author' => AuthorType::class, + ], + ]); + + self::assertArrayHasKey('author', $form->createView()->children); + } + + public function testFormStepWithNormalStringWillThrowException(): void + { + self::expectException(\InvalidArgumentException::class); + + $this->factory->create(MultiStepType::class, ['current_step_name' => 'author'], [ + 'steps' => [ + 'general' => static function (FormBuilderInterface $builder): void { + $builder + ->add('firstName', TextType::class) + ->add('lastName', TextType::class); + }, + 'contact' => static function (FormBuilderInterface $builder): void { + $builder + ->add('address', TextType::class) + ->add('city', TextType::class); + }, + 'author' => 'hello there', + ], + ]); + } + + public function testFormStepWithClassStringNotExtendingAbstractTypeWillThrowException(): void + { + self::expectException(\InvalidArgumentException::class); + + $this->factory->create(MultiStepType::class, ['current_step_name' => 'author'], [ + 'steps' => [ + 'general' => static function (FormBuilderInterface $builder): void { + $builder + ->add('firstName', TextType::class) + ->add('lastName', TextType::class); + }, + 'contact' => static function (FormBuilderInterface $builder): void { + $builder + ->add('address', TextType::class) + ->add('city', TextType::class); + }, + 'author' => \stdClass::class, + ], + ]); + } +} From 3f7442906ec470854844a6e8bb6fd5892dbe8959 Mon Sep 17 00:00:00 2001 From: Silas Joisten Date: Sat, 18 Jan 2025 13:23:26 +0200 Subject: [PATCH 02/15] Update Changelog --- src/Symfony/Component/Form/CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Symfony/Component/Form/CHANGELOG.md b/src/Symfony/Component/Form/CHANGELOG.md index 3b1fabfd17afd..7f89fb767a9e5 100644 --- a/src/Symfony/Component/Form/CHANGELOG.md +++ b/src/Symfony/Component/Form/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +7.3 +--- + + * Add `MultiStepType` to create multistep forms + 7.2 --- From bc77606b32f33fcbb7b4b56698c05a2c405e8ff2 Mon Sep 17 00:00:00 2001 From: Silas Joisten Date: Sat, 18 Jan 2025 13:28:36 +0200 Subject: [PATCH 03/15] Fix --- .../Form/Extension/Core/Type/MultiStepType.php | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Component/Form/Extension/Core/Type/MultiStepType.php b/src/Symfony/Component/Form/Extension/Core/Type/MultiStepType.php index 0e2f29ac65489..2b84a3afb815e 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/MultiStepType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/MultiStepType.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + declare(strict_types=1); /* @@ -42,9 +51,9 @@ public function buildForm(FormBuilderInterface $builder, array $options): void if (\is_callable($currentStep)) { $currentStep($builder, $options); - } elseif(\is_string($currentStep)) { + } elseif (\is_string($currentStep)) { if (!class_exists($currentStep) || !is_a($currentStep, AbstractType::class)) { - throw new \InvalidArgumentException(sprintf('The form class "%s" does not exist.', $currentStep)); + throw new \InvalidArgumentException(\sprintf('The form class "%s" does not exist.', $currentStep)); } $builder->add($options['current_step_name'], $currentStep, $options); From 92c89e6b43aec2a456a42ca4edf5d23b97b3a466 Mon Sep 17 00:00:00 2001 From: Silas Joisten Date: Sat, 18 Jan 2025 13:32:00 +0200 Subject: [PATCH 04/15] Fix --- .../Tests/Extension/Core/Type/MultiStepTypeTest.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/Type/MultiStepTypeTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/Type/MultiStepTypeTest.php index 920c615f117e6..b8370b808a03b 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/Type/MultiStepTypeTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/Type/MultiStepTypeTest.php @@ -23,14 +23,14 @@ */ final class MultiStepTypeTest extends TypeTestCase { - public function testConfigureOptionsWithoutStepsThrowsException(): void + public function testConfigureOptionsWithoutStepsThrowsException() { self::expectException(MissingOptionsException::class); $this->factory->create(MultiStepType::class); } - public function testConfigureOptionsWithStepsSetsDefaultForCurrentStepName(): void + public function testConfigureOptionsWithStepsSetsDefaultForCurrentStepName() { $form = $this->factory->create(MultiStepType::class, [], [ 'steps' => [ @@ -56,7 +56,7 @@ public function testBuildViewHasStepNames(): void self::assertSame(['general', 'contact', 'newsletter'], $form->createView()->vars['steps_names']); } - public function testFormOnlyHasCurrentStepForm(): void + public function testFormOnlyHasCurrentStepForm() { $form = $this->factory->create(MultiStepType::class, [], [ 'steps' => [ @@ -80,7 +80,7 @@ public function testFormOnlyHasCurrentStepForm(): void self::assertArrayNotHasKey('city', $form->createView()->children); } - public function testFormStepCanBeClassString(): void + public function testFormStepCanBeClassString() { $form = $this->factory->create(MultiStepType::class, ['current_step_name' => 'author'], [ 'steps' => [ @@ -101,7 +101,7 @@ public function testFormStepCanBeClassString(): void self::assertArrayHasKey('author', $form->createView()->children); } - public function testFormStepWithNormalStringWillThrowException(): void + public function testFormStepWithNormalStringWillThrowException() { self::expectException(\InvalidArgumentException::class); @@ -122,7 +122,7 @@ public function testFormStepWithNormalStringWillThrowException(): void ]); } - public function testFormStepWithClassStringNotExtendingAbstractTypeWillThrowException(): void + public function testFormStepWithClassStringNotExtendingAbstractTypeWillThrowException() { self::expectException(\InvalidArgumentException::class); From 1befa387694166585e484153437f7bcdb1ea0394 Mon Sep 17 00:00:00 2001 From: Silas Joisten Date: Sat, 18 Jan 2025 13:33:49 +0200 Subject: [PATCH 05/15] Fix --- .../Tests/Extension/Core/Type/MultiStepTypeTest.php | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/Type/MultiStepTypeTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/Type/MultiStepTypeTest.php index b8370b808a03b..1b8a5e6a166f9 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/Type/MultiStepTypeTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/Type/MultiStepTypeTest.php @@ -1,14 +1,5 @@ - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - namespace Symfony\Component\Form\Tests\Extension\Core\Type; use Symfony\Component\Form\Extension\Core\Type\MultiStepType; @@ -43,7 +34,7 @@ public function testConfigureOptionsWithStepsSetsDefaultForCurrentStepName() self::assertSame('general', $form->createView()->vars['current_step_name']); } - public function testBuildViewHasStepNames(): void + public function testBuildViewHasStepNames() { $form = $this->factory->create(MultiStepType::class, [], [ 'steps' => [ From d6e32159fa55aec1971026054fa2ff3ecfeb5a9b Mon Sep 17 00:00:00 2001 From: Silas Joisten Date: Sat, 18 Jan 2025 13:35:38 +0200 Subject: [PATCH 06/15] Fix --- .../Component/Form/Extension/Core/Type/MultiStepType.php | 8 -------- .../Form/Tests/Extension/Core/Type/MultiStepTypeTest.php | 9 +++++++++ 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/Symfony/Component/Form/Extension/Core/Type/MultiStepType.php b/src/Symfony/Component/Form/Extension/Core/Type/MultiStepType.php index 2b84a3afb815e..0fcb6bdae6c5e 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/MultiStepType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/MultiStepType.php @@ -1,13 +1,5 @@ - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ declare(strict_types=1); diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/Type/MultiStepTypeTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/Type/MultiStepTypeTest.php index 1b8a5e6a166f9..0b1795005f009 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/Type/MultiStepTypeTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/Type/MultiStepTypeTest.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Symfony\Component\Form\Tests\Extension\Core\Type; use Symfony\Component\Form\Extension\Core\Type\MultiStepType; From 5771ee608ab618a5e89f91e9fc78f6a2b93c906e Mon Sep 17 00:00:00 2001 From: Silas Joisten Date: Sat, 18 Jan 2025 13:37:43 +0200 Subject: [PATCH 07/15] Fix --- .../Component/Form/Extension/Core/Type/MultiStepType.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Symfony/Component/Form/Extension/Core/Type/MultiStepType.php b/src/Symfony/Component/Form/Extension/Core/Type/MultiStepType.php index 0fcb6bdae6c5e..3f8e62c1706e4 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/MultiStepType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/MultiStepType.php @@ -1,8 +1,5 @@ Date: Sat, 18 Jan 2025 13:58:49 +0200 Subject: [PATCH 08/15] Foix --- .../Extension/Core/Type/MultiStepType.php | 26 ++++++++++----- .../Extension/Core/Type/MultiStepTypeTest.php | 33 +++++++++++++++---- 2 files changed, 44 insertions(+), 15 deletions(-) diff --git a/src/Symfony/Component/Form/Extension/Core/Type/MultiStepType.php b/src/Symfony/Component/Form/Extension/Core/Type/MultiStepType.php index 3f8e62c1706e4..3f40ebe565528 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/MultiStepType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/MultiStepType.php @@ -28,30 +28,38 @@ final class MultiStepType extends AbstractType public function configureOptions(OptionsResolver $resolver): void { $resolver - ->setDefault('current_step_name', static function (Options $options): string { - return array_key_first($options['steps']); - }) - ->setRequired('steps'); + ->setRequired('steps') + ->setDefault('current_step', static function (Options $options): string { + if (!is_string($first = \array_key_first($options['steps']))) { + throw new \InvalidArgumentException('The option "steps" must be an associative array.'); + } + + return $first; + }); } public function buildForm(FormBuilderInterface $builder, array $options): void { - $currentStep = $options['steps'][$options['current_step_name']]; + $currentStep = $options['steps'][$options['current_step']]; if (\is_callable($currentStep)) { $currentStep($builder, $options); } elseif (\is_string($currentStep)) { - if (!class_exists($currentStep) || !is_a($currentStep, AbstractType::class)) { + if (!class_exists($currentStep)) { throw new \InvalidArgumentException(\sprintf('The form class "%s" does not exist.', $currentStep)); } - $builder->add($options['current_step_name'], $currentStep, $options); + if (!is_subclass_of($currentStep, AbstractType::class)) { + throw new \InvalidArgumentException(\sprintf('"%s" is not a form type.', $currentStep)); + } + + $builder->add($options['current_step'], $currentStep); } } public function buildView(FormView $view, FormInterface $form, array $options): void { - $view->vars['current_step_name'] = $options['current_step_name']; - $view->vars['steps_names'] = array_keys($options['steps']); + $view->vars['current_step'] = $options['current_step']; + $view->vars['steps'] = array_keys($options['steps']); } } diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/Type/MultiStepTypeTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/Type/MultiStepTypeTest.php index 0b1795005f009..e1164b4b34a0d 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/Type/MultiStepTypeTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/Type/MultiStepTypeTest.php @@ -40,10 +40,10 @@ public function testConfigureOptionsWithStepsSetsDefaultForCurrentStepName() ], ]); - self::assertSame('general', $form->createView()->vars['current_step_name']); + self::assertSame('general', $form->createView()->vars['current_step']); } - public function testBuildViewHasStepNames() + public function testBuildViewHasSteps() { $form = $this->factory->create(MultiStepType::class, [], [ 'steps' => [ @@ -53,7 +53,7 @@ public function testBuildViewHasStepNames() ], ]); - self::assertSame(['general', 'contact', 'newsletter'], $form->createView()->vars['steps_names']); + self::assertSame(['general', 'contact', 'newsletter'], $form->createView()->vars['steps']); } public function testFormOnlyHasCurrentStepForm() @@ -82,7 +82,8 @@ public function testFormOnlyHasCurrentStepForm() public function testFormStepCanBeClassString() { - $form = $this->factory->create(MultiStepType::class, ['current_step_name' => 'author'], [ + $form = $this->factory->create(MultiStepType::class, [], [ + 'current_step' => 'author', 'steps' => [ 'general' => static function (FormBuilderInterface $builder): void { $builder @@ -104,8 +105,10 @@ public function testFormStepCanBeClassString() public function testFormStepWithNormalStringWillThrowException() { self::expectException(\InvalidArgumentException::class); + self::expectExceptionMessage('The form class "hello there" does not exist.'); - $this->factory->create(MultiStepType::class, ['current_step_name' => 'author'], [ + $this->factory->create(MultiStepType::class, [], [ + 'current_step' => 'author', 'steps' => [ 'general' => static function (FormBuilderInterface $builder): void { $builder @@ -125,8 +128,10 @@ public function testFormStepWithNormalStringWillThrowException() public function testFormStepWithClassStringNotExtendingAbstractTypeWillThrowException() { self::expectException(\InvalidArgumentException::class); + self::expectExceptionMessage('"stdClass" is not a form type.'); - $this->factory->create(MultiStepType::class, ['current_step_name' => 'author'], [ + $this->factory->create(MultiStepType::class, [], [ + 'current_step' => 'author', 'steps' => [ 'general' => static function (FormBuilderInterface $builder): void { $builder @@ -142,4 +147,20 @@ public function testFormStepWithClassStringNotExtendingAbstractTypeWillThrowExce ], ]); } + + public function testFormStepsWithInvalidConfiguration() + { + self::expectException(\InvalidArgumentException::class); + self::expectExceptionMessage('The option "steps" must be an associative array.'); + + $this->factory->create(MultiStepType::class, [], [ + 'steps' => [ + 1 => static function (FormBuilderInterface $builder): void { + $builder + ->add('firstName', TextType::class) + ->add('lastName', TextType::class); + }, + ], + ]); + } } From 9025a651a137bead9e4c70ecb4d616419eb14403 Mon Sep 17 00:00:00 2001 From: Silas Joisten Date: Sat, 18 Jan 2025 13:59:35 +0200 Subject: [PATCH 09/15] Fix --- .../Component/Form/Extension/Core/Type/MultiStepType.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Symfony/Component/Form/Extension/Core/Type/MultiStepType.php b/src/Symfony/Component/Form/Extension/Core/Type/MultiStepType.php index 3f40ebe565528..ce52b580883e3 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/MultiStepType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/MultiStepType.php @@ -30,7 +30,7 @@ public function configureOptions(OptionsResolver $resolver): void $resolver ->setRequired('steps') ->setDefault('current_step', static function (Options $options): string { - if (!is_string($first = \array_key_first($options['steps']))) { + if (!\is_string($first = array_key_first($options['steps']))) { throw new \InvalidArgumentException('The option "steps" must be an associative array.'); } From 865288b2c1141e3d0f7d96ba9ce385ef39bf8327 Mon Sep 17 00:00:00 2001 From: Silas Joisten Date: Sat, 18 Jan 2025 14:06:59 +0200 Subject: [PATCH 10/15] Fix Psalm? --- .../Component/Form/Extension/Core/Type/MultiStepType.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Component/Form/Extension/Core/Type/MultiStepType.php b/src/Symfony/Component/Form/Extension/Core/Type/MultiStepType.php index ce52b580883e3..597de8c92c487 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/MultiStepType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/MultiStepType.php @@ -30,11 +30,12 @@ public function configureOptions(OptionsResolver $resolver): void $resolver ->setRequired('steps') ->setDefault('current_step', static function (Options $options): string { - if (!\is_string($first = array_key_first($options['steps']))) { + $firstStep = array_key_first($options['steps']); + if (!\is_string($firstStep)) { throw new \InvalidArgumentException('The option "steps" must be an associative array.'); } - return $first; + return $firstStep; }); } From a71ee28544e7bd1245a2d76027f7534dd9439aa2 Mon Sep 17 00:00:00 2001 From: Silas Joisten Date: Sat, 18 Jan 2025 14:18:12 +0200 Subject: [PATCH 11/15] Fix --- .../Component/Form/Extension/Core/Type/MultiStepType.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Symfony/Component/Form/Extension/Core/Type/MultiStepType.php b/src/Symfony/Component/Form/Extension/Core/Type/MultiStepType.php index 597de8c92c487..53980d6a3e240 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/MultiStepType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/MultiStepType.php @@ -30,7 +30,9 @@ public function configureOptions(OptionsResolver $resolver): void $resolver ->setRequired('steps') ->setDefault('current_step', static function (Options $options): string { - $firstStep = array_key_first($options['steps']); + /** @var array $steps */ + $steps = $options['steps']; + $firstStep = array_key_first($steps); if (!\is_string($firstStep)) { throw new \InvalidArgumentException('The option "steps" must be an associative array.'); } From a23b651ff1c041b41ef6fe21dd924f995893636e Mon Sep 17 00:00:00 2001 From: Silas Joisten Date: Sun, 19 Jan 2025 10:27:27 +0200 Subject: [PATCH 12/15] Fix --- .../Extension/Core/Type/MultiStepType.php | 51 +++-- .../Extension/Core/Type/MultiStepTypeTest.php | 191 +++++++++++------- 2 files changed, 156 insertions(+), 86 deletions(-) diff --git a/src/Symfony/Component/Form/Extension/Core/Type/MultiStepType.php b/src/Symfony/Component/Form/Extension/Core/Type/MultiStepType.php index 53980d6a3e240..467e6e200ef93 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/MultiStepType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/MultiStepType.php @@ -15,6 +15,7 @@ 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; @@ -29,15 +30,31 @@ public function configureOptions(OptionsResolver $resolver): void { $resolver ->setRequired('steps') - ->setDefault('current_step', static function (Options $options): string { - /** @var array $steps */ - $steps = $options['steps']; - $firstStep = array_key_first($steps); - if (!\is_string($firstStep)) { - throw new \InvalidArgumentException('The option "steps" must be an associative array.'); + ->setAllowedTypes('steps', 'array') + ->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)) { + return false; + } } - return $firstStep; + 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 { + return \array_key_first($options['steps']); }); } @@ -48,14 +65,6 @@ public function buildForm(FormBuilderInterface $builder, array $options): void if (\is_callable($currentStep)) { $currentStep($builder, $options); } elseif (\is_string($currentStep)) { - if (!class_exists($currentStep)) { - throw new \InvalidArgumentException(\sprintf('The form class "%s" does not exist.', $currentStep)); - } - - if (!is_subclass_of($currentStep, AbstractType::class)) { - throw new \InvalidArgumentException(\sprintf('"%s" is not a form type.', $currentStep)); - } - $builder->add($options['current_step'], $currentStep); } } @@ -63,6 +72,16 @@ public function buildForm(FormBuilderInterface $builder, array $options): void public function buildView(FormView $view, FormInterface $form, array $options): void { $view->vars['current_step'] = $options['current_step']; - $view->vars['steps'] = array_keys($options['steps']); + $view->vars['steps'] = \array_keys($options['steps']); + $view->vars['total_steps_count'] = \count($options['steps']); + + /** @var int $currentStepIndex */ + $currentStepIndex = \array_search($options['current_step'], \array_keys($options['steps']), true); + $view->vars['current_step_number'] = $currentStepIndex + 1; + $view->vars['is_first_step'] = $currentStepIndex === 0; + + /** @var int $lastStepIndex */ + $lastStepIndex = \array_key_last(\array_keys($options['steps'])); + $view->vars['is_last_step'] = $lastStepIndex === $currentStepIndex; } } diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/Type/MultiStepTypeTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/Type/MultiStepTypeTest.php index e1164b4b34a0d..78b70b2d466f8 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/Type/MultiStepTypeTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/Type/MultiStepTypeTest.php @@ -16,6 +16,7 @@ use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\Test\TypeTestCase; use Symfony\Component\Form\Tests\Fixtures\AuthorType; +use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException; use Symfony\Component\OptionsResolver\Exception\MissingOptionsException; /** @@ -30,35 +31,98 @@ public function testConfigureOptionsWithoutStepsThrowsException() $this->factory->create(MultiStepType::class); } - public function testConfigureOptionsWithStepsSetsDefaultForCurrentStepName() + /** + * @dataProvider invalidStepValues + */ + public function testConfigureOptionsStepsMustBeArray(mixed $steps) { - $form = $this->factory->create(MultiStepType::class, [], [ - 'steps' => [ - 'general' => static function (): void {}, - 'contact' => static function (): void {}, - 'newsletter' => static function (): void {}, - ], - ]); + self::expectException(InvalidOptionsException::class); - self::assertSame('general', $form->createView()->vars['current_step']); + $this->factory->create(MultiStepType::class, [], ['steps' => $steps]); + } + + /** + * @return iterable>> + */ + public static function invalidStepValues(): iterable + { + yield 'Steps is string' => ['hello there']; + yield 'Steps is int' => [3]; + yield 'Steps is null' => [null]; + } + + /** + * @dataProvider invalidSteps + * + * @param array $steps + */ + public function testConfigureOptionsMustBeClassStringOrCallable(array $steps) + { + self::expectException(InvalidOptionsException::class); + self::expectExceptionMessage('The option "steps" with value array is invalid.'); + + $this->factory->create(MultiStepType::class, [], ['steps' => $steps]); + } + + /** + * @return iterable>> + */ + public static function invalidSteps(): iterable + { + yield 'Steps with invalid string value' => [['step1' => static function (): void {}, 'step2' => 'hello there']]; + yield 'Steps with invalid class value' => [['step1' => static function (): void {}, 'step2' => \stdClass::class]]; + yield 'Steps with array value' => [['step1' => static function (): void {}, 'step2' => []]]; + yield 'Steps with null value' => [['step1' => null]]; + yield 'Steps with int value' => [['step1' => 4]]; + yield 'Steps as non associative array' => [[0 => static function(): void {}]]; + } + + /** + * @dataProvider invalidStepNames + */ + public function testConfigureOptionsStepNameMustBeString(mixed $steps) + { + self::expectException(InvalidOptionsException::class); + + $this->factory->create(MultiStepType::class, [], ['steps' => ['step1' => static function (): void {}], 'current_step' => $steps]); + } + + /** + * @return iterable>> + */ + public static function invalidStepNames(): iterable + { + yield 'Step name is int' => [3]; + yield 'Step name is bool' => [false]; + yield 'Step name is callable' => [static function (): void {}]; } - public function testBuildViewHasSteps() + public function testConfigureOptionsStepNameMustExistInSteps() + { + self::expectException(InvalidOptionsException::class); + self::expectExceptionMessage('The current step "step2" does not exist.'); + + $this->factory->create(MultiStepType::class, [], ['steps' => ['step1' => static function (): void {}], 'current_step' => 'step2']); + } + + + public function testConfigureOptionsSetsDefaultValueForCurrentStepName() { $form = $this->factory->create(MultiStepType::class, [], [ 'steps' => [ - 'general' => static function (): void {}, - 'contact' => static function (): void {}, - 'newsletter' => static function (): void {}, + 'step1' => static function (): void {}, + 'step2' => static function (): void {}, + 'step3' => static function (): void {}, ], ]); - self::assertSame(['general', 'contact', 'newsletter'], $form->createView()->vars['steps']); + self::assertSame('step1', $form->createView()->vars['current_step']); } - public function testFormOnlyHasCurrentStepForm() + public function testBuildFormStepCanBeCallable() { $form = $this->factory->create(MultiStepType::class, [], [ + 'current_step' => 'contact', 'steps' => [ 'general' => static function (FormBuilderInterface $builder): void { $builder @@ -70,17 +134,14 @@ public function testFormOnlyHasCurrentStepForm() ->add('address', TextType::class) ->add('city', TextType::class); }, - 'newsletter' => static function (): void {}, ], ]); - self::assertArrayHasKey('firstName', $form->createView()->children); - self::assertArrayHasKey('lastName', $form->createView()->children); - self::assertArrayNotHasKey('address', $form->createView()->children); - self::assertArrayNotHasKey('city', $form->createView()->children); + self::assertArrayHasKey('address', $form->createView()->children); + self::assertArrayHasKey('city', $form->createView()->children); } - public function testFormStepCanBeClassString() + public function testBuildFormStepCanBeClassString() { $form = $this->factory->create(MultiStepType::class, [], [ 'current_step' => 'author', @@ -90,11 +151,6 @@ public function testFormStepCanBeClassString() ->add('firstName', TextType::class) ->add('lastName', TextType::class); }, - 'contact' => static function (FormBuilderInterface $builder): void { - $builder - ->add('address', TextType::class) - ->add('city', TextType::class); - }, 'author' => AuthorType::class, ], ]); @@ -102,65 +158,60 @@ public function testFormStepCanBeClassString() self::assertArrayHasKey('author', $form->createView()->children); } - public function testFormStepWithNormalStringWillThrowException() + public function testBuildView() { - self::expectException(\InvalidArgumentException::class); - self::expectExceptionMessage('The form class "hello there" does not exist.'); - - $this->factory->create(MultiStepType::class, [], [ - 'current_step' => 'author', + $form = $this->factory->create(MultiStepType::class, [], [ + 'current_step' => 'contact', 'steps' => [ - 'general' => static function (FormBuilderInterface $builder): void { - $builder - ->add('firstName', TextType::class) - ->add('lastName', TextType::class); - }, - 'contact' => static function (FormBuilderInterface $builder): void { - $builder - ->add('address', TextType::class) - ->add('city', TextType::class); - }, - 'author' => 'hello there', + 'contact' => static function (): void {}, + 'general' => static function (): void {}, + 'newsletter' => static function (): void {}, ], ]); + + self::assertSame('contact', $form->createView()->vars['current_step']); + self::assertSame(['contact', 'general', 'newsletter'], $form->createView()->vars['steps']); + self::assertSame(3, $form->createView()->vars['total_steps_count']); + self::assertSame(1, $form->createView()->vars['current_step_number']); + self::assertTrue($form->createView()->vars['is_first_step']); + self::assertFalse($form->createView()->vars['is_last_step']); } - public function testFormStepWithClassStringNotExtendingAbstractTypeWillThrowException() + public function testBuildViewIsLastStep() { - self::expectException(\InvalidArgumentException::class); - self::expectExceptionMessage('"stdClass" is not a form type.'); - - $this->factory->create(MultiStepType::class, [], [ - 'current_step' => 'author', + $form = $this->factory->create(MultiStepType::class, [], [ + 'current_step' => 'newsletter', 'steps' => [ - 'general' => static function (FormBuilderInterface $builder): void { - $builder - ->add('firstName', TextType::class) - ->add('lastName', TextType::class); - }, - 'contact' => static function (FormBuilderInterface $builder): void { - $builder - ->add('address', TextType::class) - ->add('city', TextType::class); - }, - 'author' => \stdClass::class, + 'contact' => static function (): void {}, + 'general' => static function (): void {}, + 'newsletter' => static function (): void {}, ], ]); + + self::assertSame('newsletter', $form->createView()->vars['current_step']); + self::assertSame(['contact', 'general', 'newsletter'], $form->createView()->vars['steps']); + self::assertSame(3, $form->createView()->vars['total_steps_count']); + self::assertSame(3, $form->createView()->vars['current_step_number']); + self::assertFalse($form->createView()->vars['is_first_step']); + self::assertTrue($form->createView()->vars['is_last_step']); } - public function testFormStepsWithInvalidConfiguration() + public function testBuildViewStepIsNotLastAndNotFirst() { - self::expectException(\InvalidArgumentException::class); - self::expectExceptionMessage('The option "steps" must be an associative array.'); - - $this->factory->create(MultiStepType::class, [], [ + $form = $this->factory->create(MultiStepType::class, [], [ + 'current_step' => 'general', 'steps' => [ - 1 => static function (FormBuilderInterface $builder): void { - $builder - ->add('firstName', TextType::class) - ->add('lastName', TextType::class); - }, + 'contact' => static function (): void {}, + 'general' => static function (): void {}, + 'newsletter' => static function (): void {}, ], ]); + + self::assertSame('general', $form->createView()->vars['current_step']); + self::assertSame(['contact', 'general', 'newsletter'], $form->createView()->vars['steps']); + self::assertSame(3, $form->createView()->vars['total_steps_count']); + self::assertSame(2, $form->createView()->vars['current_step_number']); + self::assertFalse($form->createView()->vars['is_first_step']); + self::assertFalse($form->createView()->vars['is_last_step']); } } From 5dba33162f430bab5d6dc7417e67d5a132c6e2e2 Mon Sep 17 00:00:00 2001 From: Silas Joisten Date: Sun, 19 Jan 2025 10:31:29 +0200 Subject: [PATCH 13/15] Cs fixes --- .../Form/Extension/Core/Type/MultiStepType.php | 12 ++++++------ .../Tests/Extension/Core/Type/MultiStepTypeTest.php | 3 +-- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/Symfony/Component/Form/Extension/Core/Type/MultiStepType.php b/src/Symfony/Component/Form/Extension/Core/Type/MultiStepType.php index 467e6e200ef93..f796841caed5d 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/MultiStepType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/MultiStepType.php @@ -37,7 +37,7 @@ public function configureOptions(OptionsResolver $resolver): void return false; } - if ((!\is_string($step) || !\is_subclass_of($step, AbstractType::class)) && !\is_callable($step)) { + if ((!\is_string($step) || !is_subclass_of($step, AbstractType::class)) && !\is_callable($step)) { return false; } } @@ -54,7 +54,7 @@ public function configureOptions(OptionsResolver $resolver): void return $value; }) ->setDefault('current_step', static function (Options $options): string { - return \array_key_first($options['steps']); + return array_key_first($options['steps']); }); } @@ -72,16 +72,16 @@ public function buildForm(FormBuilderInterface $builder, array $options): void public function buildView(FormView $view, FormInterface $form, array $options): void { $view->vars['current_step'] = $options['current_step']; - $view->vars['steps'] = \array_keys($options['steps']); + $view->vars['steps'] = array_keys($options['steps']); $view->vars['total_steps_count'] = \count($options['steps']); /** @var int $currentStepIndex */ - $currentStepIndex = \array_search($options['current_step'], \array_keys($options['steps']), true); + $currentStepIndex = array_search($options['current_step'], array_keys($options['steps']), true); $view->vars['current_step_number'] = $currentStepIndex + 1; - $view->vars['is_first_step'] = $currentStepIndex === 0; + $view->vars['is_first_step'] = 0 === $currentStepIndex; /** @var int $lastStepIndex */ - $lastStepIndex = \array_key_last(\array_keys($options['steps'])); + $lastStepIndex = array_key_last(array_keys($options['steps'])); $view->vars['is_last_step'] = $lastStepIndex === $currentStepIndex; } } diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/Type/MultiStepTypeTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/Type/MultiStepTypeTest.php index 78b70b2d466f8..bd46d6b06069a 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/Type/MultiStepTypeTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/Type/MultiStepTypeTest.php @@ -74,7 +74,7 @@ public static function invalidSteps(): iterable yield 'Steps with array value' => [['step1' => static function (): void {}, 'step2' => []]]; yield 'Steps with null value' => [['step1' => null]]; yield 'Steps with int value' => [['step1' => 4]]; - yield 'Steps as non associative array' => [[0 => static function(): void {}]]; + yield 'Steps as non associative array' => [[0 => static function (): void {}]]; } /** @@ -105,7 +105,6 @@ public function testConfigureOptionsStepNameMustExistInSteps() $this->factory->create(MultiStepType::class, [], ['steps' => ['step1' => static function (): void {}], 'current_step' => 'step2']); } - public function testConfigureOptionsSetsDefaultValueForCurrentStepName() { $form = $this->factory->create(MultiStepType::class, [], [ From 8784a81582c441b7f19eb84b6d3879c487c44e82 Mon Sep 17 00:00:00 2001 From: Silas Joisten Date: Sun, 19 Jan 2025 10:36:53 +0200 Subject: [PATCH 14/15] Psalm fix --- .../Component/Form/Extension/Core/Type/MultiStepType.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Symfony/Component/Form/Extension/Core/Type/MultiStepType.php b/src/Symfony/Component/Form/Extension/Core/Type/MultiStepType.php index f796841caed5d..28afbba264a1e 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/MultiStepType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/MultiStepType.php @@ -54,7 +54,10 @@ public function configureOptions(OptionsResolver $resolver): void return $value; }) ->setDefault('current_step', static function (Options $options): string { - return array_key_first($options['steps']); + /** @var string $firstStep */ + $firstStep = array_key_first($options['steps']); + + return $firstStep; }); } From 0f0b6459ce845a387b430edb628dea136cd79e2b Mon Sep 17 00:00:00 2001 From: Silas Joisten Date: Sun, 19 Jan 2025 14:15:17 +0200 Subject: [PATCH 15/15] Fix --- .../Extension/Core/Type/MultiStepType.php | 98 +++++++++++++-- .../Extension/Core/Type/MultiStepTypeTest.php | 112 +++++++++++++++++- 2 files changed, 200 insertions(+), 10 deletions(-) diff --git a/src/Symfony/Component/Form/Extension/Core/Type/MultiStepType.php b/src/Symfony/Component/Form/Extension/Core/Type/MultiStepType.php index 28afbba264a1e..26d03cd8e1ca0 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/MultiStepType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/MultiStepType.php @@ -58,7 +58,56 @@ public function configureOptions(OptionsResolver $resolver): void $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, + '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 @@ -70,6 +119,18 @@ public function buildForm(FormBuilderInterface $builder, array $options): void } elseif (\is_string($currentStep)) { $builder->add($options['current_step'], $currentStep); } + + $builder->add('back', SubmitType::class, [ + '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 @@ -77,14 +138,37 @@ public function buildView(FormView $view, FormInterface $form, array $options): $view->vars['current_step'] = $options['current_step']; $view->vars['steps'] = array_keys($options['steps']); $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']; + } - /** @var int $currentStepIndex */ - $currentStepIndex = array_search($options['current_step'], array_keys($options['steps']), true); - $view->vars['current_step_number'] = $currentStepIndex + 1; - $view->vars['is_first_step'] = 0 === $currentStepIndex; + /** + * @param array $steps + */ + private function currentStepIndex(string $currentStep, array $steps): int + { + /** @var int $currentStep */ + $currentStep = array_search($currentStep, array_keys($steps), true); + + return $currentStep; + } - /** @var int $lastStepIndex */ - $lastStepIndex = array_key_last(array_keys($options['steps'])); - $view->vars['is_last_step'] = $lastStepIndex === $currentStepIndex; + /** + * @param array $steps + */ + private function isLastStep(string $currentStep, array $steps): bool + { + return array_key_last(array_keys($steps)) === $this->currentStepIndex($currentStep, $steps); + } + + /** + * @param array $steps + */ + private function isFirstStep(string $currentStep, array $steps): bool + { + return 0 === $this->currentStepIndex($currentStep, $steps); } } diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/Type/MultiStepTypeTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/Type/MultiStepTypeTest.php index bd46d6b06069a..8ed5068ca66b3 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/Type/MultiStepTypeTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/Type/MultiStepTypeTest.php @@ -56,7 +56,7 @@ public static function invalidStepValues(): iterable * * @param array $steps */ - public function testConfigureOptionsMustBeClassStringOrCallable(array $steps) + public function testConfigureOptionsStepsMustBeClassStringOrCallable(array $steps) { self::expectException(InvalidOptionsException::class); self::expectExceptionMessage('The option "steps" with value array is invalid.'); @@ -80,7 +80,7 @@ public static function invalidSteps(): iterable /** * @dataProvider invalidStepNames */ - public function testConfigureOptionsStepNameMustBeString(mixed $steps) + public function testConfigureOptionsCurrentStepMustBeString(mixed $steps) { self::expectException(InvalidOptionsException::class); @@ -97,7 +97,7 @@ public static function invalidStepNames(): iterable yield 'Step name is callable' => [static function (): void {}]; } - public function testConfigureOptionsStepNameMustExistInSteps() + public function testConfigureOptionsCurrentStepMustExistInSteps() { self::expectException(InvalidOptionsException::class); self::expectExceptionMessage('The current step "step2" does not exist.'); @@ -105,6 +105,112 @@ public function testConfigureOptionsStepNameMustExistInSteps() $this->factory->create(MultiStepType::class, [], ['steps' => ['step1' => static function (): void {}], 'current_step' => 'step2']); } + /** + * @dataProvider invalidStepNames + */ + public function testConfigureOptionsNextStepMustBeStringOrNull(mixed $steps) + { + self::expectException(InvalidOptionsException::class); + + $this->factory->create(MultiStepType::class, [], ['steps' => ['step1' => static function (): void {}, 'step2' => static function (): void {}], 'next_step' => $steps]); + } + + public function testNextStepDefault() + { + $form = $this->factory->create(MultiStepType::class, [], [ + 'current_step' => 'step1', + 'steps' => [ + 'step1' => static function (): void {}, + 'step2' => static function (): void {}, + 'step3' => static function (): void {}, + ], + ]); + + self::assertSame('step2', $form->getConfig()->getOption('next_step')); + } + + public function testNextStepDefaultNullWhenNoNextStepsAvailable() + { + $form = $this->factory->create(MultiStepType::class, [], [ + 'current_step' => 'step3', + 'steps' => [ + 'step1' => static function (): void {}, + 'step2' => static function (): void {}, + 'step3' => static function (): void {}, + ], + ]); + + self::assertNull($form->getConfig()->getOption('next_step')); + } + + public function testNextStepMustBeNullOrInSteps() + { + self::expectException(InvalidOptionsException::class); + + $this->factory->create(MultiStepType::class, [], [ + 'current_step' => 'step1', + 'next_step' => 'step20', + 'steps' => [ + 'step1' => static function (): void {}, + 'step2' => static function (): void {}, + 'step3' => static function (): void {}, + ], + ]); + } + + /** + * @dataProvider invalidStepNames + */ + public function testConfigureOptionsPreviousStepMustBeStringOrNull(mixed $steps) + { + self::expectException(InvalidOptionsException::class); + + $this->factory->create(MultiStepType::class, [], ['steps' => ['step1' => static function (): void {}, 'step2' => static function (): void {}], 'previous_step' => $steps]); + } + + public function testPreviousStepDefault() + { + $form = $this->factory->create(MultiStepType::class, [], [ + 'current_step' => 'step2', + 'steps' => [ + 'step1' => static function (): void {}, + 'step2' => static function (): void {}, + 'step3' => static function (): void {}, + ], + ]); + + self::assertSame('step1', $form->getConfig()->getOption('previous_step')); + } + + public function testPreviousStepDefaultNullWhenNoNextStepsAvailable() + { + $form = $this->factory->create(MultiStepType::class, [], [ + 'current_step' => 'step1', + 'steps' => [ + 'step1' => static function (): void {}, + 'step2' => static function (): void {}, + 'step3' => static function (): void {}, + ], + ]); + + self::assertNull($form->getConfig()->getOption('previous_step')); + } + + public function testPreviousStepMustBeNullOrInSteps() + { + self::expectException(InvalidOptionsException::class); + + $this->factory->create(MultiStepType::class, [], [ + 'current_step' => 'step1', + 'previous_step' => 'step20', + 'steps' => [ + 'step1' => static function (): void {}, + 'step2' => static function (): void {}, + 'step3' => static function (): void {}, + ], + ]); + } + public function testConfigureOptionsSetsDefaultValueForCurrentStepName() { $form = $this->factory->create(MultiStepType::class, [], [