Skip to content
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 Aug 14, 2025
commit be5200a881f0952b0fee44b3a7a3ad0f0eba169b
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 @@ -90,7 +90,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];
}
}
26 changes: 26 additions & 0 deletions src/Symfony/Component/Form/Flow/AbstractFlowButtonType.php
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;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Flow\Type\FlowButtonType;

/**
* @author Yonel Ceruto <open@yceruto.dev>
*/
abstract class AbstractFlowButtonType extends AbstractType implements FlowButtonTypeInterface
{
public function getParent(): string
{
return FlowButtonType::class;
}
}
62 changes: 62 additions & 0 deletions src/Symfony/Component/Form/Flow/AbstractFlowType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<?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;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;

/**
* @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);
}

final public function buildView(FormView $view, FormInterface $form, array $options): void
{
\assert($form instanceof FormFlowInterface);

$this->buildViewFlow($view, $form, $options);
}

final public function finishView(FormView $view, FormInterface $form, array $options): void
{
\assert($form instanceof FormFlowInterface);

$this->finishViewFlow($view, $form, $options);
}

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

public function buildViewFlow(FormView $view, FormFlowInterface $form, array $options): void
{
}

public function finishViewFlow(FormView $view, FormFlowInterface $form, 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);
}
}
Loading
Loading