mirror of
https://github.com/symfony/form.git
synced 2026-03-24 00:02:23 +01:00
Add FormFlow for multistep forms management
This commit is contained in:
@@ -7,6 +7,7 @@ CHANGELOG
|
||||
* Add `input=date_point` to `DateTimeType`, `DateType` and `TimeType`
|
||||
* Add support for guessing form type of enum properties
|
||||
* Add `active_at`, `not_active_at` and `legal_tender` options to `CurrencyType`
|
||||
* Add `FormFlow` for multistep forms management
|
||||
|
||||
7.3
|
||||
---
|
||||
|
||||
@@ -17,6 +17,7 @@ use Symfony\Component\Form\ChoiceList\Factory\ChoiceListFactoryInterface;
|
||||
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;
|
||||
@@ -78,6 +79,12 @@ class CoreExtension extends AbstractExtension
|
||||
new Type\TelType(),
|
||||
new Type\ColorType($this->translator),
|
||||
new Type\WeekType(),
|
||||
new Flow\Type\ButtonFlowType(),
|
||||
new Flow\Type\FinishFlowType(),
|
||||
new Flow\Type\NavigatorFlowType(),
|
||||
new Flow\Type\NextFlowType(),
|
||||
new Flow\Type\PreviousFlowType(),
|
||||
new Flow\Type\FormFlowType($this->propertyAccessor),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ class HttpFoundationExtension extends AbstractExtension
|
||||
{
|
||||
return [
|
||||
new Type\FormTypeHttpFoundationExtension(),
|
||||
new Type\FormFlowTypeSessionDataStorageExtension(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
<?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
|
||||
{
|
||||
if (!$builder instanceof FormFlowBuilderInterface) {
|
||||
throw new \InvalidArgumentException(\sprintf('The "%s" can only be used with FormFlowType.', self::class));
|
||||
}
|
||||
|
||||
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
Flow/AbstractButtonFlowType.php
Normal file
26
Flow/AbstractButtonFlowType.php
Normal file
@@ -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\ButtonFlowType;
|
||||
|
||||
/**
|
||||
* @author Yonel Ceruto <open@yceruto.dev>
|
||||
*/
|
||||
abstract class AbstractButtonFlowType extends AbstractType implements ButtonFlowTypeInterface
|
||||
{
|
||||
public function getParent(): string
|
||||
{
|
||||
return ButtonFlowType::class;
|
||||
}
|
||||
}
|
||||
68
Flow/AbstractFlowType.php
Normal file
68
Flow/AbstractFlowType.php
Normal file
@@ -0,0 +1,68 @@
|
||||
<?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
|
||||
{
|
||||
if (!$builder instanceof FormFlowBuilderInterface) {
|
||||
throw new \InvalidArgumentException(\sprintf('The "%s" can only be used with FormFlowType.', self::class));
|
||||
}
|
||||
|
||||
$this->buildFormFlow($builder, $options);
|
||||
}
|
||||
|
||||
final public function buildView(FormView $view, FormInterface $form, array $options): void
|
||||
{
|
||||
if (!$form instanceof FormFlowInterface) {
|
||||
throw new \InvalidArgumentException(\sprintf('The "%s" can only be used with FormFlowType.', self::class));
|
||||
}
|
||||
|
||||
$this->buildViewFlow($view, $form, $options);
|
||||
}
|
||||
|
||||
final public function finishView(FormView $view, FormInterface $form, array $options): void
|
||||
{
|
||||
if (!$form instanceof FormFlowInterface) {
|
||||
throw new \InvalidArgumentException(\sprintf('The "%s" can only be used with FormFlowType.', self::class));
|
||||
}
|
||||
|
||||
$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;
|
||||
}
|
||||
}
|
||||
92
Flow/ButtonFlow.php
Normal file
92
Flow/ButtonFlow.php
Normal file
@@ -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 ButtonFlow extends SubmitButton implements ButtonFlowInterface
|
||||
{
|
||||
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');
|
||||
}
|
||||
}
|
||||
27
Flow/ButtonFlowBuilder.php
Normal file
27
Flow/ButtonFlowBuilder.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?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\ButtonBuilder;
|
||||
|
||||
/**
|
||||
* A builder for {@link ButtonFlow} instances.
|
||||
*
|
||||
* @author Yonel Ceruto <open@yceruto.dev>
|
||||
*/
|
||||
class ButtonFlowBuilder extends ButtonBuilder
|
||||
{
|
||||
public function getForm(): ButtonFlow
|
||||
{
|
||||
return new ButtonFlow($this->getFormConfig());
|
||||
}
|
||||
}
|
||||
56
Flow/ButtonFlowInterface.php
Normal file
56
Flow/ButtonFlowInterface.php
Normal file
@@ -0,0 +1,56 @@
|
||||
<?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\ClickableInterface;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
|
||||
/**
|
||||
* @author Yonel Ceruto <open@yceruto.dev>
|
||||
*/
|
||||
interface ButtonFlowInterface extends FormInterface, ClickableInterface
|
||||
{
|
||||
/**
|
||||
* Executes the callable handler.
|
||||
*/
|
||||
public function handle(): void;
|
||||
|
||||
/**
|
||||
* Checks if the callable handler was already called.
|
||||
*/
|
||||
public function isHandled(): bool;
|
||||
|
||||
/**
|
||||
* Checks if the button's action is 'reset'.
|
||||
*/
|
||||
public function isResetAction(): bool;
|
||||
|
||||
/**
|
||||
* Checks if the button's action is 'previous'.
|
||||
*/
|
||||
public function isPreviousAction(): bool;
|
||||
|
||||
/**
|
||||
* Checks if the button's action is 'next'.
|
||||
*/
|
||||
public function isNextAction(): bool;
|
||||
|
||||
/**
|
||||
* Checks if the button's action is 'finish'.
|
||||
*/
|
||||
public function isFinishAction(): bool;
|
||||
|
||||
/**
|
||||
* Checks if the button is configured to clear submission data.
|
||||
*/
|
||||
public function isClearSubmission(): bool;
|
||||
}
|
||||
23
Flow/ButtonFlowTypeInterface.php
Normal file
23
Flow/ButtonFlowTypeInterface.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?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\FormTypeInterface;
|
||||
|
||||
/**
|
||||
* A type that should be converted into a {@link ButtonFlow} instance.
|
||||
*
|
||||
* @author Yonel Ceruto <open@yceruto.dev>
|
||||
*/
|
||||
interface ButtonFlowTypeInterface extends FormTypeInterface
|
||||
{
|
||||
}
|
||||
26
Flow/DataStorage/DataStorageInterface.php
Normal file
26
Flow/DataStorage/DataStorageInterface.php
Normal file
@@ -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;
|
||||
}
|
||||
40
Flow/DataStorage/InMemoryDataStorage.php
Normal file
40
Flow/DataStorage/InMemoryDataStorage.php
Normal file
@@ -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
Flow/DataStorage/NullDataStorage.php
Normal file
33
Flow/DataStorage/NullDataStorage.php
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
41
Flow/DataStorage/SessionDataStorage.php
Normal file
41
Flow/DataStorage/SessionDataStorage.php
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
243
Flow/FormFlow.php
Normal file
243
Flow/FormFlow.php
Normal file
@@ -0,0 +1,243 @@
|
||||
<?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\ClickableInterface;
|
||||
use Symfony\Component\Form\Exception\AlreadySubmittedException;
|
||||
use Symfony\Component\Form\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\Form\Exception\RuntimeException;
|
||||
use Symfony\Component\Form\Exception\TransformationFailedException;
|
||||
use Symfony\Component\Form\Form;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
|
||||
/**
|
||||
* FormFlow represents a multistep form.
|
||||
*
|
||||
* @author Yonel Ceruto <open@yceruto.dev>
|
||||
*
|
||||
* @implements \IteratorAggregate<string, FormInterface>
|
||||
*/
|
||||
class FormFlow extends Form implements FormFlowInterface
|
||||
{
|
||||
private ?ButtonFlowInterface $clickedFlowButton = null;
|
||||
private bool $finished = false;
|
||||
|
||||
public function __construct(
|
||||
private readonly FormFlowConfigInterface $config,
|
||||
private FormFlowCursor $cursor,
|
||||
) {
|
||||
parent::__construct($config);
|
||||
}
|
||||
|
||||
public function submit(mixed $submittedData, bool $clearMissing = true): static
|
||||
{
|
||||
if ($this->isSubmitted()) {
|
||||
throw new AlreadySubmittedException('A form can only be submitted once.');
|
||||
}
|
||||
|
||||
if (!\is_array($submittedData)) {
|
||||
throw new TransformationFailedException('The submitted data must be an array.');
|
||||
}
|
||||
|
||||
if (!$this->isCurrentStepSubmitted($submittedData)) {
|
||||
// the submitted data doesn't match the current step,
|
||||
// it's probably a reload of a POST visit from a different step
|
||||
return $this;
|
||||
}
|
||||
|
||||
$this->setClickedFlowButton($submittedData, $this);
|
||||
|
||||
parent::submit($submittedData, $clearMissing);
|
||||
|
||||
if (!$this->clickedFlowButton || !$this->isSubmitted() || !$this->isValid()) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
$this->finished = $this->clickedFlowButton->isFinishAction();
|
||||
|
||||
if ($this->finished && $this->config->isAutoReset()) {
|
||||
$this->reset();
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function reset(): void
|
||||
{
|
||||
$this->config->getDataStorage()->clear();
|
||||
$this->cursor = $this->cursor->withCurrentStep($this->config->getInitialStep());
|
||||
}
|
||||
|
||||
public function movePrevious(?string $step = null): void
|
||||
{
|
||||
if ($step) {
|
||||
$this->moveBackTo($step);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$this->move(fn (FormFlowCursor $cursor) => $cursor->getPreviousStep())) {
|
||||
throw new RuntimeException('Cannot determine previous step.');
|
||||
}
|
||||
}
|
||||
|
||||
public function moveNext(): void
|
||||
{
|
||||
if (!$this->move(fn (FormFlowCursor $cursor) => $cursor->getNextStep())) {
|
||||
throw new RuntimeException('Cannot determine next step.');
|
||||
}
|
||||
}
|
||||
|
||||
public function newStepForm(): static
|
||||
{
|
||||
return $this->config->getFormFactory()->createNamed($this->config->getName(), $this->config->getType()->getInnerType()::class, $this->getData(), $this->config->getInitialOptions());
|
||||
}
|
||||
|
||||
public function getStepForm(): static
|
||||
{
|
||||
if (!$this->isSubmitted() || !$this->isValid()) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
if ($this->clickedFlowButton && !$this->clickedFlowButton->isHandled()) {
|
||||
$this->clickedFlowButton->handle();
|
||||
}
|
||||
|
||||
if (!$this->isValid()) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
return $this->newStepForm();
|
||||
}
|
||||
|
||||
public function getCursor(): FormFlowCursor
|
||||
{
|
||||
return $this->cursor;
|
||||
}
|
||||
|
||||
public function getConfig(): FormFlowConfigInterface
|
||||
{
|
||||
return $this->config;
|
||||
}
|
||||
|
||||
public function isFinished(): bool
|
||||
{
|
||||
return $this->finished;
|
||||
}
|
||||
|
||||
public function getClickedButton(): ButtonFlowInterface|FormInterface|ClickableInterface|null
|
||||
{
|
||||
return parent::getClickedButton() ?? $this->clickedFlowButton;
|
||||
}
|
||||
|
||||
private function setClickedFlowButton(mixed $submittedData, FormInterface $form): void
|
||||
{
|
||||
if (!\is_array($submittedData)) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($form as $name => $child) {
|
||||
if (!\array_key_exists($name, $submittedData)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($child->count() > 0) {
|
||||
$this->setClickedFlowButton($submittedData[$name], $child);
|
||||
|
||||
if ($this->clickedFlowButton) {
|
||||
return;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$child instanceof ButtonFlowInterface) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$child->submit($submittedData[$name]);
|
||||
|
||||
if ($child->isClicked()) {
|
||||
$this->clickedFlowButton = $child;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function moveBackTo(string $step): void
|
||||
{
|
||||
$steps = $this->cursor->getSteps();
|
||||
|
||||
if (false === $targetIndex = array_search($step, $steps)) {
|
||||
throw new InvalidArgumentException(\sprintf('Step "%s" does not exist.', $step));
|
||||
}
|
||||
|
||||
$currentStep = $this->cursor->getCurrentStep();
|
||||
$currentIndex = $this->cursor->getStepIndex();
|
||||
|
||||
if ($targetIndex === $currentIndex) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($targetIndex > $currentIndex) {
|
||||
throw new RuntimeException(\sprintf('Cannot move back to step "%s" because it is ahead of the current step "%s".', $step, $currentStep));
|
||||
}
|
||||
|
||||
while ($targetIndex < $currentIndex) {
|
||||
$this->movePrevious();
|
||||
$currentIndex = $this->cursor->getStepIndex();
|
||||
}
|
||||
|
||||
if ($targetIndex > $currentIndex) {
|
||||
throw new RuntimeException(\sprintf('Cannot move back to step "%s" because it is a skipped step.', $step));
|
||||
}
|
||||
}
|
||||
|
||||
private function move(\Closure $direction): bool
|
||||
{
|
||||
$data = $this->getData();
|
||||
$cursor = $this->cursor;
|
||||
|
||||
while (true) {
|
||||
if (null === $newStep = $direction($cursor)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($cursor->getCurrentStep() === $newStep) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$cursor = $cursor->withCurrentStep($newStep);
|
||||
|
||||
if (!$this->config->getStep($newStep)->isSkipped($data)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$this->cursor = $cursor;
|
||||
$this->config->getStepAccessor()->setStep($data, $newStep);
|
||||
$this->config->getDataStorage()->save($data);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function isCurrentStepSubmitted(array $submittedData): bool
|
||||
{
|
||||
foreach ($this->cursor->getSteps() as $step) {
|
||||
if (\array_key_exists($step, $submittedData)) {
|
||||
return $step === $this->cursor->getCurrentStep();
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
255
Flow/FormFlowBuilder.php
Normal file
255
Flow/FormFlowBuilder.php
Normal file
@@ -0,0 +1,255 @@
|
||||
<?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\Exception\BadMethodCallException;
|
||||
use Symfony\Component\Form\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\Form\Exception\LogicException;
|
||||
use Symfony\Component\Form\Extension\Core\Type\FormType;
|
||||
use Symfony\Component\Form\Flow\DataStorage\DataStorageInterface;
|
||||
use Symfony\Component\Form\Flow\StepAccessor\StepAccessorInterface;
|
||||
use Symfony\Component\Form\FormBuilder;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
|
||||
/**
|
||||
* A builder for creating {@link FormFlow} instances.
|
||||
*
|
||||
* @author Yonel Ceruto <open@yceruto.dev>
|
||||
*
|
||||
* @implements \IteratorAggregate<string, FormBuilderInterface>
|
||||
*/
|
||||
class FormFlowBuilder extends FormBuilder implements FormFlowBuilderInterface
|
||||
{
|
||||
/**
|
||||
* @var array<string, StepFlowBuilderConfigInterface>
|
||||
*/
|
||||
private array $steps = [];
|
||||
private array $initialOptions = [];
|
||||
private DataStorageInterface $dataStorage;
|
||||
private StepAccessorInterface $stepAccessor;
|
||||
|
||||
public function createStep(string $name, string $type = FormType::class, array $options = []): StepFlowBuilderConfigInterface
|
||||
{
|
||||
if ($this->locked) {
|
||||
throw new BadMethodCallException('FormFlowBuilder methods cannot be accessed anymore once the builder is turned into a FormFlowConfigInterface instance.');
|
||||
}
|
||||
|
||||
return new StepFlowBuilder($name, $type, $options);
|
||||
}
|
||||
|
||||
public function addStep(StepFlowBuilderConfigInterface|string $name, string $type = FormType::class, array $options = [], ?callable $skip = null, int $priority = 0): static
|
||||
{
|
||||
if ($this->locked) {
|
||||
throw new BadMethodCallException('FormFlowBuilder methods cannot be accessed anymore once the builder is turned into a FormFlowConfigInterface instance.');
|
||||
}
|
||||
|
||||
if ($name instanceof StepFlowBuilderConfigInterface) {
|
||||
$this->steps[$name->getName()] = $name;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
$this->steps[$name] = $this->createStep($name, $type, $options)
|
||||
->setSkip($skip ? $skip(...) : null)
|
||||
->setPriority($priority)
|
||||
;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeStep(string $name): static
|
||||
{
|
||||
if ($this->locked) {
|
||||
throw new BadMethodCallException('FormFlowBuilder methods cannot be accessed anymore once the builder is turned into a FormFlowConfigInterface instance.');
|
||||
}
|
||||
|
||||
unset($this->steps[$name]);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function hasStep(string $name): bool
|
||||
{
|
||||
return isset($this->steps[$name]);
|
||||
}
|
||||
|
||||
public function getStep(string $name): StepFlowBuilderConfigInterface
|
||||
{
|
||||
return $this->steps[$name] ?? throw new InvalidArgumentException(\sprintf('Step "%s" does not exist.', $name));
|
||||
}
|
||||
|
||||
public function getSteps(): array
|
||||
{
|
||||
return $this->steps;
|
||||
}
|
||||
|
||||
public function setInitialOptions(array $options): static
|
||||
{
|
||||
if ($this->locked) {
|
||||
throw new BadMethodCallException('FormFlowBuilder methods cannot be accessed anymore once the builder is turned into a FormFlowConfigInterface instance.');
|
||||
}
|
||||
|
||||
$this->initialOptions = $options;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getInitialStep(): string
|
||||
{
|
||||
$defaultStep = (string) key($this->steps);
|
||||
|
||||
if (!isset($this->initialOptions['data'])) {
|
||||
return $defaultStep;
|
||||
}
|
||||
|
||||
return (string) $this->stepAccessor->getStep($this->initialOptions['data'], $defaultStep);
|
||||
}
|
||||
|
||||
public function getInitialOptions(): array
|
||||
{
|
||||
return $this->initialOptions;
|
||||
}
|
||||
|
||||
public function setDataStorage(DataStorageInterface $dataStorage): static
|
||||
{
|
||||
if ($this->locked) {
|
||||
throw new BadMethodCallException('FormFlowBuilder methods cannot be accessed anymore once the builder is turned into a FormFlowConfigInterface instance.');
|
||||
}
|
||||
|
||||
$this->dataStorage = $dataStorage;
|
||||
|
||||
// make sure the current data is available immediately
|
||||
$this->setData($dataStorage->load($this->getData()));
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDataStorage(): DataStorageInterface
|
||||
{
|
||||
return $this->dataStorage;
|
||||
}
|
||||
|
||||
public function setStepAccessor(StepAccessorInterface $stepAccessor): static
|
||||
{
|
||||
if ($this->locked) {
|
||||
throw new BadMethodCallException('FormFlowBuilder methods cannot be accessed anymore once the builder is turned into a FormFlowConfigInterface instance.');
|
||||
}
|
||||
|
||||
$this->stepAccessor = $stepAccessor;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getStepAccessor(): StepAccessorInterface
|
||||
{
|
||||
return $this->stepAccessor;
|
||||
}
|
||||
|
||||
public function isAutoReset(): bool
|
||||
{
|
||||
return $this->getOption('auto_reset');
|
||||
}
|
||||
|
||||
public function getFormConfig(): FormFlowConfigInterface
|
||||
{
|
||||
/** @var self $config */
|
||||
$config = parent::getFormConfig();
|
||||
|
||||
foreach ($config->steps as $name => $step) {
|
||||
$config->steps[$name] = $step->getStepConfig();
|
||||
}
|
||||
|
||||
return $config;
|
||||
}
|
||||
|
||||
public function getForm(): FormFlowInterface
|
||||
{
|
||||
if ($this->locked) {
|
||||
throw new BadMethodCallException('FormFlowBuilder methods cannot be accessed anymore once the builder is turned into a FormFlowConfigInterface instance.');
|
||||
}
|
||||
|
||||
$flow = $this->createFormFlow();
|
||||
|
||||
foreach ($this->all() as $child) {
|
||||
if ($child instanceof FormFlowBuilderInterface) {
|
||||
throw new LogicException('Nested form flows is not currently supported.');
|
||||
}
|
||||
|
||||
// Automatic initialization is only supported on root forms
|
||||
$flow->add($child->setAutoInitialize(false)->getForm());
|
||||
}
|
||||
|
||||
if ($this->getAutoInitialize()) {
|
||||
// Automatically initialize the form if it is configured so
|
||||
$flow->initialize();
|
||||
}
|
||||
|
||||
return $flow;
|
||||
}
|
||||
|
||||
private function createFormFlow(): FormFlowInterface
|
||||
{
|
||||
if (!$this->steps) {
|
||||
throw new InvalidArgumentException('Steps not configured.');
|
||||
}
|
||||
|
||||
uasort($this->steps, static function (StepFlowBuilderConfigInterface $a, StepFlowBuilderConfigInterface $b) {
|
||||
return $b->getPriority() <=> $a->getPriority();
|
||||
});
|
||||
|
||||
$currentStep = $this->resolveCurrentStep();
|
||||
|
||||
if (!isset($this->steps[$currentStep])) {
|
||||
throw new InvalidArgumentException(\sprintf('Step form "%s" is not defined.', $currentStep));
|
||||
}
|
||||
|
||||
$step = $this->steps[$currentStep];
|
||||
$this->add($step->getName(), $step->getType(), $step->getOptions());
|
||||
|
||||
$cursor = new FormFlowCursor(array_keys($this->steps), $currentStep);
|
||||
$this->pruneActionButtons($this, $cursor);
|
||||
|
||||
return new FormFlow($this->getFormConfig(), $cursor);
|
||||
}
|
||||
|
||||
private function resolveCurrentStep(): string
|
||||
{
|
||||
$data = $this->getData();
|
||||
|
||||
if (!$currentStep = $this->getStepAccessor()->getStep($data)) {
|
||||
$currentStep = key($this->steps);
|
||||
$this->getStepAccessor()->setStep($data, $currentStep);
|
||||
$this->setData($data);
|
||||
}
|
||||
|
||||
return $currentStep;
|
||||
}
|
||||
|
||||
private function pruneActionButtons(FormBuilderInterface $builder, FormFlowCursor $cursor): void
|
||||
{
|
||||
foreach ($builder->all() as $child) {
|
||||
if ($child->count() > 0) {
|
||||
$this->pruneActionButtons($child, $cursor);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$child instanceof ButtonFlowBuilder || !\is_callable($include = $child->getOption('include_if'))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$include($cursor)) {
|
||||
$builder->remove($child->getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
74
Flow/FormFlowBuilderInterface.php
Normal file
74
Flow/FormFlowBuilderInterface.php
Normal file
@@ -0,0 +1,74 @@
|
||||
<?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\Extension\Core\Type\FormType;
|
||||
use Symfony\Component\Form\Flow\DataStorage\DataStorageInterface;
|
||||
use Symfony\Component\Form\Flow\StepAccessor\StepAccessorInterface;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
|
||||
/**
|
||||
* @author Yonel Ceruto <open@yceruto.dev>
|
||||
*
|
||||
* @extends \Traversable<string, FormBuilderInterface>
|
||||
*/
|
||||
interface FormFlowBuilderInterface extends FormBuilderInterface, FormFlowConfigInterface
|
||||
{
|
||||
/**
|
||||
* Creates a new step builder.
|
||||
*/
|
||||
public function createStep(string $name, string $type = FormType::class, array $options = []): StepFlowBuilderConfigInterface;
|
||||
|
||||
/**
|
||||
* Adds a step to the form flow.
|
||||
*/
|
||||
public function addStep(StepFlowBuilderConfigInterface|string $name, string $type = FormType::class, array $options = [], ?callable $skip = null, int $priority = 0): static;
|
||||
|
||||
/**
|
||||
* Removes a step from the form flow.
|
||||
*/
|
||||
public function removeStep(string $name): static;
|
||||
|
||||
/**
|
||||
* Returns a step builder by name.
|
||||
*/
|
||||
public function getStep(string $name): StepFlowBuilderConfigInterface;
|
||||
|
||||
/**
|
||||
* Returns all step builders.
|
||||
*
|
||||
* @return array<string, StepFlowBuilderConfigInterface>
|
||||
*/
|
||||
public function getSteps(): array;
|
||||
|
||||
/**
|
||||
* Sets the initial options for the form flow.
|
||||
*
|
||||
* @param array<string, mixed> $options
|
||||
*/
|
||||
public function setInitialOptions(array $options): static;
|
||||
|
||||
/**
|
||||
* Sets the data storage for the form flow.
|
||||
*/
|
||||
public function setDataStorage(DataStorageInterface $dataStorage): static;
|
||||
|
||||
/**
|
||||
* Sets the step accessor for the form flow.
|
||||
*/
|
||||
public function setStepAccessor(StepAccessorInterface $stepAccessor): static;
|
||||
|
||||
/**
|
||||
* Creates and returns the form flow instance.
|
||||
*/
|
||||
public function getForm(): FormFlowInterface;
|
||||
}
|
||||
68
Flow/FormFlowConfigInterface.php
Normal file
68
Flow/FormFlowConfigInterface.php
Normal file
@@ -0,0 +1,68 @@
|
||||
<?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\Flow\DataStorage\DataStorageInterface;
|
||||
use Symfony\Component\Form\Flow\StepAccessor\StepAccessorInterface;
|
||||
use Symfony\Component\Form\FormConfigInterface;
|
||||
|
||||
/**
|
||||
* The configuration of a {@link FormFlow} object.
|
||||
*
|
||||
* @author Yonel Ceruto <open@yceruto.dev>
|
||||
*/
|
||||
interface FormFlowConfigInterface extends FormConfigInterface
|
||||
{
|
||||
/**
|
||||
* Checks if a step with the given name exists.
|
||||
*/
|
||||
public function hasStep(string $name): bool;
|
||||
|
||||
/**
|
||||
* Returns the step with the given name.
|
||||
*/
|
||||
public function getStep(string $name): StepFlowConfigInterface;
|
||||
|
||||
/**
|
||||
* Returns all steps.
|
||||
*
|
||||
* @return array<string, StepFlowConfigInterface>
|
||||
*/
|
||||
public function getSteps(): array;
|
||||
|
||||
/**
|
||||
* Returns the name of the initial step.
|
||||
*/
|
||||
public function getInitialStep(): string;
|
||||
|
||||
/**
|
||||
* Returns the initial options for the form flow.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function getInitialOptions(): array;
|
||||
|
||||
/**
|
||||
* Returns the data storage for the form flow.
|
||||
*/
|
||||
public function getDataStorage(): DataStorageInterface;
|
||||
|
||||
/**
|
||||
* Returns the step accessor for the form flow.
|
||||
*/
|
||||
public function getStepAccessor(): StepAccessorInterface;
|
||||
|
||||
/**
|
||||
* Checks if the form flow is configured to auto reset once it's finished.
|
||||
*/
|
||||
public function isAutoReset(): bool;
|
||||
}
|
||||
103
Flow/FormFlowCursor.php
Normal file
103
Flow/FormFlowCursor.php
Normal file
@@ -0,0 +1,103 @@
|
||||
<?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\Exception\InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* @author Yonel Ceruto <open@yceruto.dev>
|
||||
*/
|
||||
class FormFlowCursor
|
||||
{
|
||||
/**
|
||||
* @param array<string> $steps
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly array $steps,
|
||||
private readonly string $currentStep,
|
||||
) {
|
||||
if (!\in_array($currentStep, $steps, true)) {
|
||||
throw new InvalidArgumentException(\sprintf('Step "%s" does not exist. Available steps are: "%s".', $currentStep, implode('", "', $steps)));
|
||||
}
|
||||
}
|
||||
|
||||
public function getSteps(): array
|
||||
{
|
||||
return $this->steps;
|
||||
}
|
||||
|
||||
public function getTotalSteps(): int
|
||||
{
|
||||
return \count($this->steps);
|
||||
}
|
||||
|
||||
public function getStepIndex(): int
|
||||
{
|
||||
return (int) array_search($this->currentStep, $this->steps, true);
|
||||
}
|
||||
|
||||
public function getFirstStep(): string
|
||||
{
|
||||
return $this->steps[0];
|
||||
}
|
||||
|
||||
public function getPreviousStep(): ?string
|
||||
{
|
||||
$currentPos = array_search($this->currentStep, $this->steps, true);
|
||||
|
||||
return $this->steps[$currentPos - 1] ?? null;
|
||||
}
|
||||
|
||||
public function getCurrentStep(): string
|
||||
{
|
||||
return $this->currentStep;
|
||||
}
|
||||
|
||||
public function withCurrentStep(string $step): self
|
||||
{
|
||||
return new self($this->steps, $step);
|
||||
}
|
||||
|
||||
public function getNextStep(): ?string
|
||||
{
|
||||
$currentPos = array_search($this->currentStep, $this->steps, true);
|
||||
|
||||
return $this->steps[$currentPos + 1] ?? null;
|
||||
}
|
||||
|
||||
public function getLastStep(): string
|
||||
{
|
||||
return $this->steps[\count($this->steps) - 1];
|
||||
}
|
||||
|
||||
public function isFirstStep(): bool
|
||||
{
|
||||
return 0 === array_search($this->currentStep, $this->steps, true);
|
||||
}
|
||||
|
||||
public function isLastStep(): bool
|
||||
{
|
||||
$currentPos = array_search($this->currentStep, $this->steps, true);
|
||||
|
||||
return \count($this->steps) === $currentPos + 1;
|
||||
}
|
||||
|
||||
public function canMoveBack(): bool
|
||||
{
|
||||
return null !== $this->getPreviousStep();
|
||||
}
|
||||
|
||||
public function canMoveNext(): bool
|
||||
{
|
||||
return null !== $this->getNextStep();
|
||||
}
|
||||
}
|
||||
74
Flow/FormFlowInterface.php
Normal file
74
Flow/FormFlowInterface.php
Normal file
@@ -0,0 +1,74 @@
|
||||
<?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\ClickableInterface;
|
||||
use Symfony\Component\Form\Exception\RuntimeException;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
|
||||
/**
|
||||
* @author Yonel Ceruto <open@yceruto.dev>
|
||||
*/
|
||||
interface FormFlowInterface extends FormInterface
|
||||
{
|
||||
/**
|
||||
* Returns the button used to submit the form.
|
||||
*/
|
||||
public function getClickedButton(): ButtonFlowInterface|FormInterface|ClickableInterface|null;
|
||||
|
||||
/**
|
||||
* Resets the flow by clearing stored data and setting the cursor to the initial step.
|
||||
*/
|
||||
public function reset(): void;
|
||||
|
||||
/**
|
||||
* Moves back to a previous step in the flow.
|
||||
*
|
||||
* @param string|null $step The step to move back to, or null to move back one step
|
||||
*
|
||||
* @throws RuntimeException If the previous step cannot be determined
|
||||
*/
|
||||
public function movePrevious(?string $step = null): void;
|
||||
|
||||
/**
|
||||
* Moves to the next step in the flow.
|
||||
*
|
||||
* @throws RuntimeException If the next step cannot be determined
|
||||
*/
|
||||
public function moveNext(): void;
|
||||
|
||||
/**
|
||||
* Creates a new form for the current step with initial options.
|
||||
*/
|
||||
public function newStepForm(): static;
|
||||
|
||||
/**
|
||||
* Gets the form for the current step, handling any action if needed.
|
||||
* Returns a new step form if the current form is valid and submitted.
|
||||
*/
|
||||
public function getStepForm(): static;
|
||||
|
||||
/**
|
||||
* Returns the cursor that tracks the current position in the flow.
|
||||
*/
|
||||
public function getCursor(): FormFlowCursor;
|
||||
|
||||
/**
|
||||
* Returns the configuration for this flow.
|
||||
*/
|
||||
public function getConfig(): FormFlowConfigInterface;
|
||||
|
||||
/**
|
||||
* Checks if the flow has been completed.
|
||||
*/
|
||||
public function isFinished(): bool;
|
||||
}
|
||||
62
Flow/FormFlowTypeInterface.php
Normal file
62
Flow/FormFlowTypeInterface.php
Normal file
@@ -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\FormTypeInterface;
|
||||
use Symfony\Component\Form\FormView;
|
||||
|
||||
/**
|
||||
* A type that should be converted into a {@link FormFlow} instance.
|
||||
*
|
||||
* @author Yonel Ceruto <open@yceruto.dev>
|
||||
*/
|
||||
interface FormFlowTypeInterface extends FormTypeInterface
|
||||
{
|
||||
/**
|
||||
* Builds the multistep form.
|
||||
*
|
||||
* This method is called for each multistep type. Type extensions can further
|
||||
* modify the multistep form.
|
||||
*
|
||||
* @param array<string, mixed> $options
|
||||
*/
|
||||
public function buildFormFlow(FormFlowBuilderInterface $builder, array $options): void;
|
||||
|
||||
/**
|
||||
* Builds the multistep form view.
|
||||
*
|
||||
* This method is called for each multistep type. Type extensions can further
|
||||
* modify the view.
|
||||
*
|
||||
* A view of a multistep form is built before the views of the child forms are built.
|
||||
* This means that you cannot access child views in this method. If you need
|
||||
* to do so, move your logic to {@link finishViewFlow()} instead.
|
||||
*
|
||||
* @param array<string, mixed> $options
|
||||
*/
|
||||
public function buildViewFlow(FormView $view, FormFlowInterface $form, array $options): void;
|
||||
|
||||
/**
|
||||
* Finishes the multistep form view.
|
||||
*
|
||||
* This method gets called for each multistep type. Type extensions can further
|
||||
* modify the view.
|
||||
*
|
||||
* When this method is called, views of the multistep form's children have already
|
||||
* been built and finished and can be accessed. You should only implement
|
||||
* such logic in this method that actually accesses child views. For everything
|
||||
* else you are recommended to implement {@link buildViewFlow()} instead.
|
||||
*
|
||||
* @param array<string, mixed> $options
|
||||
*/
|
||||
public function finishViewFlow(FormView $view, FormFlowInterface $form, array $options): void;
|
||||
}
|
||||
37
Flow/StepAccessor/PropertyPathStepAccessor.php
Normal file
37
Flow/StepAccessor/PropertyPathStepAccessor.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?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\StepAccessor;
|
||||
|
||||
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
|
||||
use Symfony\Component\PropertyAccess\PropertyPathInterface;
|
||||
|
||||
/**
|
||||
* @author Yonel Ceruto <open@yceruto.dev>
|
||||
*/
|
||||
class PropertyPathStepAccessor implements StepAccessorInterface
|
||||
{
|
||||
public function __construct(
|
||||
private readonly PropertyAccessorInterface $propertyAccessor,
|
||||
private readonly PropertyPathInterface $propertyPath,
|
||||
) {
|
||||
}
|
||||
|
||||
public function getStep(object|array $data, ?string $default = null): ?string
|
||||
{
|
||||
return $this->propertyAccessor->getValue($data, $this->propertyPath) ?: $default;
|
||||
}
|
||||
|
||||
public function setStep(object|array &$data, string $step): void
|
||||
{
|
||||
$this->propertyAccessor->setValue($data, $this->propertyPath, $step);
|
||||
}
|
||||
}
|
||||
24
Flow/StepAccessor/StepAccessorInterface.php
Normal file
24
Flow/StepAccessor/StepAccessorInterface.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?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\StepAccessor;
|
||||
|
||||
/**
|
||||
* Reads from or writes the current step name to a provided data source.
|
||||
*
|
||||
* @author Yonel Ceruto <open@yceruto.dev>
|
||||
*/
|
||||
interface StepAccessorInterface
|
||||
{
|
||||
public function getStep(object|array $data, ?string $default = null): ?string;
|
||||
|
||||
public function setStep(object|array &$data, string $step): void;
|
||||
}
|
||||
112
Flow/StepFlowBuilder.php
Normal file
112
Flow/StepFlowBuilder.php
Normal file
@@ -0,0 +1,112 @@
|
||||
<?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\Exception\BadMethodCallException;
|
||||
use Symfony\Component\Form\FormTypeInterface;
|
||||
|
||||
/**
|
||||
* @author Yonel Ceruto <open@yceruto.dev>
|
||||
*/
|
||||
class StepFlowBuilder implements StepFlowBuilderConfigInterface
|
||||
{
|
||||
private bool $locked = false;
|
||||
private int $priority = 0;
|
||||
private ?\Closure $skip = null;
|
||||
|
||||
/**
|
||||
* @param class-string<FormTypeInterface> $type
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly string $name,
|
||||
private readonly string $type,
|
||||
private readonly array $options = [],
|
||||
) {
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function getType(): string
|
||||
{
|
||||
if ($this->locked) {
|
||||
throw new BadMethodCallException('StepFlowBuilder methods cannot be accessed anymore once the builder is turned into a StepFlowConfigInterface instance.');
|
||||
}
|
||||
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
public function getOptions(): array
|
||||
{
|
||||
if ($this->locked) {
|
||||
throw new BadMethodCallException('StepFlowBuilder methods cannot be accessed anymore once the builder is turned into a StepFlowConfigInterface instance.');
|
||||
}
|
||||
|
||||
return $this->options;
|
||||
}
|
||||
|
||||
public function getPriority(): int
|
||||
{
|
||||
return $this->priority;
|
||||
}
|
||||
|
||||
public function setPriority(int $priority): static
|
||||
{
|
||||
if ($this->locked) {
|
||||
throw new BadMethodCallException('StepFlowBuilder methods cannot be accessed anymore once the builder is turned into a StepFlowConfigInterface instance.');
|
||||
}
|
||||
|
||||
$this->priority = $priority;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getSkip(): ?\Closure
|
||||
{
|
||||
return $this->skip;
|
||||
}
|
||||
|
||||
public function isSkipped(mixed $data): bool
|
||||
{
|
||||
if (null === $this->skip) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return ($this->skip)($data);
|
||||
}
|
||||
|
||||
public function setSkip(?\Closure $skip): static
|
||||
{
|
||||
if ($this->locked) {
|
||||
throw new BadMethodCallException('StepFlowBuilder methods cannot be accessed anymore once the builder is turned into a StepFlowConfigInterface instance.');
|
||||
}
|
||||
|
||||
$this->skip = $skip;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getStepConfig(): StepFlowConfigInterface
|
||||
{
|
||||
if ($this->locked) {
|
||||
throw new BadMethodCallException('StepFlowBuilder methods cannot be accessed anymore once the builder is turned into a StepFlowConfigInterface instance.');
|
||||
}
|
||||
|
||||
// This method should be idempotent, so clone the builder
|
||||
$config = clone $this;
|
||||
$config->locked = true;
|
||||
|
||||
return $config;
|
||||
}
|
||||
}
|
||||
48
Flow/StepFlowBuilderConfigInterface.php
Normal file
48
Flow/StepFlowBuilderConfigInterface.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* @author Yonel Ceruto <open@yceruto.dev>
|
||||
*/
|
||||
interface StepFlowBuilderConfigInterface extends StepFlowConfigInterface
|
||||
{
|
||||
/**
|
||||
* Returns the form type class name for the step.
|
||||
*/
|
||||
public function getType(): string;
|
||||
|
||||
/**
|
||||
* Returns the form options for the step.
|
||||
*/
|
||||
public function getOptions(): array;
|
||||
|
||||
/**
|
||||
* Returns the priority of the step.
|
||||
*/
|
||||
public function getPriority(): int;
|
||||
|
||||
/**
|
||||
* Sets the priority of the step.
|
||||
*/
|
||||
public function setPriority(int $priority): static;
|
||||
|
||||
/**
|
||||
* Sets the closure that determines if the step should be skipped.
|
||||
*/
|
||||
public function setSkip(?\Closure $skip): static;
|
||||
|
||||
/**
|
||||
* Returns a StepFlowConfigInterface instance for the step.
|
||||
*/
|
||||
public function getStepConfig(): StepFlowConfigInterface;
|
||||
}
|
||||
33
Flow/StepFlowConfigInterface.php
Normal file
33
Flow/StepFlowConfigInterface.php
Normal file
@@ -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;
|
||||
|
||||
/**
|
||||
* @author Yonel Ceruto <open@yceruto.dev>
|
||||
*/
|
||||
interface StepFlowConfigInterface
|
||||
{
|
||||
/**
|
||||
* Returns the name of the step.
|
||||
*/
|
||||
public function getName(): string;
|
||||
|
||||
/**
|
||||
* Returns the closure that determines if the step should be skipped.
|
||||
*/
|
||||
public function getSkip(): ?\Closure;
|
||||
|
||||
/**
|
||||
* Determines if the step should be skipped based on the provided data.
|
||||
*/
|
||||
public function isSkipped(mixed $data): bool;
|
||||
}
|
||||
65
Flow/Type/ButtonFlowType.php
Normal file
65
Flow/Type/ButtonFlowType.php
Normal file
@@ -0,0 +1,65 @@
|
||||
<?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\Type;
|
||||
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
||||
use Symfony\Component\Form\Flow\ButtonFlowTypeInterface;
|
||||
use Symfony\Component\Form\Flow\FormFlowCursor;
|
||||
use Symfony\Component\OptionsResolver\Options;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
/**
|
||||
* A submit button with a callable handler for a form flow.
|
||||
*
|
||||
* @author Yonel Ceruto <open@yceruto.dev>
|
||||
*/
|
||||
class ButtonFlowType extends AbstractType implements ButtonFlowTypeInterface
|
||||
{
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->define('handler')
|
||||
->info('The callable that will be called when this button is clicked')
|
||||
->required()
|
||||
->allowedTypes('callable');
|
||||
|
||||
$resolver->define('include_if')
|
||||
->info('Decide whether to include this button in the current form')
|
||||
->default(null)
|
||||
->allowedTypes('null', 'array', 'callable')
|
||||
->normalize(function (Options $options, mixed $value) {
|
||||
if (\is_array($value)) {
|
||||
return fn (FormFlowCursor $cursor): bool => \in_array($cursor->getCurrentStep(), $value, true);
|
||||
}
|
||||
|
||||
return $value;
|
||||
});
|
||||
|
||||
$resolver->define('clear_submission')
|
||||
->info('Whether the submitted data will be cleared when this button is clicked')
|
||||
->default(false)
|
||||
->allowedTypes('bool');
|
||||
|
||||
$resolver->setDefault('validate', function (Options $options) {
|
||||
return !$options['clear_submission'];
|
||||
});
|
||||
|
||||
$resolver->setDefault('validation_groups', function (Options $options) {
|
||||
return $options['clear_submission'] ? false : null;
|
||||
});
|
||||
}
|
||||
|
||||
public function getParent(): string
|
||||
{
|
||||
return SubmitType::class;
|
||||
}
|
||||
}
|
||||
35
Flow/Type/FinishFlowType.php
Normal file
35
Flow/Type/FinishFlowType.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?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\Type;
|
||||
|
||||
use Symfony\Component\Form\Flow\AbstractButtonFlowType;
|
||||
use Symfony\Component\Form\Flow\ButtonFlowInterface;
|
||||
use Symfony\Component\Form\Flow\FormFlowCursor;
|
||||
use Symfony\Component\Form\Flow\FormFlowInterface;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class FinishFlowType extends AbstractButtonFlowType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder->setAttribute('action', 'finish');
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'handler' => fn (mixed $data, ButtonFlowInterface $button, FormFlowInterface $flow) => $flow->reset(),
|
||||
'include_if' => fn (FormFlowCursor $cursor): bool => $cursor->isLastStep(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
129
Flow/Type/FormFlowType.php
Normal file
129
Flow/Type/FormFlowType.php
Normal file
@@ -0,0 +1,129 @@
|
||||
<?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\Type;
|
||||
|
||||
use Symfony\Component\Form\Extension\Core\Type\FormType;
|
||||
use Symfony\Component\Form\Flow\AbstractFlowType;
|
||||
use Symfony\Component\Form\Flow\ButtonFlowInterface;
|
||||
use Symfony\Component\Form\Flow\DataStorage\DataStorageInterface;
|
||||
use Symfony\Component\Form\Flow\DataStorage\NullDataStorage;
|
||||
use Symfony\Component\Form\Flow\FormFlowBuilderInterface;
|
||||
use Symfony\Component\Form\Flow\FormFlowInterface;
|
||||
use Symfony\Component\Form\Flow\StepAccessor\PropertyPathStepAccessor;
|
||||
use Symfony\Component\Form\Flow\StepAccessor\StepAccessorInterface;
|
||||
use Symfony\Component\Form\FormEvent;
|
||||
use Symfony\Component\Form\FormEvents;
|
||||
use Symfony\Component\Form\FormView;
|
||||
use Symfony\Component\OptionsResolver\Exception\MissingOptionsException;
|
||||
use Symfony\Component\OptionsResolver\Options;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
use Symfony\Component\PropertyAccess\PropertyAccess;
|
||||
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
|
||||
use Symfony\Component\PropertyAccess\PropertyPath;
|
||||
use Symfony\Component\PropertyAccess\PropertyPathInterface;
|
||||
|
||||
/**
|
||||
* A multistep form.
|
||||
*
|
||||
* @author Yonel Ceruto <open@yceruto.dev>
|
||||
*/
|
||||
class FormFlowType extends AbstractFlowType
|
||||
{
|
||||
public function __construct(
|
||||
private ?PropertyAccessorInterface $propertyAccessor = null,
|
||||
) {
|
||||
$this->propertyAccessor ??= PropertyAccess::createPropertyAccessor();
|
||||
}
|
||||
|
||||
public function buildFormFlow(FormFlowBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder->setDataStorage($options['data_storage'] ?? new NullDataStorage());
|
||||
$builder->setStepAccessor($options['step_accessor']);
|
||||
|
||||
$builder->addEventListener(FormEvents::PRE_SUBMIT, $this->onPreSubmit(...), -100);
|
||||
}
|
||||
|
||||
public function buildViewFlow(FormView $view, FormFlowInterface $form, array $options): void
|
||||
{
|
||||
$view->vars['cursor'] = $cursor = $form->getCursor();
|
||||
|
||||
$index = 0;
|
||||
$position = 1;
|
||||
foreach ($form->getConfig()->getSteps() as $name => $step) {
|
||||
$isSkipped = $step->isSkipped($form->getViewData());
|
||||
|
||||
$stepVars = [
|
||||
'name' => $name,
|
||||
'index' => $index++,
|
||||
'position' => $isSkipped ? -1 : $position++,
|
||||
'is_current_step' => $name === $cursor->getCurrentStep(),
|
||||
'can_be_skipped' => null !== $step->getSkip(),
|
||||
'is_skipped' => $isSkipped,
|
||||
];
|
||||
|
||||
$view->vars['steps'][$name] = $stepVars;
|
||||
|
||||
if (!$isSkipped) {
|
||||
$view->vars['visible_steps'][$name] = $stepVars;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->define('data_storage')
|
||||
->default(null)
|
||||
->allowedTypes('null', DataStorageInterface::class);
|
||||
|
||||
$resolver->define('step_accessor')
|
||||
->default(function (Options $options) {
|
||||
if (!isset($options['step_property_path'])) {
|
||||
throw new MissingOptionsException('Option "step_property_path" is required.');
|
||||
}
|
||||
|
||||
return new PropertyPathStepAccessor($this->propertyAccessor, $options['step_property_path']);
|
||||
})
|
||||
->allowedTypes(StepAccessorInterface::class);
|
||||
|
||||
$resolver->define('step_property_path')
|
||||
->info('Required if the default step_accessor is being used')
|
||||
->allowedTypes('string', PropertyPathInterface::class)
|
||||
->normalize(function (Options $options, string|PropertyPathInterface $value): PropertyPathInterface {
|
||||
return \is_string($value) ? new PropertyPath($value) : $value;
|
||||
});
|
||||
|
||||
$resolver->define('auto_reset')
|
||||
->info('Whether the FormFlow will be reset automatically when it is finished')
|
||||
->default(true)
|
||||
->allowedTypes('bool');
|
||||
|
||||
$resolver->setDefault('validation_groups', function (FormFlowInterface $flow) {
|
||||
return ['Default', $flow->getCursor()->getCurrentStep()];
|
||||
});
|
||||
}
|
||||
|
||||
public function getParent(): string
|
||||
{
|
||||
return FormType::class;
|
||||
}
|
||||
|
||||
public function onPreSubmit(FormEvent $event): void
|
||||
{
|
||||
/** @var FormFlowInterface $flow */
|
||||
$flow = $event->getForm();
|
||||
$button = $flow->getClickedButton();
|
||||
|
||||
if ($button instanceof ButtonFlowInterface && $button->isClearSubmission()) {
|
||||
$event->setData([]);
|
||||
}
|
||||
}
|
||||
}
|
||||
40
Flow/Type/NavigatorFlowType.php
Normal file
40
Flow/Type/NavigatorFlowType.php
Normal file
@@ -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\Type;
|
||||
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
/**
|
||||
* A navigator type that defines default buttons to interact with a form flow.
|
||||
*
|
||||
* @author Yonel Ceruto <open@yceruto.dev>
|
||||
*/
|
||||
class NavigatorFlowType extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder->add('previous', PreviousFlowType::class);
|
||||
$builder->add('next', NextFlowType::class);
|
||||
$builder->add('finish', FinishFlowType::class);
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'label' => false,
|
||||
'mapped' => false,
|
||||
'priority' => -100,
|
||||
]);
|
||||
}
|
||||
}
|
||||
35
Flow/Type/NextFlowType.php
Normal file
35
Flow/Type/NextFlowType.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?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\Type;
|
||||
|
||||
use Symfony\Component\Form\Flow\AbstractButtonFlowType;
|
||||
use Symfony\Component\Form\Flow\ButtonFlowInterface;
|
||||
use Symfony\Component\Form\Flow\FormFlowCursor;
|
||||
use Symfony\Component\Form\Flow\FormFlowInterface;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class NextFlowType extends AbstractButtonFlowType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder->setAttribute('action', 'next');
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'handler' => fn (mixed $data, ButtonFlowInterface $button, FormFlowInterface $flow) => $flow->moveNext(),
|
||||
'include_if' => fn (FormFlowCursor $cursor): bool => $cursor->canMoveNext(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
36
Flow/Type/PreviousFlowType.php
Normal file
36
Flow/Type/PreviousFlowType.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?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\Type;
|
||||
|
||||
use Symfony\Component\Form\Flow\AbstractButtonFlowType;
|
||||
use Symfony\Component\Form\Flow\ButtonFlowInterface;
|
||||
use Symfony\Component\Form\Flow\FormFlowCursor;
|
||||
use Symfony\Component\Form\Flow\FormFlowInterface;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class PreviousFlowType extends AbstractButtonFlowType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder->setAttribute('action', 'previous');
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'handler' => fn (mixed $data, ButtonFlowInterface $button, FormFlowInterface $flow) => $flow->movePrevious($button->getViewData()),
|
||||
'include_if' => fn (FormFlowCursor $cursor): bool => $cursor->canMoveBack(),
|
||||
'clear_submission' => true,
|
||||
]);
|
||||
}
|
||||
}
|
||||
34
Flow/Type/ResetFlowType.php
Normal file
34
Flow/Type/ResetFlowType.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?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\Type;
|
||||
|
||||
use Symfony\Component\Form\Flow\AbstractButtonFlowType;
|
||||
use Symfony\Component\Form\Flow\ButtonFlowInterface;
|
||||
use Symfony\Component\Form\Flow\FormFlowInterface;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class ResetFlowType extends AbstractButtonFlowType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder->setAttribute('action', 'reset');
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'handler' => fn (mixed $data, ButtonFlowInterface $button, FormFlowInterface $flow) => $flow->reset(),
|
||||
'clear_submission' => true,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,9 @@ namespace Symfony\Component\Form;
|
||||
|
||||
use Symfony\Component\Form\Extension\Core\Type\FormType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Form\Flow\FormFlowBuilderInterface;
|
||||
use Symfony\Component\Form\Flow\FormFlowInterface;
|
||||
use Symfony\Component\Form\Flow\FormFlowTypeInterface;
|
||||
|
||||
class FormFactory implements FormFactoryInterface
|
||||
{
|
||||
@@ -21,11 +24,17 @@ class FormFactory implements FormFactoryInterface
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ($type is class-string<FormFlowTypeInterface> ? FormFlowInterface : FormInterface)
|
||||
*/
|
||||
public function create(string $type = FormType::class, mixed $data = null, array $options = []): FormInterface
|
||||
{
|
||||
return $this->createBuilder($type, $data, $options)->getForm();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ($type is class-string<FormFlowTypeInterface> ? FormFlowInterface : FormInterface)
|
||||
*/
|
||||
public function createNamed(string $name, string $type = FormType::class, mixed $data = null, array $options = []): FormInterface
|
||||
{
|
||||
return $this->createNamedBuilder($name, $type, $data, $options)->getForm();
|
||||
@@ -36,11 +45,17 @@ class FormFactory implements FormFactoryInterface
|
||||
return $this->createBuilderForProperty($class, $property, $data, $options)->getForm();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ($type is class-string<FormFlowTypeInterface> ? FormFlowBuilderInterface : FormBuilderInterface)
|
||||
*/
|
||||
public function createBuilder(string $type = FormType::class, mixed $data = null, array $options = []): FormBuilderInterface
|
||||
{
|
||||
return $this->createNamedBuilder($this->registry->getType($type)->getBlockPrefix(), $type, $data, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ($type is class-string<FormFlowTypeInterface> ? FormFlowBuilderInterface : FormBuilderInterface)
|
||||
*/
|
||||
public function createNamedBuilder(string $name, string $type = FormType::class, mixed $data = null, array $options = []): FormBuilderInterface
|
||||
{
|
||||
if (null !== $data && !\array_key_exists('data', $options)) {
|
||||
@@ -51,6 +66,10 @@ class FormFactory implements FormFactoryInterface
|
||||
|
||||
$builder = $type->createBuilder($this, $name, $options);
|
||||
|
||||
if ($builder instanceof FormFlowBuilderInterface) {
|
||||
$builder->setInitialOptions($options);
|
||||
}
|
||||
|
||||
// Explicitly call buildForm() in order to be able to override either
|
||||
// createBuilder() or buildForm() in the resolved form type
|
||||
$type->buildForm($builder, $builder->getOptions());
|
||||
|
||||
@@ -12,6 +12,9 @@
|
||||
namespace Symfony\Component\Form;
|
||||
|
||||
use Symfony\Component\Form\Extension\Core\Type\FormType;
|
||||
use Symfony\Component\Form\Flow\FormFlowBuilderInterface;
|
||||
use Symfony\Component\Form\Flow\FormFlowInterface;
|
||||
use Symfony\Component\Form\Flow\FormFlowTypeInterface;
|
||||
use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException;
|
||||
|
||||
/**
|
||||
@@ -28,6 +31,8 @@ interface FormFactoryInterface
|
||||
*
|
||||
* @param mixed $data The initial data
|
||||
*
|
||||
* @return ($type is class-string<FormFlowTypeInterface> ? FormFlowInterface : FormInterface)
|
||||
*
|
||||
* @throws InvalidOptionsException if any given option is not applicable to the given type
|
||||
*/
|
||||
public function create(string $type = FormType::class, mixed $data = null, array $options = []): FormInterface;
|
||||
@@ -39,6 +44,8 @@ interface FormFactoryInterface
|
||||
*
|
||||
* @param mixed $data The initial data
|
||||
*
|
||||
* @return ($type is class-string<FormFlowTypeInterface> ? FormFlowInterface : FormInterface)
|
||||
*
|
||||
* @throws InvalidOptionsException if any given option is not applicable to the given type
|
||||
*/
|
||||
public function createNamed(string $name, string $type = FormType::class, mixed $data = null, array $options = []): FormInterface;
|
||||
@@ -61,6 +68,8 @@ interface FormFactoryInterface
|
||||
*
|
||||
* @param mixed $data The initial data
|
||||
*
|
||||
* @return ($type is class-string<FormFlowTypeInterface> ? FormFlowBuilderInterface : FormBuilderInterface)
|
||||
*
|
||||
* @throws InvalidOptionsException if any given option is not applicable to the given type
|
||||
*/
|
||||
public function createBuilder(string $type = FormType::class, mixed $data = null, array $options = []): FormBuilderInterface;
|
||||
@@ -70,6 +79,8 @@ interface FormFactoryInterface
|
||||
*
|
||||
* @param mixed $data The initial data
|
||||
*
|
||||
* @return ($type is class-string<FormFlowTypeInterface> ? FormFlowBuilderInterface : FormBuilderInterface)
|
||||
*
|
||||
* @throws InvalidOptionsException if any given option is not applicable to the given type
|
||||
*/
|
||||
public function createNamedBuilder(string $name, string $type = FormType::class, mixed $data = null, array $options = []): FormBuilderInterface;
|
||||
|
||||
@@ -13,6 +13,10 @@ namespace Symfony\Component\Form;
|
||||
|
||||
use Symfony\Component\EventDispatcher\EventDispatcher;
|
||||
use Symfony\Component\Form\Exception\UnexpectedTypeException;
|
||||
use Symfony\Component\Form\Flow\ButtonFlowBuilder;
|
||||
use Symfony\Component\Form\Flow\ButtonFlowTypeInterface;
|
||||
use Symfony\Component\Form\Flow\FormFlowBuilder;
|
||||
use Symfony\Component\Form\Flow\FormFlowTypeInterface;
|
||||
use Symfony\Component\OptionsResolver\Exception\ExceptionInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
@@ -156,6 +160,14 @@ class ResolvedFormType implements ResolvedFormTypeInterface
|
||||
return new SubmitButtonBuilder($name, $options);
|
||||
}
|
||||
|
||||
if ($this->innerType instanceof ButtonFlowTypeInterface) {
|
||||
return new ButtonFlowBuilder($name, $options);
|
||||
}
|
||||
|
||||
if ($this->innerType instanceof FormFlowTypeInterface) {
|
||||
return new FormFlowBuilder($name, $dataClass, new EventDispatcher(), $factory, $options);
|
||||
}
|
||||
|
||||
return new FormBuilder($name, $dataClass, new EventDispatcher(), $factory, $options);
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,9 @@ namespace Symfony\Component\Form\Test;
|
||||
|
||||
use Symfony\Component\Form\FormInterface as BaseFormInterface;
|
||||
|
||||
/**
|
||||
* @extends \Iterator<string, BaseFormInterface>
|
||||
*/
|
||||
interface FormInterface extends \Iterator, BaseFormInterface
|
||||
{
|
||||
}
|
||||
|
||||
40
Tests/Fixtures/Flow/Data/UserSignUp.php
Normal file
40
Tests/Fixtures/Flow/Data/UserSignUp.php
Normal file
@@ -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\Tests\Fixtures\Flow\Data;
|
||||
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
final class UserSignUp
|
||||
{
|
||||
// personal step
|
||||
#[Assert\NotBlank(groups: ['personal'])]
|
||||
#[Assert\Length(min: 3, groups: ['personal'])]
|
||||
public ?string $firstName = null;
|
||||
public ?string $lastName = null;
|
||||
public bool $worker = false;
|
||||
|
||||
// professional step
|
||||
#[Assert\NotBlank(groups: ['professional'])]
|
||||
#[Assert\Length(min: 3, groups: ['professional'])]
|
||||
public ?string $company = null;
|
||||
public ?string $role = null;
|
||||
|
||||
// account step
|
||||
#[Assert\NotBlank(groups: ['account'])]
|
||||
#[Assert\Email(groups: ['account'])]
|
||||
public ?string $email = null;
|
||||
#[Assert\NotBlank(groups: ['account'])]
|
||||
#[Assert\PasswordStrength(groups: ['account'])]
|
||||
public ?string $password = null;
|
||||
|
||||
public string $currentStep = '';
|
||||
}
|
||||
36
Tests/Fixtures/Flow/Extension/UserSignUpTypeExtension.php
Normal file
36
Tests/Fixtures/Flow/Extension/UserSignUpTypeExtension.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?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\Tests\Fixtures\Flow\Extension;
|
||||
|
||||
use Symfony\Component\Form\AbstractTypeExtension;
|
||||
use Symfony\Component\Form\Extension\Core\Type\FormType;
|
||||
use Symfony\Component\Form\Flow\FormFlowBuilderInterface;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\Form\Tests\Fixtures\Flow\UserSignUpType;
|
||||
|
||||
class UserSignUpTypeExtension extends AbstractTypeExtension
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
if (!$builder instanceof FormFlowBuilderInterface) {
|
||||
throw new \InvalidArgumentException(\sprintf('The "%s" can only be used with FormFlowType.', self::class));
|
||||
}
|
||||
|
||||
$builder->addStep('first', FormType::class, ['mapped' => false], priority: 1);
|
||||
$builder->addStep('last', FormType::class, ['mapped' => false]);
|
||||
}
|
||||
|
||||
public static function getExtendedTypes(): iterable
|
||||
{
|
||||
return [UserSignUpType::class];
|
||||
}
|
||||
}
|
||||
34
Tests/Fixtures/Flow/Step/UserSignUpAccountType.php
Normal file
34
Tests/Fixtures/Flow/Step/UserSignUpAccountType.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?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\Tests\Fixtures\Flow\Step;
|
||||
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\EmailType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class UserSignUpAccountType extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder->add('email', EmailType::class);
|
||||
$builder->add('password', PasswordType::class);
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'inherit_data' => true,
|
||||
]);
|
||||
}
|
||||
}
|
||||
35
Tests/Fixtures/Flow/Step/UserSignUpPersonalType.php
Normal file
35
Tests/Fixtures/Flow/Step/UserSignUpPersonalType.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?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\Tests\Fixtures\Flow\Step;
|
||||
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class UserSignUpPersonalType extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder->add('firstName', TextType::class);
|
||||
$builder->add('lastName', TextType::class);
|
||||
$builder->add('worker', CheckboxType::class, ['required' => false]);
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'inherit_data' => true,
|
||||
]);
|
||||
}
|
||||
}
|
||||
39
Tests/Fixtures/Flow/Step/UserSignUpProfessionalType.php
Normal file
39
Tests/Fixtures/Flow/Step/UserSignUpProfessionalType.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?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\Tests\Fixtures\Flow\Step;
|
||||
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class UserSignUpProfessionalType extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder->add('company');
|
||||
$builder->add('role', ChoiceType::class, [
|
||||
'choices' => [
|
||||
'Product Manager' => 'ROLE_MANAGER',
|
||||
'Developer' => 'ROLE_DEVELOPER',
|
||||
'Designer' => 'ROLE_DESIGNER',
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'inherit_data' => true,
|
||||
]);
|
||||
}
|
||||
}
|
||||
36
Tests/Fixtures/Flow/UserSignUpNavigatorType.php
Normal file
36
Tests/Fixtures/Flow/UserSignUpNavigatorType.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?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\Tests\Fixtures\Flow;
|
||||
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Flow\Type\NavigatorFlowType;
|
||||
use Symfony\Component\Form\Flow\Type\NextFlowType;
|
||||
use Symfony\Component\Form\Flow\Type\ResetFlowType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
|
||||
class UserSignUpNavigatorType extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder->add('skip', NextFlowType::class, [
|
||||
'clear_submission' => true,
|
||||
'include_if' => ['professional'],
|
||||
]);
|
||||
|
||||
$builder->add('reset', ResetFlowType::class);
|
||||
}
|
||||
|
||||
public function getParent(): string
|
||||
{
|
||||
return NavigatorFlowType::class;
|
||||
}
|
||||
}
|
||||
46
Tests/Fixtures/Flow/UserSignUpType.php
Normal file
46
Tests/Fixtures/Flow/UserSignUpType.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?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\Tests\Fixtures\Flow;
|
||||
|
||||
use Symfony\Component\Form\Flow\AbstractFlowType;
|
||||
use Symfony\Component\Form\Flow\DataStorage\InMemoryDataStorage;
|
||||
use Symfony\Component\Form\Flow\FormFlowBuilderInterface;
|
||||
use Symfony\Component\Form\Tests\Fixtures\Flow\Data\UserSignUp;
|
||||
use Symfony\Component\Form\Tests\Fixtures\Flow\Step\UserSignUpAccountType;
|
||||
use Symfony\Component\Form\Tests\Fixtures\Flow\Step\UserSignUpPersonalType;
|
||||
use Symfony\Component\Form\Tests\Fixtures\Flow\Step\UserSignUpProfessionalType;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class UserSignUpType extends AbstractFlowType
|
||||
{
|
||||
public function buildFormFlow(FormFlowBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$skip = $options['data_class']
|
||||
? static fn (UserSignUp $data) => !$data->worker
|
||||
: static fn (array $data) => !$data['worker'];
|
||||
|
||||
$builder->addStep('personal', UserSignUpPersonalType::class);
|
||||
$builder->addStep('professional', UserSignUpProfessionalType::class, skip: $skip);
|
||||
$builder->addStep('account', UserSignUpAccountType::class);
|
||||
|
||||
$builder->add('navigator', UserSignUpNavigatorType::class);
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => UserSignUp::class,
|
||||
'data_storage' => new InMemoryDataStorage('user_sign_up'),
|
||||
'step_property_path' => 'currentStep',
|
||||
]);
|
||||
}
|
||||
}
|
||||
92
Tests/Flow/FormFlowBuilderTest.php
Normal file
92
Tests/Flow/FormFlowBuilderTest.php
Normal file
@@ -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\Tests\Flow;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcher;
|
||||
use Symfony\Component\Form\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\Form\Exception\LogicException;
|
||||
use Symfony\Component\Form\Flow\DataStorage\InMemoryDataStorage;
|
||||
use Symfony\Component\Form\Flow\FormFlowBuilder;
|
||||
use Symfony\Component\Form\Flow\StepAccessor\PropertyPathStepAccessor;
|
||||
use Symfony\Component\Form\FormFactoryInterface;
|
||||
use Symfony\Component\Form\Forms;
|
||||
use Symfony\Component\PropertyAccess\PropertyAccess;
|
||||
use Symfony\Component\PropertyAccess\PropertyPath;
|
||||
|
||||
class FormFlowBuilderTest extends TestCase
|
||||
{
|
||||
private FormFactoryInterface $factory;
|
||||
private InMemoryDataStorage $dataStorage;
|
||||
private PropertyPathStepAccessor $stepAccessor;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->factory = Forms::createFormFactoryBuilder()->getFormFactory();
|
||||
$this->dataStorage = new InMemoryDataStorage('key');
|
||||
$this->stepAccessor = new PropertyPathStepAccessor(PropertyAccess::createPropertyAccessor(), new PropertyPath('[currentStep]'));
|
||||
}
|
||||
|
||||
public function testNoStepsConfigured()
|
||||
{
|
||||
$builder = new FormFlowBuilder('test', null, new EventDispatcher(), $this->factory);
|
||||
$builder->setData([]);
|
||||
$builder->setDataStorage($this->dataStorage);
|
||||
$builder->setStepAccessor($this->stepAccessor);
|
||||
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
$this->expectExceptionMessage('Steps not configured.');
|
||||
|
||||
$builder->getForm();
|
||||
}
|
||||
|
||||
public function testRemoveAllStepsDynamically()
|
||||
{
|
||||
$builder = new FormFlowBuilder('test', null, new EventDispatcher(), $this->factory);
|
||||
$builder->setData([]);
|
||||
$builder->setDataStorage($this->dataStorage);
|
||||
$builder->setStepAccessor($this->stepAccessor);
|
||||
$builder->addStep('step1');
|
||||
|
||||
// In a type extension context
|
||||
$builder->removeStep('step1');
|
||||
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
$this->expectExceptionMessage('Steps not configured.');
|
||||
|
||||
$builder->getForm();
|
||||
}
|
||||
|
||||
public function testNestedFormFlowException()
|
||||
{
|
||||
// Create parent form flow builder
|
||||
$builder = new FormFlowBuilder('parent', null, new EventDispatcher(), $this->factory);
|
||||
$builder->setData([]);
|
||||
$builder->setDataStorage($this->dataStorage);
|
||||
$builder->setStepAccessor($this->stepAccessor);
|
||||
$builder->addStep('step1');
|
||||
|
||||
// Create child form flow builder
|
||||
$childBuilder = new FormFlowBuilder('child', null, new EventDispatcher(), $this->factory);
|
||||
$childBuilder->setDataStorage(new InMemoryDataStorage('child_key'));
|
||||
$childBuilder->setStepAccessor($this->stepAccessor);
|
||||
$childBuilder->addStep('child_step1');
|
||||
|
||||
// Add child form flow to parent
|
||||
$builder->add($childBuilder);
|
||||
|
||||
$this->expectException(LogicException::class);
|
||||
$this->expectExceptionMessage('Nested form flows is not currently supported.');
|
||||
|
||||
$builder->getForm();
|
||||
}
|
||||
}
|
||||
202
Tests/Flow/FormFlowCursorTest.php
Normal file
202
Tests/Flow/FormFlowCursorTest.php
Normal file
@@ -0,0 +1,202 @@
|
||||
<?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\Tests\Flow;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Form\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\Form\Flow\FormFlowCursor;
|
||||
|
||||
class FormFlowCursorTest extends TestCase
|
||||
{
|
||||
private static array $steps = ['personal', 'professional', 'account'];
|
||||
|
||||
public function testConstructorWithValidStep()
|
||||
{
|
||||
$cursor = new FormFlowCursor(self::$steps, 'personal');
|
||||
|
||||
$this->assertSame(self::$steps, $cursor->getSteps());
|
||||
$this->assertSame('personal', $cursor->getCurrentStep());
|
||||
}
|
||||
|
||||
public function testConstructorWithInvalidStep()
|
||||
{
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
$this->expectExceptionMessage('Step "invalid" does not exist. Available steps are: "personal", "professional", "account".');
|
||||
|
||||
new FormFlowCursor(self::$steps, 'invalid');
|
||||
}
|
||||
|
||||
public function testGetSteps()
|
||||
{
|
||||
$cursor = new FormFlowCursor(self::$steps, 'personal');
|
||||
|
||||
$this->assertSame(self::$steps, $cursor->getSteps());
|
||||
}
|
||||
|
||||
public function testGetTotalSteps()
|
||||
{
|
||||
$cursor = new FormFlowCursor(self::$steps, 'personal');
|
||||
|
||||
$this->assertSame(3, $cursor->getTotalSteps());
|
||||
}
|
||||
|
||||
public function testGetStepIndex()
|
||||
{
|
||||
$cursor = new FormFlowCursor(self::$steps, 'personal');
|
||||
$this->assertSame(0, $cursor->getStepIndex());
|
||||
|
||||
$cursor = new FormFlowCursor(self::$steps, 'professional');
|
||||
$this->assertSame(1, $cursor->getStepIndex());
|
||||
|
||||
$cursor = new FormFlowCursor(self::$steps, 'account');
|
||||
$this->assertSame(2, $cursor->getStepIndex());
|
||||
}
|
||||
|
||||
public function testGetFirstStep()
|
||||
{
|
||||
$cursor = new FormFlowCursor(self::$steps, 'professional');
|
||||
|
||||
$this->assertSame('personal', $cursor->getFirstStep());
|
||||
}
|
||||
|
||||
public function testGetPrevStep()
|
||||
{
|
||||
// First step has no previous step
|
||||
$cursor = new FormFlowCursor(self::$steps, 'personal');
|
||||
$this->assertNull($cursor->getPreviousStep());
|
||||
|
||||
// Middle step has previous step
|
||||
$cursor = new FormFlowCursor(self::$steps, 'professional');
|
||||
$this->assertSame('personal', $cursor->getPreviousStep());
|
||||
|
||||
// Last step has previous step
|
||||
$cursor = new FormFlowCursor(self::$steps, 'account');
|
||||
$this->assertSame('professional', $cursor->getPreviousStep());
|
||||
}
|
||||
|
||||
public function testGetCurrentStep()
|
||||
{
|
||||
$cursor = new FormFlowCursor(self::$steps, 'professional');
|
||||
|
||||
$this->assertSame('professional', $cursor->getCurrentStep());
|
||||
}
|
||||
|
||||
public function testWithCurrentStep()
|
||||
{
|
||||
$cursor = new FormFlowCursor(self::$steps, 'personal');
|
||||
|
||||
$newCursor = $cursor->withCurrentStep('professional');
|
||||
|
||||
// Original cursor should remain unchanged
|
||||
$this->assertSame('personal', $cursor->getCurrentStep());
|
||||
|
||||
// New cursor should have the new current step
|
||||
$this->assertSame('professional', $newCursor->getCurrentStep());
|
||||
|
||||
// Both cursors should have the same steps
|
||||
$this->assertSame(self::$steps, $cursor->getSteps());
|
||||
$this->assertSame(self::$steps, $newCursor->getSteps());
|
||||
}
|
||||
|
||||
public function testGetNextStep()
|
||||
{
|
||||
// First step has next step
|
||||
$cursor = new FormFlowCursor(self::$steps, 'personal');
|
||||
$this->assertSame('professional', $cursor->getNextStep());
|
||||
|
||||
// Middle step has next step
|
||||
$cursor = new FormFlowCursor(self::$steps, 'professional');
|
||||
$this->assertSame('account', $cursor->getNextStep());
|
||||
|
||||
// Last step has no next step
|
||||
$cursor = new FormFlowCursor(self::$steps, 'account');
|
||||
$this->assertNull($cursor->getNextStep());
|
||||
}
|
||||
|
||||
public function testGetLastStep()
|
||||
{
|
||||
$cursor = new FormFlowCursor(self::$steps, 'personal');
|
||||
|
||||
$this->assertSame('account', $cursor->getLastStep());
|
||||
}
|
||||
|
||||
public function testIsFirstStep()
|
||||
{
|
||||
// First step
|
||||
$cursor = new FormFlowCursor(self::$steps, 'personal');
|
||||
$this->assertTrue($cursor->isFirstStep());
|
||||
|
||||
// Not first step
|
||||
$cursor = new FormFlowCursor(self::$steps, 'professional');
|
||||
$this->assertFalse($cursor->isFirstStep());
|
||||
}
|
||||
|
||||
public function testIsLastStep()
|
||||
{
|
||||
// Not last step
|
||||
$cursor = new FormFlowCursor(self::$steps, 'personal');
|
||||
$this->assertFalse($cursor->isLastStep());
|
||||
|
||||
// Last step
|
||||
$cursor = new FormFlowCursor(self::$steps, 'account');
|
||||
$this->assertTrue($cursor->isLastStep());
|
||||
}
|
||||
|
||||
public function testCanMovePreviousStep()
|
||||
{
|
||||
// First position cannot move a previous step
|
||||
$cursor = new FormFlowCursor(self::$steps, 'personal');
|
||||
$this->assertFalse($cursor->canMoveBack());
|
||||
|
||||
// Middle position can move a previous step
|
||||
$cursor = new FormFlowCursor(self::$steps, 'professional');
|
||||
$this->assertTrue($cursor->canMoveBack());
|
||||
|
||||
// Last step can move a previous step
|
||||
$cursor = new FormFlowCursor(self::$steps, 'account');
|
||||
$this->assertTrue($cursor->canMoveBack());
|
||||
}
|
||||
|
||||
public function testCanMoveNext()
|
||||
{
|
||||
// First position can move next step
|
||||
$cursor = new FormFlowCursor(self::$steps, 'personal');
|
||||
$this->assertTrue($cursor->canMoveNext());
|
||||
|
||||
// Middle position can move next step
|
||||
$cursor = new FormFlowCursor(self::$steps, 'professional');
|
||||
$this->assertTrue($cursor->canMoveNext());
|
||||
|
||||
// Last position cannot move the next step
|
||||
$cursor = new FormFlowCursor(self::$steps, 'account');
|
||||
$this->assertFalse($cursor->canMoveNext());
|
||||
}
|
||||
|
||||
public function testCursorWithSingleStep()
|
||||
{
|
||||
$steps = ['single'];
|
||||
$cursor = new FormFlowCursor($steps, 'single');
|
||||
|
||||
$this->assertSame('single', $cursor->getCurrentStep());
|
||||
$this->assertTrue($cursor->isFirstStep());
|
||||
$this->assertTrue($cursor->isLastStep());
|
||||
$this->assertSame('single', $cursor->getFirstStep());
|
||||
$this->assertNull($cursor->getPreviousStep());
|
||||
$this->assertNull($cursor->getNextStep());
|
||||
$this->assertSame('single', $cursor->getLastStep());
|
||||
$this->assertSame(['single'], $cursor->getSteps());
|
||||
$this->assertSame(0, $cursor->getStepIndex());
|
||||
$this->assertSame(1, $cursor->getTotalSteps());
|
||||
$this->assertFalse($cursor->canMoveBack());
|
||||
$this->assertFalse($cursor->canMoveNext());
|
||||
}
|
||||
}
|
||||
935
Tests/Flow/FormFlowTest.php
Normal file
935
Tests/Flow/FormFlowTest.php
Normal file
@@ -0,0 +1,935 @@
|
||||
<?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\Tests\Flow;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Form\Exception\BadMethodCallException;
|
||||
use Symfony\Component\Form\Exception\RuntimeException;
|
||||
use Symfony\Component\Form\Extension\Validator\ValidatorExtension;
|
||||
use Symfony\Component\Form\Flow\ButtonFlowInterface;
|
||||
use Symfony\Component\Form\Flow\DataStorage\InMemoryDataStorage;
|
||||
use Symfony\Component\Form\Flow\FormFlowCursor;
|
||||
use Symfony\Component\Form\Flow\FormFlowInterface;
|
||||
use Symfony\Component\Form\Flow\Type\NextFlowType;
|
||||
use Symfony\Component\Form\Flow\Type\PreviousFlowType;
|
||||
use Symfony\Component\Form\FormError;
|
||||
use Symfony\Component\Form\FormFactoryInterface;
|
||||
use Symfony\Component\Form\Forms;
|
||||
use Symfony\Component\Form\Tests\Fixtures\Flow\Data\UserSignUp;
|
||||
use Symfony\Component\Form\Tests\Fixtures\Flow\Extension\UserSignUpTypeExtension;
|
||||
use Symfony\Component\Form\Tests\Fixtures\Flow\UserSignUpType;
|
||||
use Symfony\Component\Validator\Mapping\Factory\LazyLoadingMetadataFactory;
|
||||
use Symfony\Component\Validator\Mapping\Loader\AttributeLoader;
|
||||
use Symfony\Component\Validator\Validation;
|
||||
|
||||
class FormFlowTest extends TestCase
|
||||
{
|
||||
private FormFactoryInterface $factory;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$validator = Validation::createValidatorBuilder()
|
||||
->setMetadataFactory(new LazyLoadingMetadataFactory(new AttributeLoader()))
|
||||
->getValidator();
|
||||
|
||||
$this->factory = Forms::createFormFactoryBuilder()
|
||||
->addExtensions([new ValidatorExtension($validator)])
|
||||
->getFormFactory();
|
||||
}
|
||||
|
||||
public function testFlowConfig()
|
||||
{
|
||||
$flow = $this->factory->create(UserSignUpType::class, new UserSignUp());
|
||||
$config = $flow->getConfig();
|
||||
|
||||
self::assertInstanceOf(UserSignUp::class, $data = $config->getData());
|
||||
self::assertEquals(['data' => $data], $config->getInitialOptions());
|
||||
self::assertCount(3, $config->getSteps());
|
||||
self::assertTrue($config->hasStep('personal'));
|
||||
self::assertTrue($config->hasStep('professional'));
|
||||
self::assertTrue($config->hasStep('account'));
|
||||
}
|
||||
|
||||
public function testFlowCursor()
|
||||
{
|
||||
$flow = $this->factory->create(UserSignUpType::class, new UserSignUp());
|
||||
$cursor = $flow->getCursor();
|
||||
|
||||
self::assertSame('personal', $cursor->getCurrentStep());
|
||||
self::assertTrue($cursor->isFirstStep());
|
||||
self::assertFalse($cursor->isLastStep());
|
||||
self::assertSame('personal', $cursor->getFirstStep());
|
||||
self::assertNull($cursor->getPreviousStep());
|
||||
self::assertSame('professional', $cursor->getNextStep());
|
||||
self::assertSame('account', $cursor->getLastStep());
|
||||
self::assertSame(['personal', 'professional', 'account'], $cursor->getSteps());
|
||||
self::assertSame(0, $cursor->getStepIndex());
|
||||
self::assertSame(3, $cursor->getTotalSteps());
|
||||
self::assertFalse($cursor->canMoveBack());
|
||||
self::assertTrue($cursor->canMoveNext());
|
||||
|
||||
$cursor = $cursor->withCurrentStep('professional');
|
||||
|
||||
self::assertSame('professional', $cursor->getCurrentStep());
|
||||
self::assertFalse($cursor->isFirstStep());
|
||||
self::assertFalse($cursor->isLastStep());
|
||||
self::assertSame('personal', $cursor->getFirstStep());
|
||||
self::assertSame('personal', $cursor->getPreviousStep());
|
||||
self::assertSame('account', $cursor->getNextStep());
|
||||
self::assertSame('account', $cursor->getLastStep());
|
||||
self::assertSame(1, $cursor->getStepIndex());
|
||||
self::assertSame(3, $cursor->getTotalSteps());
|
||||
self::assertTrue($cursor->canMoveBack());
|
||||
self::assertTrue($cursor->canMoveNext());
|
||||
|
||||
$cursor = $cursor->withCurrentStep('account');
|
||||
|
||||
self::assertSame('account', $cursor->getCurrentStep());
|
||||
self::assertFalse($cursor->isFirstStep());
|
||||
self::assertTrue($cursor->isLastStep());
|
||||
self::assertSame('personal', $cursor->getFirstStep());
|
||||
self::assertSame('professional', $cursor->getPreviousStep());
|
||||
self::assertNull($cursor->getNextStep());
|
||||
self::assertSame('account', $cursor->getLastStep());
|
||||
self::assertSame(2, $cursor->getStepIndex());
|
||||
self::assertSame(3, $cursor->getTotalSteps());
|
||||
self::assertTrue($cursor->canMoveBack());
|
||||
self::assertFalse($cursor->canMoveNext());
|
||||
}
|
||||
|
||||
public function testFlowViewVars()
|
||||
{
|
||||
$view = $this->factory->create(UserSignUpType::class, new UserSignUp())
|
||||
->createView();
|
||||
|
||||
self::assertArrayHasKey('steps', $view->vars);
|
||||
self::assertArrayHasKey('visible_steps', $view->vars);
|
||||
|
||||
self::assertCount(3, $view->vars['steps']);
|
||||
self::assertCount(2, $view->vars['visible_steps']);
|
||||
|
||||
self::assertArrayHasKey('personal', $view->vars['steps']);
|
||||
self::assertArrayHasKey('professional', $view->vars['steps']);
|
||||
self::assertArrayHasKey('account', $view->vars['steps']);
|
||||
self::assertArrayHasKey('personal', $view->vars['visible_steps']);
|
||||
self::assertArrayHasKey('account', $view->vars['visible_steps']);
|
||||
|
||||
$step1 = [
|
||||
'name' => 'personal',
|
||||
'index' => 0,
|
||||
'position' => 1,
|
||||
'is_current_step' => true,
|
||||
'can_be_skipped' => false,
|
||||
'is_skipped' => false,
|
||||
];
|
||||
$step2 = [
|
||||
'name' => 'professional',
|
||||
'index' => 1,
|
||||
'position' => -1,
|
||||
'is_current_step' => false,
|
||||
'can_be_skipped' => true,
|
||||
'is_skipped' => true,
|
||||
];
|
||||
$step3 = [
|
||||
'name' => 'account',
|
||||
'index' => 2,
|
||||
'position' => 2,
|
||||
'is_current_step' => false,
|
||||
'can_be_skipped' => false,
|
||||
'is_skipped' => false,
|
||||
];
|
||||
|
||||
self::assertSame($step1, $view->vars['steps']['personal']);
|
||||
self::assertSame($step2, $view->vars['steps']['professional']);
|
||||
self::assertSame($step3, $view->vars['steps']['account']);
|
||||
self::assertSame($step1, $view->vars['visible_steps']['personal']);
|
||||
self::assertSame($step3, $view->vars['visible_steps']['account']);
|
||||
}
|
||||
|
||||
public function testWholeStepsFlow()
|
||||
{
|
||||
$data = new UserSignUp();
|
||||
$flow = $this->factory->create(UserSignUpType::class, $data);
|
||||
|
||||
self::assertSame('personal', $flow->getCursor()->getCurrentStep());
|
||||
self::assertFalse($flow->isSubmitted());
|
||||
self::assertNull($flow->getClickedButton());
|
||||
self::assertTrue($flow->has('personal'));
|
||||
self::assertTrue($flow->has('navigator'));
|
||||
|
||||
$stepForm = $flow->get('personal');
|
||||
self::assertCount(3, $stepForm->all());
|
||||
self::assertTrue($stepForm->has('firstName'));
|
||||
self::assertTrue($stepForm->has('lastName'));
|
||||
self::assertTrue($stepForm->has('worker'));
|
||||
|
||||
$navigatorForm = $flow->get('navigator');
|
||||
self::assertCount(2, $navigatorForm->all());
|
||||
self::assertTrue($navigatorForm->has('reset'));
|
||||
self::assertTrue($navigatorForm->has('next'));
|
||||
|
||||
$flow->submit([
|
||||
'personal' => [
|
||||
'firstName' => 'John',
|
||||
'lastName' => 'Doe',
|
||||
'worker' => '1',
|
||||
],
|
||||
'navigator' => [
|
||||
'next' => '',
|
||||
],
|
||||
]);
|
||||
|
||||
self::assertSame('personal', $flow->getCursor()->getCurrentStep());
|
||||
self::assertTrue($flow->isSubmitted());
|
||||
self::assertTrue($flow->isValid());
|
||||
self::assertFalse($flow->isFinished());
|
||||
self::assertNotNull($button = $flow->getClickedButton());
|
||||
self::assertTrue($button->isNextAction());
|
||||
self::assertTrue($button->isClicked());
|
||||
|
||||
$flow = $flow->getStepForm();
|
||||
|
||||
self::assertSame('professional', $data->currentStep);
|
||||
self::assertSame('professional', $flow->getCursor()->getCurrentStep());
|
||||
self::assertFalse($flow->isSubmitted());
|
||||
self::assertNull($flow->getClickedButton());
|
||||
self::assertTrue($flow->has('professional'));
|
||||
self::assertTrue($flow->has('navigator'));
|
||||
|
||||
$stepForm = $flow->get('professional');
|
||||
self::assertCount(2, $stepForm->all());
|
||||
self::assertTrue($stepForm->has('company'));
|
||||
self::assertTrue($stepForm->has('role'));
|
||||
|
||||
$navigatorForm = $flow->get('navigator');
|
||||
self::assertCount(4, $navigatorForm->all());
|
||||
self::assertTrue($navigatorForm->has('reset'));
|
||||
self::assertTrue($navigatorForm->has('previous'));
|
||||
self::assertTrue($navigatorForm->has('skip'));
|
||||
self::assertTrue($navigatorForm->has('next'));
|
||||
|
||||
$flow->submit([
|
||||
'professional' => [
|
||||
'company' => 'Acme',
|
||||
'role' => 'ROLE_DEVELOPER',
|
||||
],
|
||||
'navigator' => [
|
||||
'next' => '',
|
||||
],
|
||||
]);
|
||||
|
||||
self::assertSame('professional', $flow->getCursor()->getCurrentStep());
|
||||
self::assertTrue($flow->isSubmitted());
|
||||
self::assertTrue($flow->isValid());
|
||||
self::assertFalse($flow->isFinished());
|
||||
self::assertNotNull($button = $flow->getClickedButton());
|
||||
self::assertTrue($button->isNextAction());
|
||||
self::assertTrue($button->isClicked());
|
||||
|
||||
$flow = $flow->getStepForm();
|
||||
|
||||
/** @var UserSignUp $data */
|
||||
$data = $flow->getViewData();
|
||||
self::assertSame('account', $data->currentStep);
|
||||
self::assertSame('account', $flow->getCursor()->getCurrentStep());
|
||||
self::assertFalse($flow->isSubmitted());
|
||||
self::assertNull($flow->getClickedButton());
|
||||
self::assertTrue($flow->has('account'));
|
||||
self::assertTrue($flow->has('navigator'));
|
||||
|
||||
$stepForm = $flow->get('account');
|
||||
self::assertCount(2, $stepForm->all());
|
||||
self::assertTrue($stepForm->has('email'));
|
||||
self::assertTrue($stepForm->has('password'));
|
||||
|
||||
$navigatorForm = $flow->get('navigator');
|
||||
self::assertCount(3, $navigatorForm->all());
|
||||
self::assertTrue($navigatorForm->has('reset'));
|
||||
self::assertTrue($navigatorForm->has('previous'));
|
||||
self::assertTrue($navigatorForm->has('finish'));
|
||||
|
||||
$flow->submit([
|
||||
'account' => [
|
||||
'email' => 'john@acme.com',
|
||||
'password' => 'eBvU2vBLfSXqf36',
|
||||
],
|
||||
'navigator' => [
|
||||
'finish' => '',
|
||||
],
|
||||
]);
|
||||
|
||||
self::assertSame('account', $flow->getCursor()->getCurrentStep());
|
||||
self::assertTrue($flow->isSubmitted());
|
||||
self::assertTrue($flow->isValid());
|
||||
self::assertTrue($flow->isFinished());
|
||||
self::assertNotNull($button = $flow->getClickedButton());
|
||||
self::assertTrue($button->isFinishAction());
|
||||
self::assertTrue($button->isClicked());
|
||||
|
||||
self::assertSame($data, $flow->getViewData());
|
||||
self::assertSame('John', $data->firstName);
|
||||
self::assertSame('Doe', $data->lastName);
|
||||
self::assertTrue($data->worker);
|
||||
self::assertSame('Acme', $data->company);
|
||||
self::assertSame('ROLE_DEVELOPER', $data->role);
|
||||
self::assertSame('john@acme.com', $data->email);
|
||||
self::assertSame('eBvU2vBLfSXqf36', $data->password);
|
||||
}
|
||||
|
||||
public function testPreviousActionWithPurgeSubmission()
|
||||
{
|
||||
$data = new UserSignUp();
|
||||
$data->firstName = 'John';
|
||||
$data->lastName = 'Doe';
|
||||
$data->worker = true;
|
||||
$data->currentStep = 'professional';
|
||||
$flow = $this->factory->create(UserSignUpType::class, $data);
|
||||
|
||||
self::assertSame('professional', $flow->getCursor()->getCurrentStep());
|
||||
self::assertTrue($flow->has('professional'));
|
||||
|
||||
$flow->submit([
|
||||
'professional' => [
|
||||
'company' => 'Acme',
|
||||
'role' => 'ROLE_DEVELOPER',
|
||||
],
|
||||
'navigator' => [
|
||||
'previous' => '',
|
||||
],
|
||||
]);
|
||||
|
||||
self::assertTrue($flow->isSubmitted());
|
||||
self::assertTrue($flow->isValid());
|
||||
self::assertFalse($flow->isFinished());
|
||||
self::assertNotNull($button = $flow->getClickedButton());
|
||||
self::assertTrue($button->isPreviousAction());
|
||||
|
||||
$flow = $flow->getStepForm();
|
||||
|
||||
self::assertSame('personal', $flow->getCursor()->getCurrentStep());
|
||||
self::assertTrue($flow->has('personal'), 'back action should move the flow one step back');
|
||||
self::assertNull($data->company, 'pro step should be silenced on submit');
|
||||
self::assertNull($data->role, 'pro step should be silenced on submit');
|
||||
}
|
||||
|
||||
public function testPreviousActionWithoutPurgeSubmission()
|
||||
{
|
||||
$data = new UserSignUp();
|
||||
$data->firstName = 'John';
|
||||
$data->lastName = 'Doe';
|
||||
$data->worker = true;
|
||||
$data->currentStep = 'professional';
|
||||
|
||||
$flow = $this->factory->create(UserSignUpType::class, $data);
|
||||
// previous action without purge submission
|
||||
$flow->get('navigator')->add('previous', PreviousFlowType::class, [
|
||||
'validate' => false,
|
||||
'validation_groups' => false,
|
||||
'clear_submission' => false,
|
||||
'include_if' => fn (FormFlowCursor $cursor) => $cursor->canMoveBack(),
|
||||
]);
|
||||
|
||||
self::assertSame('professional', $flow->getCursor()->getCurrentStep());
|
||||
self::assertTrue($flow->has('professional'));
|
||||
|
||||
$flow->submit([
|
||||
'professional' => [
|
||||
'company' => 'Acme',
|
||||
'role' => 'ROLE_DEVELOPER',
|
||||
],
|
||||
'navigator' => [
|
||||
'previous' => '',
|
||||
],
|
||||
]);
|
||||
|
||||
self::assertTrue($flow->isSubmitted());
|
||||
self::assertTrue($flow->isValid());
|
||||
self::assertFalse($flow->isFinished());
|
||||
self::assertNotNull($button = $flow->getClickedButton());
|
||||
self::assertTrue($button->isPreviousAction());
|
||||
|
||||
$flow = $flow->getStepForm();
|
||||
|
||||
self::assertSame('personal', $flow->getCursor()->getCurrentStep());
|
||||
self::assertTrue($flow->has('personal'), 'previous action should move the flow one step back');
|
||||
self::assertSame('Acme', $data->company, 'pro step should NOT be silenced on submit');
|
||||
self::assertSame('ROLE_DEVELOPER', $data->role, 'pro step should NOT be silenced on submit');
|
||||
}
|
||||
|
||||
public function testSkipStepBasedOnData()
|
||||
{
|
||||
$flow = $this->factory->create(UserSignUpType::class, new UserSignUp());
|
||||
|
||||
self::assertSame('personal', $flow->getCursor()->getCurrentStep());
|
||||
self::assertTrue($flow->has('personal'));
|
||||
|
||||
$flow->submit([
|
||||
'personal' => [
|
||||
'firstName' => 'John',
|
||||
'lastName' => 'Doe',
|
||||
// worker checkbox was not clicked
|
||||
],
|
||||
'navigator' => [
|
||||
'next' => '',
|
||||
],
|
||||
]);
|
||||
|
||||
self::assertTrue($flow->isSubmitted());
|
||||
self::assertTrue($flow->isValid());
|
||||
self::assertFalse($flow->isFinished());
|
||||
self::assertNotNull($button = $flow->getClickedButton());
|
||||
self::assertTrue($button->isNextAction());
|
||||
|
||||
$flow = $flow->getStepForm();
|
||||
|
||||
self::assertFalse($flow->has('professional'), 'pro step should be skipped');
|
||||
self::assertSame('account', $flow->getCursor()->getCurrentStep());
|
||||
self::assertTrue($flow->has('account'));
|
||||
}
|
||||
|
||||
public function testResetAction()
|
||||
{
|
||||
$data = new UserSignUp();
|
||||
$data->firstName = 'John';
|
||||
$data->lastName = 'Doe';
|
||||
$data->worker = true;
|
||||
$data->currentStep = 'professional';
|
||||
|
||||
$dataStorage = new InMemoryDataStorage('user_sign_up');
|
||||
$dataStorage->save($data);
|
||||
|
||||
$flow = $this->factory->create(UserSignUpType::class, new UserSignUp(), [
|
||||
'data_storage' => $dataStorage,
|
||||
]);
|
||||
|
||||
self::assertSame('professional', $flow->getCursor()->getCurrentStep());
|
||||
self::assertTrue($flow->has('professional'));
|
||||
|
||||
$flow->submit([
|
||||
'professional' => [
|
||||
'company' => 'Acme',
|
||||
'role' => 'ROLE_DEVELOPER',
|
||||
],
|
||||
'navigator' => [
|
||||
'reset' => '',
|
||||
],
|
||||
]);
|
||||
|
||||
self::assertTrue($flow->isSubmitted());
|
||||
self::assertTrue($flow->isValid());
|
||||
self::assertFalse($flow->isFinished());
|
||||
self::assertNotNull($button = $flow->getClickedButton());
|
||||
self::assertTrue($button->isResetAction());
|
||||
|
||||
$flow = $flow->getStepForm();
|
||||
/** @var UserSignUp $data */
|
||||
$data = $flow->getViewData();
|
||||
|
||||
self::assertSame('personal', $flow->getCursor()->getCurrentStep());
|
||||
self::assertTrue($flow->has('personal'), 'reset action should move the flow to the initial step');
|
||||
self::assertNull($data->firstName);
|
||||
self::assertNull($data->lastName);
|
||||
self::assertFalse($data->worker);
|
||||
self::assertNull($data->company);
|
||||
self::assertNull($data->role);
|
||||
}
|
||||
|
||||
public function testResetManually()
|
||||
{
|
||||
$data = new UserSignUp();
|
||||
$data->firstName = 'John';
|
||||
$data->lastName = 'Doe';
|
||||
$data->worker = true;
|
||||
$data->currentStep = 'professional';
|
||||
|
||||
$dataStorage = new InMemoryDataStorage('user_sign_up');
|
||||
$dataStorage->save($data);
|
||||
|
||||
$flow = $this->factory->create(UserSignUpType::class, new UserSignUp(), [
|
||||
'data_storage' => $dataStorage,
|
||||
]);
|
||||
|
||||
self::assertSame('professional', $flow->getCursor()->getCurrentStep());
|
||||
|
||||
$flow->reset();
|
||||
|
||||
self::assertSame('personal', $flow->getCursor()->getCurrentStep());
|
||||
}
|
||||
|
||||
public function testSkipAction()
|
||||
{
|
||||
$data = new UserSignUp();
|
||||
$data->firstName = 'John';
|
||||
$data->lastName = 'Doe';
|
||||
$data->worker = true;
|
||||
$data->currentStep = 'professional';
|
||||
|
||||
$dataStorage = new InMemoryDataStorage('user_sign_up');
|
||||
$dataStorage->save($data);
|
||||
|
||||
$flow = $this->factory->create(UserSignUpType::class, new UserSignUp(), [
|
||||
'data_storage' => $dataStorage,
|
||||
]);
|
||||
|
||||
self::assertSame('professional', $flow->getCursor()->getCurrentStep());
|
||||
self::assertTrue($flow->has('professional'));
|
||||
|
||||
$flow->submit([
|
||||
'professional' => [
|
||||
'company' => 'Acme',
|
||||
'role' => 'ROLE_DEVELOPER',
|
||||
],
|
||||
'navigator' => [
|
||||
'skip' => '',
|
||||
],
|
||||
]);
|
||||
|
||||
self::assertTrue($flow->isSubmitted());
|
||||
self::assertTrue($flow->isValid());
|
||||
self::assertFalse($flow->isFinished());
|
||||
self::assertNotNull($button = $flow->getClickedButton());
|
||||
self::assertTrue($button->isNextAction());
|
||||
self::assertSame('skip', $button->getName());
|
||||
|
||||
$flow = $flow->getStepForm();
|
||||
/** @var UserSignUp $data */
|
||||
$data = $flow->getViewData();
|
||||
|
||||
self::assertSame('account', $flow->getCursor()->getCurrentStep());
|
||||
self::assertTrue($flow->has('account'), 'skip action should move the flow to the next step but skip submitted data and clear');
|
||||
self::assertSame('John', $data->firstName);
|
||||
self::assertSame('Doe', $data->lastName);
|
||||
self::assertTrue($data->worker);
|
||||
self::assertNull($data->company);
|
||||
self::assertNull($data->role);
|
||||
}
|
||||
|
||||
public function testTypeExtensionAndStepsPriority()
|
||||
{
|
||||
$factory = Forms::createFormFactoryBuilder()
|
||||
->addTypeExtension(new UserSignUpTypeExtension())
|
||||
->getFormFactory();
|
||||
|
||||
$flow = $factory->create(UserSignUpType::class, new UserSignUp());
|
||||
|
||||
self::assertSame('first', $flow->getCursor()->getCurrentStep());
|
||||
self::assertSame(['first', 'personal', 'professional', 'account', 'last'], $flow->getCursor()->getSteps());
|
||||
}
|
||||
|
||||
public function testMoveBackToStep()
|
||||
{
|
||||
$data = new UserSignUp();
|
||||
$data->firstName = 'John';
|
||||
$data->lastName = 'Doe';
|
||||
$data->worker = true;
|
||||
$data->company = 'Acme';
|
||||
$data->role = 'ROLE_DEVELOPER';
|
||||
$data->currentStep = 'account';
|
||||
|
||||
$flow = $this->factory->create(UserSignUpType::class, $data);
|
||||
$flow->get('navigator')->add('back_to_step', PreviousFlowType::class, [
|
||||
'validate' => false,
|
||||
'validation_groups' => false,
|
||||
'clear_submission' => false,
|
||||
]);
|
||||
|
||||
self::assertSame('account', $flow->getCursor()->getCurrentStep());
|
||||
|
||||
$flow->submit([
|
||||
'account' => [
|
||||
'email' => 'jdoe@acme.com',
|
||||
'password' => '$ecret',
|
||||
],
|
||||
'navigator' => [
|
||||
'back_to_step' => 'personal',
|
||||
],
|
||||
]);
|
||||
|
||||
self::assertTrue($flow->isSubmitted());
|
||||
self::assertTrue($flow->isValid());
|
||||
self::assertFalse($flow->isFinished());
|
||||
self::assertNotNull($button = $flow->getClickedButton());
|
||||
self::assertTrue($button->isPreviousAction());
|
||||
self::assertSame('personal', $button->getViewData());
|
||||
|
||||
$flow = $flow->getStepForm();
|
||||
|
||||
self::assertSame('personal', $flow->getCursor()->getCurrentStep());
|
||||
self::assertTrue($flow->has('personal'));
|
||||
self::assertSame('John', $data->firstName);
|
||||
self::assertSame('Acme', $data->company);
|
||||
self::assertSame('jdoe@acme.com', $data->email);
|
||||
}
|
||||
|
||||
public function testMoveManually()
|
||||
{
|
||||
$data = new UserSignUp();
|
||||
$data->firstName = 'John';
|
||||
$data->lastName = 'Doe';
|
||||
$data->worker = true;
|
||||
$data->currentStep = 'professional';
|
||||
|
||||
$dataStorage = new InMemoryDataStorage('user_sign_up');
|
||||
$dataStorage->save($data);
|
||||
|
||||
$flow = $this->factory->create(UserSignUpType::class, new UserSignUp(), [
|
||||
'data_storage' => $dataStorage,
|
||||
]);
|
||||
|
||||
self::assertSame('professional', $flow->getCursor()->getCurrentStep());
|
||||
self::assertTrue($flow->has('professional'));
|
||||
|
||||
$flow->movePrevious();
|
||||
$flow = $flow->newStepForm();
|
||||
|
||||
self::assertSame('personal', $flow->getCursor()->getCurrentStep());
|
||||
self::assertTrue($flow->has('personal'));
|
||||
|
||||
$flow->moveNext();
|
||||
$flow = $flow->newStepForm();
|
||||
|
||||
self::assertSame('professional', $flow->getCursor()->getCurrentStep());
|
||||
self::assertTrue($flow->has('professional'));
|
||||
}
|
||||
|
||||
public function testInvalidMovePreviousUntilAheadStep()
|
||||
{
|
||||
$data = new UserSignUp();
|
||||
$data->currentStep = 'personal';
|
||||
$flow = $this->factory->create(UserSignUpType::class, $data);
|
||||
|
||||
$this->expectException(RuntimeException::class);
|
||||
$this->expectExceptionMessage('Cannot move back to step "account" because it is ahead of the current step "personal".');
|
||||
|
||||
$flow->movePrevious('account');
|
||||
}
|
||||
|
||||
public function testInvalidMovePreviousUntilSkippedStep()
|
||||
{
|
||||
$data = new UserSignUp();
|
||||
$data->worker = false;
|
||||
$data->currentStep = 'account';
|
||||
$flow = $this->factory->create(UserSignUpType::class, $data);
|
||||
|
||||
$this->expectException(RuntimeException::class);
|
||||
$this->expectExceptionMessage('Cannot move back to step "professional" because it is a skipped step.');
|
||||
|
||||
$flow->movePrevious('professional');
|
||||
}
|
||||
|
||||
public function testInvalidStepForm()
|
||||
{
|
||||
$flow = $this->factory->create(UserSignUpType::class, new UserSignUp());
|
||||
|
||||
self::assertSame('personal', $flow->getCursor()->getCurrentStep());
|
||||
self::assertTrue($flow->has('personal'));
|
||||
|
||||
$flow->submit([
|
||||
'personal' => [
|
||||
'firstName' => '', // This value should not be blank
|
||||
'lastName' => 'Doe',
|
||||
],
|
||||
'navigator' => [
|
||||
'next' => '',
|
||||
],
|
||||
]);
|
||||
|
||||
self::assertTrue($flow->isSubmitted());
|
||||
self::assertFalse($flow->isValid());
|
||||
self::assertNotNull($button = $flow->getClickedButton());
|
||||
self::assertTrue($button->isNextAction());
|
||||
self::assertSame($flow, $flow->getStepForm());
|
||||
self::assertSame('This value should not be blank.', $flow->getErrors(true)->current()->getMessage());
|
||||
}
|
||||
|
||||
public function testCannotModifyStepConfigAfterFormBuilding()
|
||||
{
|
||||
$flow = $this->factory->create(UserSignUpType::class, new UserSignUp());
|
||||
|
||||
$this->expectException(BadMethodCallException::class);
|
||||
$this->expectExceptionMessage('StepFlowBuilder methods cannot be accessed anymore once the builder is turned into a StepFlowConfigInterface instance.');
|
||||
|
||||
$flow->getConfig()->getStep('personal')->setPriority(0);
|
||||
}
|
||||
|
||||
public function testIgnoreSubmissionIfStepIsMissing()
|
||||
{
|
||||
$flow = $this->factory->create(UserSignUpType::class, new UserSignUp());
|
||||
|
||||
self::assertSame('personal', $flow->getCursor()->getCurrentStep());
|
||||
self::assertTrue($flow->has('personal'));
|
||||
|
||||
$flow->submit([
|
||||
'account' => [
|
||||
'firstName' => '',
|
||||
'lastName' => '',
|
||||
],
|
||||
'navigator' => [
|
||||
'previous' => '',
|
||||
],
|
||||
]);
|
||||
|
||||
self::assertFalse($flow->isSubmitted());
|
||||
}
|
||||
|
||||
public function testViewVars()
|
||||
{
|
||||
$flow = $this->factory->create(UserSignUpType::class, new UserSignUp());
|
||||
$view = $flow->createView();
|
||||
|
||||
self::assertInstanceOf(FormFlowCursor::class, $view->vars['cursor']);
|
||||
self::assertCount(3, $view->vars['steps']);
|
||||
self::assertSame(['personal', 'professional', 'account'], array_keys($view->vars['steps']));
|
||||
self::assertSame('personal', $view->vars['steps']['personal']['name']);
|
||||
self::assertTrue($view->vars['steps']['personal']['is_current_step']);
|
||||
self::assertFalse($view->vars['steps']['personal']['is_skipped']);
|
||||
self::assertSame('professional', $view->vars['steps']['professional']['name']);
|
||||
self::assertFalse($view->vars['steps']['professional']['is_current_step']);
|
||||
self::assertTrue($view->vars['steps']['professional']['is_skipped']);
|
||||
self::assertSame('account', $view->vars['steps']['account']['name']);
|
||||
self::assertFalse($view->vars['steps']['account']['is_current_step']);
|
||||
self::assertFalse($view->vars['steps']['account']['is_skipped']);
|
||||
}
|
||||
|
||||
public function testFallbackCurrentStep()
|
||||
{
|
||||
$flow = $this->factory->create(UserSignUpType::class, new UserSignUp());
|
||||
|
||||
/** @var UserSignUp $data */
|
||||
$data = $flow->getViewData();
|
||||
|
||||
self::assertSame('personal', $flow->getCursor()->getCurrentStep(), 'The current step should be the first one depending on the step priority');
|
||||
self::assertSame('personal', $data->currentStep);
|
||||
}
|
||||
|
||||
public function testInitialCurrentStep()
|
||||
{
|
||||
$data = new UserSignUp();
|
||||
$data->currentStep = 'professional';
|
||||
$flow = $this->factory->create(UserSignUpType::class, $data);
|
||||
|
||||
self::assertSame('professional', $flow->getCursor()->getCurrentStep(), 'The current step should be the one set in the initial data');
|
||||
self::assertSame('professional', $data->currentStep);
|
||||
}
|
||||
|
||||
public function testFormFlowWithArrayData()
|
||||
{
|
||||
$flow = $this->factory->create(UserSignUpType::class, [], [
|
||||
'data_class' => null,
|
||||
'step_property_path' => '[currentStep]',
|
||||
]);
|
||||
|
||||
self::assertSame('personal', $flow->getCursor()->getCurrentStep());
|
||||
self::assertFalse($flow->isSubmitted());
|
||||
self::assertNull($flow->getClickedButton());
|
||||
self::assertTrue($flow->has('personal'));
|
||||
self::assertTrue($flow->has('navigator'));
|
||||
|
||||
$stepForm = $flow->get('personal');
|
||||
self::assertCount(3, $stepForm->all());
|
||||
self::assertTrue($stepForm->has('firstName'));
|
||||
self::assertTrue($stepForm->has('lastName'));
|
||||
self::assertTrue($stepForm->has('worker'));
|
||||
|
||||
$navigatorForm = $flow->get('navigator');
|
||||
self::assertCount(2, $navigatorForm->all());
|
||||
self::assertTrue($navigatorForm->has('reset'));
|
||||
self::assertTrue($navigatorForm->has('next'));
|
||||
|
||||
$flow->submit([
|
||||
'personal' => [
|
||||
'firstName' => 'John',
|
||||
'lastName' => 'Doe',
|
||||
'worker' => '1',
|
||||
],
|
||||
'navigator' => [
|
||||
'next' => '',
|
||||
],
|
||||
]);
|
||||
|
||||
self::assertSame('personal', $flow->getCursor()->getCurrentStep());
|
||||
self::assertTrue($flow->isSubmitted());
|
||||
self::assertTrue($flow->isValid());
|
||||
self::assertFalse($flow->isFinished());
|
||||
self::assertNotNull($button = $flow->getClickedButton());
|
||||
self::assertTrue($button->isNextAction());
|
||||
self::assertTrue($button->isClicked());
|
||||
|
||||
$flow = $flow->getStepForm();
|
||||
|
||||
$data = $flow->getData();
|
||||
self::assertSame('professional', $data['currentStep']);
|
||||
self::assertSame('professional', $flow->getCursor()->getCurrentStep());
|
||||
self::assertFalse($flow->isSubmitted());
|
||||
self::assertNull($flow->getClickedButton());
|
||||
self::assertTrue($flow->has('professional'));
|
||||
self::assertTrue($flow->has('navigator'));
|
||||
|
||||
$stepForm = $flow->get('professional');
|
||||
self::assertCount(2, $stepForm->all());
|
||||
self::assertTrue($stepForm->has('company'));
|
||||
self::assertTrue($stepForm->has('role'));
|
||||
|
||||
$navigatorForm = $flow->get('navigator');
|
||||
self::assertCount(4, $navigatorForm->all());
|
||||
self::assertTrue($navigatorForm->has('reset'));
|
||||
self::assertTrue($navigatorForm->has('previous'));
|
||||
self::assertTrue($navigatorForm->has('skip'));
|
||||
self::assertTrue($navigatorForm->has('next'));
|
||||
|
||||
$flow->submit([
|
||||
'professional' => [
|
||||
'company' => 'Acme',
|
||||
'role' => 'ROLE_DEVELOPER',
|
||||
],
|
||||
'navigator' => [
|
||||
'next' => '',
|
||||
],
|
||||
]);
|
||||
|
||||
self::assertSame('professional', $flow->getCursor()->getCurrentStep());
|
||||
self::assertTrue($flow->isSubmitted());
|
||||
self::assertTrue($flow->isValid());
|
||||
self::assertFalse($flow->isFinished());
|
||||
self::assertNotNull($button = $flow->getClickedButton());
|
||||
self::assertTrue($button->isNextAction());
|
||||
self::assertTrue($button->isClicked());
|
||||
|
||||
$flow = $flow->getStepForm();
|
||||
|
||||
$data = $flow->getData();
|
||||
self::assertSame('account', $data['currentStep']);
|
||||
self::assertSame('account', $flow->getCursor()->getCurrentStep());
|
||||
self::assertFalse($flow->isSubmitted());
|
||||
self::assertNull($flow->getClickedButton());
|
||||
self::assertTrue($flow->has('account'));
|
||||
self::assertTrue($flow->has('navigator'));
|
||||
|
||||
$stepForm = $flow->get('account');
|
||||
self::assertCount(2, $stepForm->all());
|
||||
self::assertTrue($stepForm->has('email'));
|
||||
self::assertTrue($stepForm->has('password'));
|
||||
|
||||
$navigatorForm = $flow->get('navigator');
|
||||
self::assertCount(3, $navigatorForm->all());
|
||||
self::assertTrue($navigatorForm->has('reset'));
|
||||
self::assertTrue($navigatorForm->has('previous'));
|
||||
self::assertTrue($navigatorForm->has('finish'));
|
||||
|
||||
$flow->submit([
|
||||
'account' => [
|
||||
'email' => 'john@acme.com',
|
||||
'password' => 'eBvU2vBLfSXqf36',
|
||||
],
|
||||
'navigator' => [
|
||||
'finish' => '',
|
||||
],
|
||||
]);
|
||||
|
||||
self::assertTrue($flow->isSubmitted());
|
||||
self::assertTrue($flow->isValid());
|
||||
self::assertTrue($flow->isFinished());
|
||||
self::assertNotNull($button = $flow->getClickedButton());
|
||||
self::assertTrue($button->isFinishAction());
|
||||
self::assertTrue($button->isClicked());
|
||||
self::assertSame('personal', $flow->getCursor()->getCurrentStep());
|
||||
|
||||
$data = $flow->getData();
|
||||
self::assertSame('John', $data['firstName']);
|
||||
self::assertSame('Doe', $data['lastName']);
|
||||
self::assertTrue($data['worker']);
|
||||
self::assertSame('Acme', $data['company']);
|
||||
self::assertSame('ROLE_DEVELOPER', $data['role']);
|
||||
self::assertSame('john@acme.com', $data['email']);
|
||||
self::assertSame('eBvU2vBLfSXqf36', $data['password']);
|
||||
}
|
||||
|
||||
public function testHandleActionManually()
|
||||
{
|
||||
$flow = $this->factory->create(UserSignUpType::class, new UserSignUp());
|
||||
|
||||
self::assertSame('personal', $flow->getCursor()->getCurrentStep());
|
||||
|
||||
$flow->submit([
|
||||
'personal' => [
|
||||
'firstName' => 'John',
|
||||
'lastName' => 'Doe',
|
||||
'worker' => '1',
|
||||
],
|
||||
'navigator' => [
|
||||
'next' => '',
|
||||
],
|
||||
]);
|
||||
|
||||
self::assertTrue($flow->isSubmitted());
|
||||
self::assertTrue($flow->isValid());
|
||||
self::assertNotNull($actionButton = $flow->getClickedButton());
|
||||
self::assertSame('personal', $flow->getCursor()->getCurrentStep());
|
||||
|
||||
$actionButton->handle();
|
||||
|
||||
self::assertSame('professional', $flow->getCursor()->getCurrentStep());
|
||||
}
|
||||
|
||||
public function testAddFormErrorOnActionHandling()
|
||||
{
|
||||
$flow = $this->factory->create(UserSignUpType::class, new UserSignUp());
|
||||
$flow->get('navigator')->add('next', NextFlowType::class, [
|
||||
'handler' => function (mixed $data, ButtonFlowInterface $button, FormFlowInterface $flow) {
|
||||
$flow->addError(new FormError('Action error'));
|
||||
},
|
||||
]);
|
||||
|
||||
self::assertSame('personal', $flow->getCursor()->getCurrentStep());
|
||||
|
||||
$flow->submit([
|
||||
'personal' => [
|
||||
'firstName' => 'John',
|
||||
'lastName' => 'Doe',
|
||||
],
|
||||
'navigator' => [
|
||||
'next' => '',
|
||||
],
|
||||
]);
|
||||
|
||||
self::assertTrue($flow->isSubmitted());
|
||||
self::assertTrue($flow->isValid());
|
||||
self::assertNotNull($actionButton = $flow->getClickedButton());
|
||||
self::assertSame('personal', $flow->getCursor()->getCurrentStep());
|
||||
|
||||
$actionButton->handle();
|
||||
$flow = $flow->getStepForm();
|
||||
$errors = $flow->getErrors(true);
|
||||
|
||||
self::assertFalse($flow->isValid());
|
||||
self::assertCount(1, $errors);
|
||||
self::assertSame('Action error', $errors->current()->getMessage());
|
||||
self::assertSame('personal', $flow->getCursor()->getCurrentStep());
|
||||
}
|
||||
|
||||
public function testStepValidationGroups()
|
||||
{
|
||||
$data = new UserSignUp();
|
||||
$data->worker = true;
|
||||
$flow = $this->factory->create(UserSignUpType::class, $data);
|
||||
|
||||
// Check that validation groups include the current step name
|
||||
self::assertSame(['Default', 'personal'], $flow->getConfig()->getOption('validation_groups')($flow));
|
||||
|
||||
// Move to next step
|
||||
$flow->moveNext();
|
||||
$flow = $flow->newStepForm();
|
||||
|
||||
// Check that validation groups are updated
|
||||
self::assertEquals(['Default', 'professional'], $flow->getConfig()->getOption('validation_groups')($flow));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user