Skip to content

Navigation Menu

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

Commit 7da47cf

Browse filesBrowse files
committed
Add FormFlow component
1 parent 5276de0 commit 7da47cf
Copy full SHA for 7da47cf

Some content is hidden

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

43 files changed

+2745
-2
lines changed

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

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/FrameworkBundle/Controller/AbstractController.php
+7Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/FrameworkBundle/Resources/config/form.php
+5Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/FrameworkBundle/composer.json
+2-2Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Form/CHANGELOG.md
+5Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Form/Extension/Core/CoreExtension.php
+1Lines 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

+63Lines 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+
}
+52Lines 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+
}
+105Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
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+
foreach ($form->getConfig()->getSteps() as $name => $step) {
63+
$isCurrentStep = $name === $view->vars['cursor']->getCurrentStep();
64+
$view->vars['steps'][$name] = new FormFlowStepView($name, $isCurrentStep, $step->isSkipped($form->getViewData()));
65+
}
66+
}
67+
68+
public function configureOptions(OptionsResolver $resolver): void
69+
{
70+
$resolver->define('data_storage')
71+
->default(new NullDataStorage())
72+
->allowedTypes(DataStorageInterface::class);
73+
74+
$resolver->define('step_accessor')
75+
->default(function (Options $options) {
76+
if (!isset($options['step_property_path'])) {
77+
throw new MissingOptionsException('Option "step_property_path" is required.');
78+
}
79+
80+
return new PropertyPathStepAccessor($this->propertyAccessor, $options['step_property_path']);
81+
})
82+
->allowedTypes(StepAccessorInterface::class);
83+
84+
$resolver->define('step_property_path')
85+
->info('Required if the default step_accessor is being used')
86+
->allowedTypes('string', PropertyPathInterface::class)
87+
->normalize(function (Options $options, string|PropertyPathInterface $value): PropertyPathInterface {
88+
return \is_string($value) ? new PropertyPath($value) : $value;
89+
});
90+
91+
$resolver->define('auto_reset')
92+
->info('Whether the FormFlow will be reset automatically when it is finished')
93+
->default(true)
94+
->allowedTypes('bool');
95+
96+
$resolver->setDefault('validation_groups', function (FormFlowInterface $flow) {
97+
return ['Default', $flow->getCursor()->getCurrentStep()];
98+
});
99+
}
100+
101+
public function getParent(): string
102+
{
103+
return FormType::class;
104+
}
105+
}

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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Form/Extension/HttpFoundation/HttpFoundationExtension.php
+1Lines 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
}
+46Lines 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('_form_flow.%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+
}
+26Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
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\Flow;
13+
14+
use Symfony\Component\Form\AbstractType;
15+
use Symfony\Component\Form\Extension\Core\Type\FormFlowType;
16+
17+
/**
18+
* @author Yonel Ceruto <open@yceruto.dev>
19+
*/
20+
abstract class AbstractFlowType extends AbstractType implements FormFlowTypeInterface
21+
{
22+
public function getParent(): string
23+
{
24+
return FormFlowType::class;
25+
}
26+
}

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.