Skip to content

[Form] Add FormFlow for multistep forms management #60212

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

Open
wants to merge 1 commit into
base: 7.4
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Add FormFlow for multistep forms management
  • Loading branch information
yceruto committed Jul 30, 2025
commit 92ee39c3c626ce2d97c0b179cbbc95893d6ae49c
5 changes: 5 additions & 0 deletions src/Symfony/Bundle/FrameworkBundle/Resources/config/form.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
use Symfony\Component\Form\Extension\DependencyInjection\DependencyInjectionExtension;
use Symfony\Component\Form\Extension\HtmlSanitizer\Type\TextTypeHtmlSanitizerExtension;
use Symfony\Component\Form\Extension\HttpFoundation\HttpFoundationRequestHandler;
use Symfony\Component\Form\Extension\HttpFoundation\Type\FormFlowTypeSessionDataStorageExtension;
use Symfony\Component\Form\Extension\HttpFoundation\Type\FormTypeHttpFoundationExtension;
use Symfony\Component\Form\Extension\Validator\Type\FormTypeValidatorExtension;
use Symfony\Component\Form\Extension\Validator\Type\RepeatedTypeValidatorExtension;
Expand Down Expand Up @@ -123,6 +124,10 @@
->args([service('form.type_extension.form.request_handler')])
->tag('form.type_extension')

->set('form.type_extension.form.flow.session_data_storage', FormFlowTypeSessionDataStorageExtension::class)
->args([service('request_stack')->ignoreOnInvalid()])
->tag('form.type_extension')

->set('form.type_extension.form.request_handler', HttpFoundationRequestHandler::class)
->args([service('form.server_params')])

Expand Down
4 changes: 2 additions & 2 deletions src/Symfony/Bundle/FrameworkBundle/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
"symfony/dom-crawler": "^6.4|^7.0|^8.0",
"symfony/dotenv": "^6.4|^7.0|^8.0",
"symfony/polyfill-intl-icu": "~1.0",
"symfony/form": "^6.4|^7.0|^8.0",
"symfony/form": "^7.4|^8.0",
"symfony/expression-language": "^6.4|^7.0|^8.0",
"symfony/html-sanitizer": "^6.4|^7.0|^8.0",
"symfony/http-client": "^6.4|^7.0|^8.0",
Expand Down Expand Up @@ -89,7 +89,7 @@
"symfony/dotenv": "<6.4",
"symfony/dom-crawler": "<6.4",
"symfony/http-client": "<6.4",
"symfony/form": "<6.4",
"symfony/form": "<7.4",
"symfony/lock": "<6.4",
"symfony/mailer": "<6.4",
"symfony/messenger": "<6.4",
Expand Down
1 change: 1 addition & 0 deletions src/Symfony/Component/Form/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ CHANGELOG
---

* Add `input=date_point` to `DateTimeType`, `DateType` and `TimeType`
* Add `FormFlow` for multistep forms management

