diff --git a/src/Form/Extension/Core/Type/FormFlowActionType.php b/src/Form/Extension/Core/Type/FormFlowActionType.php deleted file mode 100644 index dadeabd..0000000 --- a/src/Form/Extension/Core/Type/FormFlowActionType.php +++ /dev/null @@ -1,84 +0,0 @@ -define('action') - ->info('The action name of the button') - ->default('') - ->allowedTypes('string'); - - $resolver->define('handler') - ->info('A callable that will be called when this button is clicked') - ->default(function (Options $options) { - if (!\in_array($options['action'], ['back', 'next', 'finish', 'reset'], true)) { - throw new MissingOptionsException(\sprintf('The option "handler" is required for the action "%s".', $options['action'])); - } - - return function (mixed $data, ActionButtonInterface $button, FormFlowInterface $flow): void { - match (true) { - $button->isBackAction() => $flow->moveBack($button->getViewData()), - $button->isNextAction() => $flow->moveNext(), - $button->isFinishAction() => $flow->reset(), - $button->isResetAction() => $flow->reset(), - }; - }; - }) - ->allowedTypes('callable'); - - $resolver->define('include_if') - ->info('Decide whether to include this button in the current form') - ->default(function (Options $options) { - return match ($options['action']) { - 'back' => fn (FormFlowCursor $cursor): bool => $cursor->canMoveBack(), - 'next' => fn (FormFlowCursor $cursor): bool => $cursor->canMoveNext(), - 'finish' => fn (FormFlowCursor $cursor): bool => $cursor->isLastStep(), - 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(function (Options $options) { - return 'reset' === $options['action'] || 'back' === $options['action']; - }) - ->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; - } -} diff --git a/src/Form/Extension/HttpFoundation/Type/FormFlowTypeSessionDataStorageExtension.php b/src/Form/Extension/HttpFoundation/Type/FormFlowTypeSessionDataStorageExtension.php index 29b5277..fe59991 100644 --- a/src/Form/Extension/HttpFoundation/Type/FormFlowTypeSessionDataStorageExtension.php +++ b/src/Form/Extension/HttpFoundation/Type/FormFlowTypeSessionDataStorageExtension.php @@ -5,9 +5,9 @@ use Symfony\Component\Form\AbstractTypeExtension; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\HttpFoundation\RequestStack; -use Yceruto\FormFlowBundle\Form\Extension\Core\Type\FormFlowType; use Yceruto\FormFlowBundle\Form\Flow\DataStorage\SessionDataStorage; use Yceruto\FormFlowBundle\Form\Flow\FormFlowBuilderInterface; +use Yceruto\FormFlowBundle\Form\Flow\Type\FormFlowType; class FormFlowTypeSessionDataStorageExtension extends AbstractTypeExtension { diff --git a/src/Form/Flow/AbstractFlowType.php b/src/Form/Flow/AbstractFlowType.php index 5412148..e9c655e 100644 --- a/src/Form/Flow/AbstractFlowType.php +++ b/src/Form/Flow/AbstractFlowType.php @@ -3,10 +3,22 @@ namespace Yceruto\FormFlowBundle\Form\Flow; use Symfony\Component\Form\AbstractType; -use Yceruto\FormFlowBundle\Form\Extension\Core\Type\FormFlowType; +use Symfony\Component\Form\FormBuilderInterface; +use Yceruto\FormFlowBundle\Form\Flow\Type\FormFlowType; abstract class AbstractFlowType extends AbstractType implements FormFlowTypeInterface { + final public function buildForm(FormBuilderInterface $builder, array $options): void + { + \assert($builder instanceof FormFlowBuilderInterface); + + $this->buildFormFlow($builder, $options); + } + + public function buildFormFlow(FormFlowBuilderInterface $builder, array $options): void + { + } + public function getParent(): string { return FormFlowType::class; diff --git a/src/Form/Flow/ActionButtonBuilder.php b/src/Form/Flow/ActionButtonBuilder.php deleted file mode 100644 index 17be3b4..0000000 --- a/src/Form/Flow/ActionButtonBuilder.php +++ /dev/null @@ -1,16 +0,0 @@ -getFormConfig()); - } -} diff --git a/src/Form/Flow/ActionButtonTypeInterface.php b/src/Form/Flow/ActionButtonTypeInterface.php deleted file mode 100644 index d4b52f0..0000000 --- a/src/Form/Flow/ActionButtonTypeInterface.php +++ /dev/null @@ -1,12 +0,0 @@ -data; } - public function getAction(): string - { - return $this->getConfig()->getOption('action'); - } - - public function getHandler(): callable - { - return $this->getConfig()->getOption('handler'); - } - - public function isHandled(): bool - { - return $this->handled; - } - public function handle(): void { /** @var FormInterface $form */ @@ -58,30 +44,35 @@ public function handle(): void $form = $form->getParent(); } - $handler = $this->getHandler(); + $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->getAction(); + return 'reset' === $this->getConfig()->getAttribute('action'); } - public function isBackAction(): bool + public function isPreviousAction(): bool { - return 'back' === $this->getAction(); + return 'previous' === $this->getConfig()->getAttribute('action'); } public function isNextAction(): bool { - return 'next' === $this->getAction(); + return 'next' === $this->getConfig()->getAttribute('action'); } public function isFinishAction(): bool { - return 'finish' === $this->getAction(); + return 'finish' === $this->getConfig()->getAttribute('action'); } public function isClearSubmission(): bool diff --git a/src/Form/Flow/FlowButtonBuilder.php b/src/Form/Flow/FlowButtonBuilder.php new file mode 100644 index 0000000..eec5595 --- /dev/null +++ b/src/Form/Flow/FlowButtonBuilder.php @@ -0,0 +1,16 @@ +getFormConfig()); + } +} diff --git a/src/Form/Flow/ActionButtonInterface.php b/src/Form/Flow/FlowButtonInterface.php similarity index 67% rename from src/Form/Flow/ActionButtonInterface.php rename to src/Form/Flow/FlowButtonInterface.php index 1997594..b3230a0 100644 --- a/src/Form/Flow/ActionButtonInterface.php +++ b/src/Form/Flow/FlowButtonInterface.php @@ -5,37 +5,27 @@ use Symfony\Component\Form\ClickableInterface; use Symfony\Component\Form\FormInterface; -interface ActionButtonInterface extends FormInterface, ClickableInterface +interface FlowButtonInterface extends FormInterface, ClickableInterface { /** - * Returns the action name configured for the button. - */ - public function getAction(): string; - - /** - * Returns the callable handler configured for the button. + * Executes the callable handler. */ - public function getHandler(): callable; + public function handle(): void; /** * Checks if the callable handler was already called. */ public function isHandled(): bool; - /** - * Executes the callable handler. - */ - public function handle(): void; - /** * Checks if the button's action is 'reset'. */ public function isResetAction(): bool; /** - * Checks if the button's action is 'back'. + * Checks if the button's action is 'previous'. */ - public function isBackAction(): bool; + public function isPreviousAction(): bool; /** * Checks if the button's action is 'next'. diff --git a/src/Form/Flow/FlowButtonTypeInterface.php b/src/Form/Flow/FlowButtonTypeInterface.php new file mode 100644 index 0000000..d5a2730 --- /dev/null +++ b/src/Form/Flow/FlowButtonTypeInterface.php @@ -0,0 +1,12 @@ + $steps @@ -38,7 +38,7 @@ public function getFirstStep(): string return $this->steps[0]; } - public function getPrevStep(): ?string + public function getPreviousStep(): ?string { $currentPos = array_search($this->currentStep, $this->steps, true); @@ -81,7 +81,7 @@ public function isLastStep(): bool public function canMoveBack(): bool { - return null !== $this->getPrevStep(); + return null !== $this->getPreviousStep(); } public function canMoveNext(): bool diff --git a/src/Form/Flow/FormFlowStepBuilder.php b/src/Form/Flow/FlowStepBuilder.php similarity index 62% rename from src/Form/Flow/FormFlowStepBuilder.php rename to src/Form/Flow/FlowStepBuilder.php index 0f6b7f9..d9a82cf 100644 --- a/src/Form/Flow/FormFlowStepBuilder.php +++ b/src/Form/Flow/FlowStepBuilder.php @@ -5,7 +5,7 @@ use Symfony\Component\Form\Exception\BadMethodCallException; use Symfony\Component\Form\FormTypeInterface; -class FormFlowStepBuilder implements FormFlowStepBuilderInterface +class FlowStepBuilder implements FlowStepBuilderInterface { private bool $locked = false; private int $priority = 0; @@ -29,7 +29,7 @@ public function getName(): string public function getType(): string { if ($this->locked) { - throw new BadMethodCallException('FormFlowStepBuilder methods cannot be accessed anymore once the builder is turned into a FormFlowStepConfigInterface instance.'); + throw new BadMethodCallException('FlowStepBuilder methods cannot be accessed anymore once the builder is turned into a FlowStepConfigInterface instance.'); } return $this->type; @@ -38,7 +38,7 @@ public function getType(): string public function getOptions(): array { if ($this->locked) { - throw new BadMethodCallException('FormFlowStepBuilder methods cannot be accessed anymore once the builder is turned into a FormFlowStepConfigInterface instance.'); + throw new BadMethodCallException('FlowStepBuilder methods cannot be accessed anymore once the builder is turned into a FlowStepConfigInterface instance.'); } return $this->options; @@ -52,7 +52,7 @@ public function getPriority(): int public function setPriority(int $priority): static { if ($this->locked) { - throw new BadMethodCallException('FormFlowStepBuilder methods cannot be accessed anymore once the builder is turned into a FormFlowStepConfigInterface instance.'); + throw new BadMethodCallException('FlowStepBuilder methods cannot be accessed anymore once the builder is turned into a FlowStepConfigInterface instance.'); } $this->priority = $priority; @@ -77,7 +77,7 @@ public function isSkipped(mixed $data): bool public function setSkip(?\Closure $skip): static { if ($this->locked) { - throw new BadMethodCallException('FormFlowStepBuilder methods cannot be accessed anymore once the builder is turned into a FormFlowStepConfigInterface instance.'); + throw new BadMethodCallException('FlowStepBuilder methods cannot be accessed anymore once the builder is turned into a FlowStepConfigInterface instance.'); } $this->skip = $skip; @@ -85,10 +85,10 @@ public function setSkip(?\Closure $skip): static return $this; } - public function getStepConfig(): FormFlowStepConfigInterface + public function getStepConfig(): FlowStepConfigInterface { if ($this->locked) { - throw new BadMethodCallException('FormFlowStepBuilder methods cannot be accessed anymore once the builder is turned into a FormFlowStepConfigInterface instance.'); + throw new BadMethodCallException('FlowStepBuilder methods cannot be accessed anymore once the builder is turned into a FlowStepConfigInterface instance.'); } // This method should be idempotent, so clone the builder diff --git a/src/Form/Flow/FormFlowStepBuilderInterface.php b/src/Form/Flow/FlowStepBuilderInterface.php similarity index 75% rename from src/Form/Flow/FormFlowStepBuilderInterface.php rename to src/Form/Flow/FlowStepBuilderInterface.php index a5c77cc..7113697 100644 --- a/src/Form/Flow/FormFlowStepBuilderInterface.php +++ b/src/Form/Flow/FlowStepBuilderInterface.php @@ -2,7 +2,7 @@ namespace Yceruto\FormFlowBundle\Form\Flow; -interface FormFlowStepBuilderInterface extends FormFlowStepConfigInterface +interface FlowStepBuilderInterface extends FlowStepConfigInterface { /** * Returns the form type class name for the step. @@ -30,7 +30,7 @@ public function setPriority(int $priority): static; public function setSkip(?\Closure $skip): static; /** - * Returns a FormFlowStepConfigInterface instance for the step. + * Returns a FlowStepConfigInterface instance for the step. */ - public function getStepConfig(): FormFlowStepConfigInterface; + public function getStepConfig(): FlowStepConfigInterface; } diff --git a/src/Form/Flow/FormFlowStepConfigInterface.php b/src/Form/Flow/FlowStepConfigInterface.php similarity index 91% rename from src/Form/Flow/FormFlowStepConfigInterface.php rename to src/Form/Flow/FlowStepConfigInterface.php index 6c3ee2a..cb08740 100644 --- a/src/Form/Flow/FormFlowStepConfigInterface.php +++ b/src/Form/Flow/FlowStepConfigInterface.php @@ -2,7 +2,7 @@ namespace Yceruto\FormFlowBundle\Form\Flow; -interface FormFlowStepConfigInterface +interface FlowStepConfigInterface { /** * Returns the name of the step. diff --git a/src/Form/Flow/FormFlow.php b/src/Form/Flow/FormFlow.php index 7a2ec8e..50a325e 100644 --- a/src/Form/Flow/FormFlow.php +++ b/src/Form/Flow/FormFlow.php @@ -2,6 +2,7 @@ namespace Yceruto\FormFlowBundle\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; @@ -16,12 +17,12 @@ */ class FormFlow extends Form implements FormFlowInterface { - private ?ActionButtonInterface $clickedActionButton = null; + private ?FlowButtonInterface $clickedFlowButton = null; private bool $finished = false; public function __construct( private readonly FormFlowConfigInterface $config, - private FormFlowCursor $cursor, + private FlowCursor $cursor, ) { parent::__construct($config); } @@ -42,15 +43,15 @@ public function submit(mixed $submittedData, bool $clearMissing = true): static return $this; } - $this->setClickedActionButton($submittedData, $this); + $this->setClickedFlowButton($submittedData, $this); parent::submit($submittedData, $clearMissing); - if (!$this->clickedActionButton || !$this->isSubmitted() || !$this->isValid()) { + if (!$this->clickedFlowButton || !$this->isSubmitted() || !$this->isValid()) { return $this; } - $this->finished = $this->clickedActionButton->isFinishAction(); + $this->finished = $this->clickedFlowButton->isFinishAction(); if ($this->finished && $this->config->isAutoReset()) { $this->reset(); @@ -65,7 +66,7 @@ public function reset(): void $this->cursor = $this->cursor->withCurrentStep($this->config->getInitialStep()); } - public function moveBack(?string $step = null): void + public function movePrevious(?string $step = null): void { if ($step) { $this->moveBackTo($step); @@ -73,14 +74,14 @@ public function moveBack(?string $step = null): void return; } - if (!$this->move(fn (FormFlowCursor $cursor) => $cursor->getPrevStep())) { + if (!$this->move(fn (FlowCursor $cursor) => $cursor->getPreviousStep())) { throw new RuntimeException('Cannot determine previous step.'); } } public function moveNext(): void { - if (!$this->move(fn (FormFlowCursor $cursor) => $cursor->getNextStep())) { + if (!$this->move(fn (FlowCursor $cursor) => $cursor->getNextStep())) { throw new RuntimeException('Cannot determine next step.'); } } @@ -96,8 +97,8 @@ public function getStepForm(): static return $this; } - if ($this->clickedActionButton && !$this->clickedActionButton->isHandled()) { - $this->clickedActionButton->handle(); + if ($this->clickedFlowButton && !$this->clickedFlowButton->isHandled()) { + $this->clickedFlowButton->handle(); } if (!$this->isValid()) { @@ -107,7 +108,7 @@ public function getStepForm(): static return $this->newStepForm(); } - public function getCursor(): FormFlowCursor + public function getCursor(): FlowCursor { return $this->cursor; } @@ -122,12 +123,12 @@ public function isFinished(): bool return $this->finished; } - public function getClickedActionButton(): ?ActionButtonInterface + public function getClickedButton(): FlowButtonInterface|FormInterface|ClickableInterface|null { - return $this->clickedActionButton; + return parent::getClickedButton() ?? $this->clickedFlowButton; } - private function setClickedActionButton(mixed $submittedData, FormInterface $form): void + private function setClickedFlowButton(mixed $submittedData, FormInterface $form): void { if (!\is_array($submittedData)) { return; @@ -139,23 +140,23 @@ private function setClickedActionButton(mixed $submittedData, FormInterface $for } if ($child->count() > 0) { - $this->setClickedActionButton($submittedData[$name], $child); + $this->setClickedFlowButton($submittedData[$name], $child); - if ($this->clickedActionButton) { + if ($this->clickedFlowButton) { return; } continue; } - if (!$child instanceof ActionButtonInterface) { + if (!$child instanceof FlowButtonInterface) { continue; } $child->submit($submittedData[$name]); if ($child->isClicked()) { - $this->clickedActionButton = $child; + $this->clickedFlowButton = $child; break; } } @@ -181,7 +182,7 @@ private function moveBackTo(string $step): void } while ($targetIndex < $currentIndex) { - $this->moveBack(); + $this->movePrevious(); $currentIndex = $this->cursor->getStepIndex(); } diff --git a/src/Form/Flow/FormFlowBuilder.php b/src/Form/Flow/FormFlowBuilder.php index ebdd1d0..3509ed2 100644 --- a/src/Form/Flow/FormFlowBuilder.php +++ b/src/Form/Flow/FormFlowBuilder.php @@ -6,10 +6,10 @@ use Symfony\Component\Form\Exception\InvalidArgumentException; use Symfony\Component\Form\Exception\LogicException; use Symfony\Component\Form\Extension\Core\Type\FormType; -use Yceruto\FormFlowBundle\Form\Flow\DataStorage\DataStorageInterface; -use Yceruto\FormFlowBundle\Form\Flow\StepAccessor\StepAccessorInterface; use Symfony\Component\Form\FormBuilder; use Symfony\Component\Form\FormBuilderInterface; +use Yceruto\FormFlowBundle\Form\Flow\DataStorage\DataStorageInterface; +use Yceruto\FormFlowBundle\Form\Flow\StepAccessor\StepAccessorInterface; /** * A builder for creating {@link FormFlow} instances. @@ -19,29 +19,29 @@ class FormFlowBuilder extends FormBuilder implements FormFlowBuilderInterface { /** - * @var array + * @var array */ private array $steps = []; private array $initialOptions = []; private DataStorageInterface $dataStorage; private StepAccessorInterface $stepAccessor; - public function createStep(string $name, string $type = FormType::class, array $options = []): FormFlowStepBuilderInterface + public function createStep(string $name, string $type = FormType::class, array $options = []): FlowStepBuilderInterface { if ($this->locked) { throw new BadMethodCallException('FormFlowBuilder methods cannot be accessed anymore once the builder is turned into a FormFlowConfigInterface instance.'); } - return new FormFlowStepBuilder($name, $type, $options); + return new FlowStepBuilder($name, $type, $options); } - public function addStep(FormFlowStepBuilderInterface|string $name, string $type = FormType::class, array $options = [], ?callable $skip = null, int $priority = 0): static + public function addStep(FlowStepBuilderInterface|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 FormFlowStepBuilderInterface) { + if ($name instanceof FlowStepBuilderInterface) { $this->steps[$name->getName()] = $name; return $this; @@ -71,7 +71,7 @@ public function hasStep(string $name): bool return isset($this->steps[$name]); } - public function getStep(string $name): FormFlowStepBuilderInterface + public function getStep(string $name): FlowStepBuilderInterface { return $this->steps[$name] ?? throw new InvalidArgumentException(\sprintf('Step "%s" does not exist.', $name)); } @@ -191,7 +191,7 @@ private function createFormFlow(): FormFlowInterface throw new InvalidArgumentException('Steps not configured.'); } - uasort($this->steps, static function (FormFlowStepBuilderInterface $a, FormFlowStepBuilderInterface $b) { + uasort($this->steps, static function (FlowStepBuilderInterface $a, FlowStepBuilderInterface $b) { return $b->getPriority() <=> $a->getPriority(); }); @@ -204,7 +204,7 @@ private function createFormFlow(): FormFlowInterface $step = $this->steps[$currentStep]; $this->add($step->getName(), $step->getType(), $step->getOptions()); - $cursor = new FormFlowCursor(array_keys($this->steps), $currentStep); + $cursor = new FlowCursor(array_keys($this->steps), $currentStep); $this->pruneActionButtons($this, $cursor); return new FormFlow($this->getFormConfig(), $cursor); @@ -223,7 +223,7 @@ private function resolveCurrentStep(): string return $currentStep; } - private function pruneActionButtons(FormBuilderInterface $builder, FormFlowCursor $cursor): void + private function pruneActionButtons(FormBuilderInterface $builder, FlowCursor $cursor): void { foreach ($builder->all() as $child) { if ($child->count() > 0) { @@ -232,7 +232,7 @@ private function pruneActionButtons(FormBuilderInterface $builder, FormFlowCurso continue; } - if (!$child instanceof ActionButtonBuilder || !\is_callable($include = $child->getOption('include_if'))) { + if (!$child instanceof FlowButtonBuilder || !\is_callable($include = $child->getOption('include_if'))) { continue; } diff --git a/src/Form/Flow/FormFlowBuilderInterface.php b/src/Form/Flow/FormFlowBuilderInterface.php index 679c5c5..ed05bd7 100644 --- a/src/Form/Flow/FormFlowBuilderInterface.php +++ b/src/Form/Flow/FormFlowBuilderInterface.php @@ -3,9 +3,9 @@ namespace Yceruto\FormFlowBundle\Form\Flow; use Symfony\Component\Form\Extension\Core\Type\FormType; +use Symfony\Component\Form\FormBuilderInterface; use Yceruto\FormFlowBundle\Form\Flow\DataStorage\DataStorageInterface; use Yceruto\FormFlowBundle\Form\Flow\StepAccessor\StepAccessorInterface; -use Symfony\Component\Form\FormBuilderInterface; /** * @extends \Traversable @@ -15,12 +15,12 @@ interface FormFlowBuilderInterface extends FormBuilderInterface, FormFlowConfigI /** * Creates a new step builder. */ - public function createStep(string $name, string $type = FormType::class, array $options = []): FormFlowStepBuilderInterface; + public function createStep(string $name, string $type = FormType::class, array $options = []): FlowStepBuilderInterface; /** * Adds a step to the form flow. */ - public function addStep(FormFlowStepBuilderInterface|string $name, string $type = FormType::class, array $options = [], ?callable $skip = null, int $priority = 0): static; + public function addStep(FlowStepBuilderInterface|string $name, string $type = FormType::class, array $options = [], ?callable $skip = null, int $priority = 0): static; /** * Removes a step from the form flow. @@ -30,12 +30,12 @@ public function removeStep(string $name): static; /** * Returns a step builder by name. */ - public function getStep(string $name): FormFlowStepBuilderInterface; + public function getStep(string $name): FlowStepBuilderInterface; /** * Returns all step builders. * - * @return array + * @return array */ public function getSteps(): array; diff --git a/src/Form/Flow/FormFlowConfigInterface.php b/src/Form/Flow/FormFlowConfigInterface.php index 841d3cb..ae4f22c 100644 --- a/src/Form/Flow/FormFlowConfigInterface.php +++ b/src/Form/Flow/FormFlowConfigInterface.php @@ -2,9 +2,9 @@ namespace Yceruto\FormFlowBundle\Form\Flow; +use Symfony\Component\Form\FormConfigInterface; use Yceruto\FormFlowBundle\Form\Flow\DataStorage\DataStorageInterface; use Yceruto\FormFlowBundle\Form\Flow\StepAccessor\StepAccessorInterface; -use Symfony\Component\Form\FormConfigInterface; /** * The configuration of a {@link FormFlow} object. @@ -19,12 +19,12 @@ public function hasStep(string $name): bool; /** * Returns the step with the given name. */ - public function getStep(string $name): FormFlowStepConfigInterface; + public function getStep(string $name): FlowStepConfigInterface; /** * Returns all steps. * - * @return array + * @return array */ public function getSteps(): array; diff --git a/src/Form/Flow/FormFlowInterface.php b/src/Form/Flow/FormFlowInterface.php index 3f9d69a..c6cffd2 100644 --- a/src/Form/Flow/FormFlowInterface.php +++ b/src/Form/Flow/FormFlowInterface.php @@ -2,16 +2,16 @@ namespace Yceruto\FormFlowBundle\Form\Flow; -use Symfony\Component\Form\Exception\LogicException; +use Symfony\Component\Form\ClickableInterface; use Symfony\Component\Form\Exception\RuntimeException; use Symfony\Component\Form\FormInterface; interface FormFlowInterface extends FormInterface { /** - * Returns the action button clicked during form submission. + * Returns the button used to submit the form. */ - public function getClickedActionButton(): ?ActionButtonInterface; + public function getClickedButton(): FlowButtonInterface|FormInterface|ClickableInterface|null; /** * Resets the flow by clearing stored data and setting the cursor to the initial step. @@ -25,7 +25,7 @@ public function reset(): void; * * @throws RuntimeException If the previous step cannot be determined */ - public function moveBack(?string $step = null): void; + public function movePrevious(?string $step = null): void; /** * Moves to the next step in the flow. @@ -48,7 +48,7 @@ public function getStepForm(): static; /** * Returns the cursor that tracks the current position in the flow. */ - public function getCursor(): FormFlowCursor; + public function getCursor(): FlowCursor; /** * Returns the configuration for this flow. diff --git a/src/Form/Flow/Type/FlowButtonType.php b/src/Form/Flow/Type/FlowButtonType.php new file mode 100644 index 0000000..b9dcc3b --- /dev/null +++ b/src/Form/Flow/Type/FlowButtonType.php @@ -0,0 +1,54 @@ +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 (FlowCursor $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; + } +} diff --git a/src/Form/Flow/Type/FlowFinishType.php b/src/Form/Flow/Type/FlowFinishType.php new file mode 100644 index 0000000..39d177a --- /dev/null +++ b/src/Form/Flow/Type/FlowFinishType.php @@ -0,0 +1,32 @@ +setAttribute('action', 'finish'); + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'handler' => fn (mixed $data, FlowButtonInterface $button, FormFlowInterface $flow) => $flow->reset(), + 'include_if' => fn (FlowCursor $cursor): bool => $cursor->isLastStep(), + ]); + } + + public function getParent(): string + { + return FlowButtonType::class; + } +} diff --git a/src/Form/Extension/Core/Type/FormFlowNavigatorType.php b/src/Form/Flow/Type/FlowNavigatorType.php similarity index 50% rename from src/Form/Extension/Core/Type/FormFlowNavigatorType.php rename to src/Form/Flow/Type/FlowNavigatorType.php index 8315cf0..fcb12d5 100644 --- a/src/Form/Extension/Core/Type/FormFlowNavigatorType.php +++ b/src/Form/Flow/Type/FlowNavigatorType.php @@ -1,29 +1,21 @@ add('back', FormFlowActionType::class, [ - 'action' => 'back', - ]); - - $builder->add('next', FormFlowActionType::class, [ - 'action' => 'next', - ]); - - $builder->add('finish', FormFlowActionType::class, [ - 'action' => 'finish', - ]); + $builder->add('previous', FlowPreviousType::class); + $builder->add('next', FlowNextType::class); + $builder->add('finish', FlowFinishType::class); } public function configureOptions(OptionsResolver $resolver): void diff --git a/src/Form/Flow/Type/FlowNextType.php b/src/Form/Flow/Type/FlowNextType.php new file mode 100644 index 0000000..4b8b3a0 --- /dev/null +++ b/src/Form/Flow/Type/FlowNextType.php @@ -0,0 +1,32 @@ +setAttribute('action', 'next'); + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'handler' => fn (mixed $data, FlowButtonInterface $button, FormFlowInterface $flow) => $flow->moveNext(), + 'include_if' => fn (FlowCursor $cursor): bool => $cursor->canMoveNext(), + ]); + } + + public function getParent(): string + { + return FlowButtonType::class; + } +} diff --git a/src/Form/Flow/Type/FlowPreviousType.php b/src/Form/Flow/Type/FlowPreviousType.php new file mode 100644 index 0000000..ecc4724 --- /dev/null +++ b/src/Form/Flow/Type/FlowPreviousType.php @@ -0,0 +1,33 @@ +setAttribute('action', 'previous'); + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'handler' => fn (mixed $data, FlowButtonInterface $button, FormFlowInterface $flow) => $flow->movePrevious($button->getViewData()), + 'include_if' => fn (FlowCursor $cursor): bool => $cursor->canMoveBack(), + 'clear_submission' => true, + ]); + } + + public function getParent(): string + { + return FlowButtonType::class; + } +} diff --git a/src/Form/Flow/Type/FlowResetType.php b/src/Form/Flow/Type/FlowResetType.php new file mode 100644 index 0000000..8ffb220 --- /dev/null +++ b/src/Form/Flow/Type/FlowResetType.php @@ -0,0 +1,31 @@ +setAttribute('action', 'reset'); + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'handler' => fn (mixed $data, FlowButtonInterface $button, FormFlowInterface $flow) => $flow->reset(), + 'clear_submission' => true, + ]); + } + + public function getParent(): string + { + return FlowButtonType::class; + } +} diff --git a/src/Form/Extension/Core/Type/FormFlowType.php b/src/Form/Flow/Type/FormFlowType.php similarity index 92% rename from src/Form/Extension/Core/Type/FormFlowType.php rename to src/Form/Flow/Type/FormFlowType.php index 2921bc6..f60dd70 100644 --- a/src/Form/Extension/Core/Type/FormFlowType.php +++ b/src/Form/Flow/Type/FormFlowType.php @@ -1,9 +1,8 @@ propertyAccessor ??= PropertyAccess::createPropertyAccessor(); } - public function buildForm(FormBuilderInterface $builder, array $options): void + public function buildFormFlow(FormFlowBuilderInterface $builder, array $options): void { - \assert($builder instanceof FormFlowBuilderInterface); - $builder->setDataStorage($options['data_storage'] ?? new NullDataStorage()); $builder->setStepAccessor($options['step_accessor']); @@ -114,8 +112,9 @@ public function onPreSubmit(FormEvent $event): void { /** @var FormFlowInterface $flow */ $flow = $event->getForm(); + $button = $flow->getClickedButton(); - if ($flow->getClickedActionButton()?->isClearSubmission()) { + if ($button instanceof FlowButtonInterface && $button->isClearSubmission()) { $event->setData([]); } } diff --git a/src/Form/ResolvedFormType.php b/src/Form/ResolvedFormType.php index 79bc6b6..7f33fae 100644 --- a/src/Form/ResolvedFormType.php +++ b/src/Form/ResolvedFormType.php @@ -9,8 +9,8 @@ use Symfony\Component\Form\ResolvedFormType as SymfonyResolvedFormType; use Symfony\Component\Form\ResolvedFormTypeInterface; use Symfony\Component\OptionsResolver\Exception\ExceptionInterface; -use Yceruto\FormFlowBundle\Form\Flow\ActionButtonBuilder; -use Yceruto\FormFlowBundle\Form\Flow\ActionButtonTypeInterface; +use Yceruto\FormFlowBundle\Form\Flow\FlowButtonBuilder; +use Yceruto\FormFlowBundle\Form\Flow\FlowButtonTypeInterface; use Yceruto\FormFlowBundle\Form\Flow\FormFlowBuilder; use Yceruto\FormFlowBundle\Form\Flow\FormFlowBuilderInterface; use Yceruto\FormFlowBundle\Form\Flow\FormFlowTypeInterface; @@ -41,8 +41,8 @@ public function createBuilder(FormFactoryInterface $factory, string $name, array protected function newBuilder(string $name, ?string $dataClass, FormFactoryInterface $factory, array $options): FormBuilderInterface { - if ($this->innerType instanceof ActionButtonTypeInterface) { - return new ActionButtonBuilder($name, $options); + if ($this->innerType instanceof FlowButtonTypeInterface) { + return new FlowButtonBuilder($name, $options); } if ($this->innerType instanceof FormFlowTypeInterface) { diff --git a/tests/Integration/App/FormFlowBasic/Form/Type/MultistepType.php b/tests/Integration/App/FormFlowBasic/Form/Type/MultistepType.php index 44f0905..6c92948 100644 --- a/tests/Integration/App/FormFlowBasic/Form/Type/MultistepType.php +++ b/tests/Integration/App/FormFlowBasic/Form/Type/MultistepType.php @@ -2,25 +2,21 @@ namespace Yceruto\FormFlowBundle\Tests\Integration\App\FormFlowBasic\Form\Type; -use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; -use Yceruto\FormFlowBundle\Form\Extension\Core\Type\FormFlowNavigatorType; use Yceruto\FormFlowBundle\Form\Flow\AbstractFlowType; use Yceruto\FormFlowBundle\Form\Flow\FormFlowBuilderInterface; +use Yceruto\FormFlowBundle\Form\Flow\Type\FlowNavigatorType; use Yceruto\FormFlowBundle\Tests\Integration\App\FormFlowBasic\Form\Data\MultistepDto; class MultistepType extends AbstractFlowType { - /** - * @param FormFlowBuilderInterface $builder - */ - public function buildForm(FormBuilderInterface $builder, array $options): void + public function buildFormFlow(FormFlowBuilderInterface $builder, array $options): void { $builder->addStep('step1', Step1Type::class); $builder->addStep('step2', Step2Type::class); $builder->addStep('step3', Step3Type::class); - $builder->add('navigator', FormFlowNavigatorType::class); + $builder->add('navigator', FlowNavigatorType::class); } public function configureOptions(OptionsResolver $resolver): void diff --git a/tests/Integration/App/FormFlowBasic/Form/Type/Step2Type.php b/tests/Integration/App/FormFlowBasic/Form/Type/Step2Type.php index dad22bd..c86f083 100644 --- a/tests/Integration/App/FormFlowBasic/Form/Type/Step2Type.php +++ b/tests/Integration/App/FormFlowBasic/Form/Type/Step2Type.php @@ -5,7 +5,7 @@ use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; -use Yceruto\FormFlowBundle\Form\Extension\Core\Type\FormFlowActionType; +use Yceruto\FormFlowBundle\Form\Flow\Type\FlowNextType; use Yceruto\FormFlowBundle\Tests\Integration\App\FormFlowBasic\Form\Data\MultistepDto; class Step2Type extends AbstractType @@ -15,8 +15,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void $builder->add('field21'); $builder->add('field22'); - $builder->add('skip', FormFlowActionType::class, [ - 'action' => 'next', + $builder->add('skip', FlowNextType::class, [ 'clear_submission' => true, ]); } diff --git a/tests/Integration/App/FormFlowBasic/Output/step2.html b/tests/Integration/App/FormFlowBasic/Output/step2.html index bdf8c2b..7681bc2 100644 --- a/tests/Integration/App/FormFlowBasic/Output/step2.html +++ b/tests/Integration/App/FormFlowBasic/Output/step2.html @@ -1 +1 @@ -
+
diff --git a/tests/Integration/App/FormFlowBasic/Output/step3.html b/tests/Integration/App/FormFlowBasic/Output/step3.html index 9ac4e02..d4f5dbc 100644 --- a/tests/Integration/App/FormFlowBasic/Output/step3.html +++ b/tests/Integration/App/FormFlowBasic/Output/step3.html @@ -1 +1 @@ -
+
diff --git a/tests/Integration/FormFlowBasicTest.php b/tests/Integration/FormFlowBasicTest.php index 6ce8f13..6ed6154 100644 --- a/tests/Integration/FormFlowBasicTest.php +++ b/tests/Integration/FormFlowBasicTest.php @@ -64,8 +64,8 @@ public function testGoBackToPreviousStep(): void self::assertStringContainsString('>Step3<', $crawler->html()); - $crawler = $client->submit($crawler->selectButton('Back')->form(), [ - 'multistep[navigator][back]' => '', + $crawler = $client->submit($crawler->selectButton('Previous')->form(), [ + 'multistep[navigator][previous]' => '', ]); self::assertStringContainsString('>Step2<', $crawler->html()); @@ -107,8 +107,8 @@ public function testGoBackToEarlierStep(): void self::assertStringContainsString('>Step3<', $crawler->html()); - $crawler = $client->submit($crawler->selectButton('Back')->form(), [ - 'multistep[navigator][back]' => 'step1', + $crawler = $client->submit($crawler->selectButton('Previous')->form(), [ + 'multistep[navigator][previous]' => 'step1', ]); self::assertStringContainsString('>Step1<', $crawler->html()); @@ -164,8 +164,8 @@ public function testSkipStep(): void self::assertSame(200, $client->getInternalResponse()->getStatusCode()); self::assertStringContainsString('>Step3<', $crawler->html()); - $crawler = $client->submit($crawler->selectButton('Back')->form(), [ - 'multistep[navigator][back]' => '', + $crawler = $client->submit($crawler->selectButton('Previous')->form(), [ + 'multistep[navigator][previous]' => '', ]); self::assertStringContainsString('>Step2<', $crawler->html());