mirror of
https://github.com/symfony/form.git
synced 2026-03-24 00:02:23 +01:00
244 lines
6.8 KiB
PHP
244 lines
6.8 KiB
PHP
<?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;
|
|
}
|
|
}
|