7.3
---
Expand Down
7 changes: 7 additions & 0 deletions src/Symfony/Component/Form/Extension/Core/CoreExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
use Symfony\Component\Form\ChoiceList\Factory\DefaultChoiceListFactory;
use Symfony\Component\Form\ChoiceList\Factory\PropertyAccessDecorator;
use Symfony\Component\Form\Extension\Core\Type\TransformationFailureExtension;
use Symfony\Component\Form\Flow;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
Expand Down Expand Up @@ -78,6 +79,12 @@ protected function loadTypes(): array
new Type\TelType(),
new Type\ColorType($this->translator),
new Type\WeekType(),
new Flow\Type\FlowButtonType(),
new Flow\Type\FlowFinishType(),
new Flow\Type\FlowNavigatorType(),
new Flow\Type\FlowNextType(),
new Flow\Type\FlowPreviousType(),
new Flow\Type\FormFlowType($this->propertyAccessor),
];
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ protected function loadTypeExtensions(): array
{
return [
new Type\FormTypeHttpFoundationExtension(),
new Type\FormFlowTypeSessionDataStorageExtension(),
];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?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\HttpFoundation\Type;

use Symfony\Component\Form\AbstractTypeExtension;
use Symfony\Component\Form\Flow\DataStorage\SessionDataStorage;
use Symfony\Component\Form\Flow\FormFlowBuilderInterface;
use Symfony\Component\Form\Flow\Type\FormFlowType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\HttpFoundation\RequestStack;

class FormFlowTypeSessionDataStorageExtension extends AbstractTypeExtension
{
public function __construct(
private readonly ?RequestStack $requestStack = null,
) {
}

public function buildForm(FormBuilderInterface $builder, array $options): void
{
\assert($builder instanceof FormFlowBuilderInterface);

if (null === $this->requestStack || null !== $options['data_storage']) {
return;
}

$key = \sprintf('_sf_formflow.%s_%s', strtolower(str_replace('\\', '_', $builder->getType()->getInnerType()::class)), $builder->getName());
$builder->setDataStorage(new SessionDataStorage($key, $this->requestStack));
}

public static function getExtendedTypes(): iterable
{
return [FormFlowType::class];
}
}
38 changes: 38 additions & 0 deletions src/Symfony/Component/Form/Flow/AbstractFlowType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?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\Flow;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Flow\Type\FormFlowType;
use Symfony\Component\Form\FormBuilderInterface;

/**
* @author Yonel Ceruto <open@yceruto.dev>
*/
abstract class AbstractFlowType extends AbstractType implements FormFlowTypeInterface
{
final public function buildForm(FormBuilderInterface $builder, array $options): void
{
\assert($builder instanceof FormFlowBuilderInterface);

$this->buildFormFlow($builder, $options);
}

public function buildFormFlow(FormFlowBuilderInterface $builder, array $options): void
{
}

public function getParent(): string
{
return FormFlowType::class;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?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\Flow\DataStorage;

/**
* Handles storing and retrieving form data between steps.
*
* @author Yonel Ceruto <open@yceruto.dev>
*/
interface DataStorageInterface
{
public function save(object|array $data): void;

public function load(object|array|null $default = null): object|array|null;

public function clear(): void;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?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\Flow\DataStorage;

/**
* @author Yonel Ceruto <open@yceruto.dev>
*/
class InMemoryDataStorage implements DataStorageInterface
{
private array $memory = [];

public function __construct(
private readonly string $key,
) {
}

public function save(object|array $data): void
{
$this->memory[$this->key] = $data;
}

public function load(object|array|null $default = null): object|array|null
{
return $this->memory[$this->key] ?? $default;
}

public function clear(): void
{
unset($this->memory[$this->key]);
}
}
33 changes: 33 additions & 0 deletions src/Symfony/Component/Form/Flow/DataStorage/NullDataStorage.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?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\Flow\DataStorage;

/**
* @author Yonel Ceruto <open@yceruto.dev>
*/
final class NullDataStorage implements DataStorageInterface
{
public function save(object|array $data): void
{
// no-op
}

public function load(object|array|null $default = null): object|array|null
{
return $default;
}

public function clear(): void
{
// no-op
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?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\Flow\DataStorage;

use Symfony\Component\HttpFoundation\RequestStack;

/**
* @author Yonel Ceruto <open@yceruto.dev>
*/
class SessionDataStorage implements DataStorageInterface
{
public function __construct(
private readonly string $key,
private readonly RequestStack $requestStack,
) {
}

public function save(object|array $data): void
{
$this->requestStack->getSession()->set($this->key, $data);
}

public function load(object|array|null $default = null): object|array|null
{
return $this->requestStack->getSession()->get($this->key, $default);
}

public function clear(): void
{
$this->requestStack->getSession()->remove($this->key);
}
}
92 changes: 92 additions & 0 deletions src/Symfony/Component/Form/Flow/FlowButton.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
<?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\Flow;

use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\SubmitButton;

/**
* A button that submits the form and handles an action.
*
* @author Yonel Ceruto <open@yceruto.dev>
*/
class FlowButton extends SubmitButton implements FlowButtonInterface
{
private mixed $data = null;
private bool $handled = false;

public function submit(array|string|null $submittedData, bool $clearMissing = true): static
{
if ($this->isSubmitted()) {
return $this; // ignore double submit
}

parent::submit($submittedData, $clearMissing);

if ($this->isSubmitted()) {
$this->data = $submittedData;
}

return $this;
}

public function getViewData(): mixed
{
return $this->data;
}

public function handle(): void
{
/** @var FormInterface $form */
$form = $this->getParent();
$data = $form->getData();

while ($form && !$form instanceof FormFlowInterface) {
$form = $form->getParent();
}

$handler = $this->getConfig()->getOption('handler');
$handler($data, $this, $form);

$this->handled = true;
}

public function isHandled(): bool
{
return $this->handled;
}

public function isResetAction(): bool
{
return 'reset' === $this->getConfig()->getAttribute('action');
}

public function isPreviousAction(): bool
{
return 'previous' === $this->getConfig()->getAttribute('action');
}

public function isNextAction(): bool
{
return 'next' === $this->getConfig()->getAttribute('action');
}

public function isFinishAction(): bool
{
return 'finish' === $this->getConfig()->getAttribute('action');
}

public function isClearSubmission(): bool
{
return $this->getConfig()->getOption('clear_submission');
}
}
Loading
Loading
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