Skip to content

Commit cdac9e2

Browse files
committed
Add FormFlow component
1 parent 5276de0 commit cdac9e2

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+2772
-2
lines changed

src/Symfony/Bundle/FrameworkBundle/Controller/AbstractController.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@
1717
use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;
1818
use Symfony\Component\DependencyInjection\ParameterBag\ContainerBagInterface;
1919
use Symfony\Component\Form\Extension\Core\Type\FormType;
20+
use Symfony\Component\Form\Flow\FormFlowBuilderInterface;
21+
use Symfony\Component\Form\Flow\FormFlowInterface;
22+
use Symfony\Component\Form\Flow\FormFlowTypeInterface;
2023
use Symfony\Component\Form\FormBuilderInterface;
2124
use Symfony\Component\Form\FormFactoryInterface;
2225
use Symfony\Component\Form\FormInterface;
@@ -345,6 +348,8 @@ protected function createAccessDeniedException(string $message = 'Access Denied.
345348

346349
/**
347350
* Creates and returns a Form instance from the type of the form.
351+
*
352+
* @return ($type is class-string<FormFlowTypeInterface> ? FormFlowInterface : FormInterface)
348353
*/
349354
protected function createForm(string $type, mixed $data = null, array $options = []): FormInterface
350355
{
@@ -353,6 +358,8 @@ protected function createForm(string $type, mixed $data = null, array $options =
353358

354359
/**
355360
* Creates and returns a form builder instance.
361+
*
362+
* @return ($type is class-string<FormFlowTypeInterface> ? FormFlowBuilderInterface : FormBuilderInterface)
356363
*/
357364
protected function createFormBuilder(mixed $data = null, array $options = []): FormBuilderInterface
358365
{

src/Symfony/Bundle/FrameworkBundle/Resources/config/form.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
use Symfony\Component\Form\Extension\DependencyInjection\DependencyInjectionExtension;
2525
use Symfony\Component\Form\Extension\HtmlSanitizer\Type\TextTypeHtmlSanitizerExtension;
2626
use Symfony\Component\Form\Extension\HttpFoundation\HttpFoundationRequestHandler;
27+
use Symfony\Component\Form\Extension\HttpFoundation\Type\FormFlowTypeSessionDataStorageExtension;
2728
use Symfony\Component\Form\Extension\HttpFoundation\Type\FormTypeHttpFoundationExtension;
2829
use Symfony\Component\Form\Extension\Validator\Type\FormTypeValidatorExtension;
2930
use Symfony\Component\Form\Extension\Validator\Type\RepeatedTypeValidatorExtension;
@@ -123,6 +124,10 @@
123124
->args([service('form.type_extension.form.request_handler')])
124125
->tag('form.type_extension')
125126

127+
->set('form.type_extension.form.flow.session_data_storage', FormFlowTypeSessionDataStorageExtension::class)
128+
->args([service('request_stack')->ignoreOnInvalid()])
129+
->tag('form.type_extension')
130+
126131
->set('form.type_extension.form.request_handler', HttpFoundationRequestHandler::class)
127132
->args([service('form.server_params')])
128133

src/Symfony/Bundle/FrameworkBundle/composer.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
"symfony/dom-crawler": "^6.4|^7.0",
4646
"symfony/dotenv": "^6.4|^7.0",
4747
"symfony/polyfill-intl-icu": "~1.0",
48-
"symfony/form": "^6.4|^7.0",
48+
"symfony/form": "^7.3",
4949
"symfony/expression-language": "^6.4|^7.0",
5050
"symfony/html-sanitizer": "^6.4|^7.0",
5151
"symfony/http-client": "^6.4|^7.0",
@@ -88,7 +88,7 @@
8888
"symfony/dotenv": "<6.4",
8989
"symfony/dom-crawler": "<6.4",
9090
"symfony/http-client": "<6.4",
91-
"symfony/form": "<6.4",
91+
"symfony/form": "<7.3",
9292
"symfony/json-streamer": ">=7.4",
9393
"symfony/lock": "<6.4",
9494
"symfony/mailer": "<6.4",

src/Symfony/Component/Form/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
CHANGELOG
22
=========
33

4+
7.4
5+
---
6+
7+
* Add `FormFlow` component for multistep forms management
8+
49
7.3
510
---
611

src/Symfony/Component/Form/Extension/Core/CoreExtension.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ protected function loadTypes(): array
7878
new Type\TelType(),
7979
new Type\ColorType($this->translator),
8080
new Type\WeekType(),
81+
new Type\FormFlowType(),
8182
];
8283
}
8384

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Form\Extension\Core\Type;
13+
14+
use Symfony\Component\Form\AbstractType;
15+
use Symfony\Component\Form\Flow\ActionButtonTypeInterface;
16+
use Symfony\Component\OptionsResolver\Options;
17+
use Symfony\Component\OptionsResolver\OptionsResolver;
18+
19+
/**
20+
* An action-based submit button for a form flow.
21+
*
22+
* @author Yonel Ceruto <open@yceruto.dev>
23+
*/
24+
class FormFlowActionType extends AbstractType implements ActionButtonTypeInterface
25+
{
26+
public function configureOptions(OptionsResolver $resolver): void
27+
{
28+
$resolver->define('action')
29+
->info('The action name of the button')
30+
->default('')
31+
->allowedTypes('string');
32+
33+
$resolver->define('handler')
34+
->info('A callable that will be called when this button is clicked')
35+
->default(null)
36+
->allowedTypes('null', 'callable');
37+
38+
$resolver->define('include_if')
39+
->info('Decide whether to include this button in the current form')
40+
->default(null)
41+
->allowedTypes('null', 'callable');
42+
43+
$resolver->define('clear_submission')
44+
->info('Whether the submitted data will be cleared when this button is clicked')
45+
->default(function (Options $options) {
46+
return 'reset' === $options['action'] || 'back' === $options['action'];
47+
})
48+
->allowedTypes('bool');
49+
50+
$resolver->setDefault('validate', function (Options $options) {
51+
return !$options['clear_submission'];
52+
});
53+
54+
$resolver->setDefault('validation_groups', function (Options $options) {
55+
return $options['clear_submission'] ? false : null;
56+
});
57+
}
58+
59+
public function getParent(): string
60+
{
61+
return SubmitType::class;
62+
}
63+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Form\Extension\Core\Type;
13+
14+
use Symfony\Component\Form\AbstractType;
15+
use Symfony\Component\Form\Flow\FormFlowCursor;
16+
use Symfony\Component\Form\FormBuilderInterface;
17+
use Symfony\Component\OptionsResolver\OptionsResolver;
18+
19+
/**
20+
* A navigator type that defines default actions to interact with a form flow.
21+
*
22+
* @author Yonel Ceruto <open@yceruto.dev>
23+
*/
24+
class FormFlowNavigatorType extends AbstractType
25+
{
26+
public function buildForm(FormBuilderInterface $builder, array $options): void
27+
{
28+
$builder->add('back', FormFlowActionType::class, [
29+
'action' => 'back',
30+
'include_if' => fn (FormFlowCursor $cursor): bool => $cursor->canMoveBack(),
31+
]);
32+
33+
$builder->add('next', FormFlowActionType::class, [
34+
'action' => 'next',
35+
'include_if' => fn (FormFlowCursor $cursor): bool => $cursor->canMoveNext(),
36+
]);
37+
38+
$builder->add('finish', FormFlowActionType::class, [
39+
'action' => 'finish',
40+
'include_if' => fn (FormFlowCursor $cursor): bool => $cursor->isLastStep(),
41+
]);
42+
}
43+
44+
public function configureOptions(OptionsResolver $resolver): void
45+
{
46+
$resolver->setDefaults([
47+
'label' => false,
48+
'mapped' => false,
49+
'priority' => -100,
50+
]);
51+
}
52+
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Form\Extension\Core\Type;
13+
14+
use Symfony\Component\Form\Flow\AbstractFlowType;
15+
use Symfony\Component\Form\Flow\DataAccessor\PropertyPathStepAccessor;
16+
use Symfony\Component\Form\Flow\DataAccessor\StepAccessorInterface;
17+
use Symfony\Component\Form\Flow\DataStorage\DataStorageInterface;
18+
use Symfony\Component\Form\Flow\DataStorage\NullDataStorage;
19+
use Symfony\Component\Form\Flow\FormFlowBuilderInterface;
20+
use Symfony\Component\Form\Flow\FormFlowInterface;
21+
use Symfony\Component\Form\Flow\FormFlowStepView;
22+
use Symfony\Component\Form\FormBuilderInterface;
23+
use Symfony\Component\Form\FormInterface;
24+
use Symfony\Component\Form\FormView;
25+
use Symfony\Component\OptionsResolver\Exception\MissingOptionsException;
26+
use Symfony\Component\OptionsResolver\Options;
27+
use Symfony\Component\OptionsResolver\OptionsResolver;
28+
use Symfony\Component\PropertyAccess\PropertyAccess;
29+
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
30+
use Symfony\Component\PropertyAccess\PropertyPath;
31+
use Symfony\Component\PropertyAccess\PropertyPathInterface;
32+
33+
/**
34+
* A multistep form.
35+
*
36+
* @author Yonel Ceruto <open@yceruto.dev>
37+
*/
38+
final class FormFlowType extends AbstractFlowType
39+
{
40+
public function __construct(
41+
private ?PropertyAccessorInterface $propertyAccessor = null,
42+
) {
43+
$this->propertyAccessor ??= PropertyAccess::createPropertyAccessor();
44+
}
45+
46+
/**
47+
* @param FormFlowBuilderInterface $builder
48+
*/
49+
public function buildForm(FormBuilderInterface $builder, array $options): void
50+
{
51+
$builder->setDataStorage($options['data_storage']);
52+
$builder->setStepAccessor($options['step_accessor']);
53+
}
54+
55+
/**
56+
* @param FormFlowInterface $form
57+
*/
58+
public function buildView(FormView $view, FormInterface $form, array $options): void
59+
{
60+
$view->vars['cursor'] = $form->getCursor();
61+
62+
$number = 1;
63+
foreach ($form->getConfig()->getSteps() as $name => $step) {
64+
$view->vars['steps'][$name] = new FormFlowStepView(
65+
number: $number++,
66+
name: $name,
67+
isCurrentStep: $name === $view->vars['cursor']->getCurrentStep(),
68+
canBeSkipped: null !== $step->getSkip(),
69+
isSkipped: $step->isSkipped($form->getViewData()),
70+
);
71+
}
72+
}
73+
74+
public function configureOptions(OptionsResolver $resolver): void
75+
{
76+
$resolver->define('data_storage')
77+
->default(new NullDataStorage())
78+
->allowedTypes(DataStorageInterface::class);
79+
80+
$resolver->define('step_accessor')
81+
->default(function (Options $options) {
82+
if (!isset($options['step_property_path'])) {
83+
throw new MissingOptionsException('Option "step_property_path" is required.');
84+
}
85+
86+
return new PropertyPathStepAccessor($this->propertyAccessor, $options['step_property_path']);
87+
})
88+
->allowedTypes(StepAccessorInterface::class);
89+
90+
$resolver->define('step_property_path')
91+
->info('Required if the default step_accessor is being used')
92+
->allowedTypes('string', PropertyPathInterface::class)
93+
->normalize(function (Options $options, string|PropertyPathInterface $value): PropertyPathInterface {
94+
return \is_string($value) ? new PropertyPath($value) : $value;
95+
});
96+
97+
$resolver->define('auto_reset')
98+
->info('Whether the FormFlow will be reset automatically when it is finished')
99+
->default(true)
100+
->allowedTypes('bool');
101+
102+
$resolver->setDefault('validation_groups', function (FormFlowInterface $flow) {
103+
return ['Default', $flow->getCursor()->getCurrentStep()];
104+
});
105+
106+
$resolver->setDefault('data', function (Options $options) {
107+
return $options['data_class'] ? new $options['data_class']() : [];
108+
});
109+
}
110+
111+
public function getParent(): string
112+
{
113+
return FormType::class;
114+
}
115+
}

src/Symfony/Component/Form/Extension/HttpFoundation/HttpFoundationExtension.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ protected function loadTypeExtensions(): array
2424
{
2525
return [
2626
new Type\FormTypeHttpFoundationExtension(),
27+
new Type\FormFlowTypeSessionDataStorageExtension(),
2728
];
2829
}
2930
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Form\Extension\HttpFoundation\Type;
13+
14+
use Symfony\Component\Form\AbstractTypeExtension;
15+
use Symfony\Component\Form\Extension\Core\Type\FormFlowType;
16+
use Symfony\Component\Form\Flow\DataStorage\NullDataStorage;
17+
use Symfony\Component\Form\Flow\DataStorage\SessionDataStorage;
18+
use Symfony\Component\Form\Flow\FormFlowBuilderInterface;
19+
use Symfony\Component\Form\FormBuilderInterface;
20+
use Symfony\Component\HttpFoundation\RequestStack;
21+
22+
class FormFlowTypeSessionDataStorageExtension extends AbstractTypeExtension
23+
{
24+
public function __construct(
25+
private readonly ?RequestStack $requestStack = null,
26+
) {
27+
}
28+
29+
/**
30+
* @param FormFlowBuilderInterface $builder
31+
*/
32+
public function buildForm(FormBuilderInterface $builder, array $options): void
33+
{
34+
if (null === $this->requestStack || !$builder->getDataStorage() instanceof NullDataStorage) {
35+
return;
36+
}
37+
38+
$key = \sprintf('_sf_formflow.%s_%s', strtolower(str_replace('\\', '_', $builder->getType()->getInnerType()::class)), $builder->getName());
39+
$builder->setDataStorage(new SessionDataStorage($key, $this->requestStack));
40+
}
41+
42+
public static function getExtendedTypes(): iterable
43+
{
44+
yield FormFlowType::class;
45+
}
46+
}

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy