vendor/craue/formflow-bundle/Form/FormFlow.php line 772

Open in your IDE?
  1. <?php
  2. namespace Craue\FormFlowBundle\Form;
  3. use Craue\FormFlowBundle\Event\FlowExpiredEvent;
  4. use Craue\FormFlowBundle\Event\FormFlowEvent;
  5. use Craue\FormFlowBundle\Event\GetStepsEvent;
  6. use Craue\FormFlowBundle\Event\PostBindFlowEvent;
  7. use Craue\FormFlowBundle\Event\PostBindRequestEvent;
  8. use Craue\FormFlowBundle\Event\PostBindSavedDataEvent;
  9. use Craue\FormFlowBundle\Event\PostValidateEvent;
  10. use Craue\FormFlowBundle\Event\PreBindEvent;
  11. use Craue\FormFlowBundle\Event\PreviousStepInvalidEvent;
  12. use Craue\FormFlowBundle\Exception\AllStepsSkippedException;
  13. use Craue\FormFlowBundle\Exception\InvalidTypeException;
  14. use Craue\FormFlowBundle\Storage\DataManagerInterface;
  15. use Craue\FormFlowBundle\Util\StringUtil;
  16. use Symfony\Component\EventDispatcher\EventDispatcherInterface;
  17. use Symfony\Component\Form\Extension\Core\Type\FormType;
  18. use Symfony\Component\Form\FormFactoryInterface;
  19. use Symfony\Component\Form\FormInterface;
  20. use Symfony\Component\HttpFoundation\Request;
  21. use Symfony\Component\HttpFoundation\RequestStack;
  22. use Symfony\Component\Validator\Constraints\GroupSequence;
  23. /**
  24.  * @author Christian Raue <christian.raue@gmail.com>
  25.  * @author Marcus Stöhr <dafish@soundtrack-board.de>
  26.  * @author Toni Uebernickel <tuebernickel@gmail.com>
  27.  * @copyright 2011-2024 Christian Raue
  28.  * @license http://opensource.org/licenses/mit-license.php MIT License
  29.  */
  30. abstract class FormFlow implements FormFlowInterface {
  31.     const TRANSITION_BACK 'back';
  32.     const TRANSITION_RESET 'reset';
  33.     /**
  34.      * @var FormFactoryInterface
  35.      */
  36.     protected $formFactory;
  37.     /**
  38.      * @var DataManagerInterface
  39.      */
  40.     protected $dataManager;
  41.     /**
  42.      * @var EventDispatcherInterface|null
  43.      */
  44.     protected $eventDispatcher null;
  45.     /**
  46.      * @var string|null
  47.      */
  48.     protected $transition;
  49.     /**
  50.      * @var bool
  51.      */
  52.     protected $revalidatePreviousSteps true;
  53.     /**
  54.      * @var bool
  55.      */
  56.     protected $allowDynamicStepNavigation false;
  57.     /**
  58.      * @var bool If file uploads should be handled by serializing them into the storage.
  59.      */
  60.     protected $handleFileUploads true;
  61.     /**
  62.      * @var string|null Directory for storing temporary files while handling uploads. If <code>null</code>, the system's default will be used.
  63.      */
  64.     protected $handleFileUploadsTempDir null;
  65.     /**
  66.      * @var bool
  67.      */
  68.     protected $allowRedirectAfterSubmit false;
  69.     /**
  70.      * @var string
  71.      */
  72.     protected $dynamicStepNavigationInstanceParameter 'instance';
  73.     /**
  74.      * @var string
  75.      */
  76.     protected $dynamicStepNavigationStepParameter 'step';
  77.     /**
  78.      * @var RequestStack
  79.      */
  80.     private $requestStack;
  81.     /**
  82.      * @var string|null Is only null if not yet initialized.
  83.      */
  84.     private $id null;
  85.     /**
  86.      * @var string|null Is only null if not yet initialized.
  87.      */
  88.     private $instanceKey null;
  89.     /**
  90.      * @var string|null Is only null if not yet initialized.
  91.      */
  92.     private $instanceId null;
  93.     /**
  94.      * @var string|null Is only null if not yet initialized.
  95.      */
  96.     private $formStepKey null;
  97.     /**
  98.      * @var string|null Is only null if not yet initialized.
  99.      */
  100.     private $formTransitionKey null;
  101.     /**
  102.      * @var string|null Is only null if not yet initialized.
  103.      */
  104.     private $validationGroupPrefix null;
  105.     /**
  106.      * @var StepInterface[]|null Is only null if not yet initialized.
  107.      */
  108.     private $steps null;
  109.     /**
  110.      * @var int|null Is only null if not yet initialized.
  111.      */
  112.     private $stepCount null;
  113.     /**
  114.      * @var string[]|null Is only null if not yet initialized.
  115.      */
  116.     private $stepLabels null;
  117.     /**
  118.      * @var mixed|null Is only null if not yet initialized.
  119.      */
  120.     private $formData null;
  121.     /**
  122.      * @var int|null Is only null if not yet initialized.
  123.      */
  124.     private $currentStepNumber null;
  125.     /**
  126.      * @var FormInterface[]
  127.      */
  128.     private $stepForms = [];
  129.     /**
  130.      * Options applied to forms of all steps.
  131.      * @var array
  132.      */
  133.     private $genericFormOptions = [];
  134.     /**
  135.      * Flow was determined to be expired.
  136.      * @var bool
  137.      */
  138.     private $expired false;
  139.     /**
  140.      * {@inheritDoc}
  141.      */
  142.     public function setFormFactory(FormFactoryInterface $formFactory) {
  143.         $this->formFactory $formFactory;
  144.     }
  145.     /**
  146.      * {@inheritDoc}
  147.      */
  148.     public function setRequestStack(RequestStack $requestStack) {
  149.         $this->requestStack $requestStack;
  150.     }
  151.     /**
  152.      * @return Request
  153.      * @throws \RuntimeException If the request is not available.
  154.      */
  155.     public function getRequest() {
  156.         $currentRequest $this->requestStack->getCurrentRequest();
  157.         if ($currentRequest === null) {
  158.             throw new \RuntimeException('The request is not available.');
  159.         }
  160.         return $currentRequest;
  161.     }
  162.     /**
  163.      * {@inheritDoc}
  164.      */
  165.     public function setDataManager(DataManagerInterface $dataManager) {
  166.         $this->dataManager $dataManager;
  167.     }
  168.     /**
  169.      * {@inheritDoc}
  170.      */
  171.     public function getDataManager() {
  172.         return $this->dataManager;
  173.     }
  174.     /**
  175.      * {@inheritDoc}
  176.      */
  177.     public function setEventDispatcher(EventDispatcherInterface $eventDispatcher) {
  178.         $this->eventDispatcher $eventDispatcher;
  179.     }
  180.     public function setId($id) {
  181.         $this->id $id;
  182.     }
  183.     /**
  184.      * {@inheritDoc}
  185.      */
  186.     public function getId() {
  187.         if ($this->id === null) {
  188.             $this->id 'flow_' $this->getName();
  189.         }
  190.         return $this->id;
  191.     }
  192.     /**
  193.      * {@inheritDoc}
  194.      */
  195.     public function getName() {
  196.         return StringUtil::fqcnToFlowName(get_class($this));
  197.     }
  198.     public function setInstanceKey($instanceKey) {
  199.         $this->instanceKey $instanceKey;
  200.     }
  201.     public function getInstanceKey() {
  202.         if ($this->instanceKey === null) {
  203.             $this->instanceKey $this->getId() . '_instance';
  204.         }
  205.         return $this->instanceKey;
  206.     }
  207.     public function setInstanceId($instanceId) {
  208.         $this->instanceId $instanceId;
  209.     }
  210.     /**
  211.      * {@inheritDoc}
  212.      */
  213.     public function getInstanceId() {
  214.         if ($this->instanceId === null) {
  215.             $this->instanceId $this->getId();
  216.         }
  217.         return $this->instanceId;
  218.     }
  219.     public function setFormStepKey($formStepKey) {
  220.         $this->formStepKey $formStepKey;
  221.     }
  222.     public function getFormStepKey() {
  223.         if ($this->formStepKey === null) {
  224.             $this->formStepKey $this->getId() . '_step';
  225.         }
  226.         return $this->formStepKey;
  227.     }
  228.     public function setFormTransitionKey($formTransitionKey) {
  229.         $this->formTransitionKey $formTransitionKey;
  230.     }
  231.     public function getFormTransitionKey() {
  232.         if ($this->formTransitionKey === null) {
  233.             $this->formTransitionKey $this->getId() . '_transition';
  234.         }
  235.         return $this->formTransitionKey;
  236.     }
  237.     public function setValidationGroupPrefix($validationGroupPrefix) {
  238.         $this->validationGroupPrefix $validationGroupPrefix;
  239.     }
  240.     public function getValidationGroupPrefix() {
  241.         if ($this->validationGroupPrefix === null) {
  242.             $this->validationGroupPrefix $this->getId() . '_step';
  243.         }
  244.         return $this->validationGroupPrefix;
  245.     }
  246.     /**
  247.      * {@inheritDoc}
  248.      */
  249.     public function getStepCount() {
  250.         if ($this->stepCount === null) {
  251.             $this->stepCount count($this->getSteps());
  252.         }
  253.         return $this->stepCount;
  254.     }
  255.     /**
  256.      * {@inheritDoc}
  257.      */
  258.     public function getFormData() {
  259.         if ($this->formData === null) {
  260.             throw new \RuntimeException('Form data has not been evaluated yet and thus cannot be accessed.');
  261.         }
  262.         return $this->formData;
  263.     }
  264.     /**
  265.      * {@inheritDoc}
  266.      */
  267.     public function getCurrentStepNumber() {
  268.         if ($this->currentStepNumber === null) {
  269.             throw new \RuntimeException('The current step has not been determined yet and thus cannot be accessed.');
  270.         }
  271.         return $this->currentStepNumber;
  272.     }
  273.     public function setRevalidatePreviousSteps($revalidatePreviousSteps) {
  274.         $this->revalidatePreviousSteps = (bool) $revalidatePreviousSteps;
  275.     }
  276.     /**
  277.      * {@inheritDoc}
  278.      */
  279.     public function isRevalidatePreviousSteps() {
  280.         return $this->revalidatePreviousSteps;
  281.     }
  282.     public function setAllowDynamicStepNavigation($allowDynamicStepNavigation) {
  283.         $this->allowDynamicStepNavigation = (bool) $allowDynamicStepNavigation;
  284.     }
  285.     /**
  286.      * {@inheritDoc}
  287.      */
  288.     public function isAllowDynamicStepNavigation() {
  289.         return $this->allowDynamicStepNavigation;
  290.     }
  291.     public function setHandleFileUploads($handleFileUploads) {
  292.         $this->handleFileUploads = (bool) $handleFileUploads;
  293.     }
  294.     /**
  295.      * {@inheritDoc}
  296.      */
  297.     public function isHandleFileUploads() {
  298.         return $this->handleFileUploads;
  299.     }
  300.     public function setHandleFileUploadsTempDir($handleFileUploadsTempDir) {
  301.         $this->handleFileUploadsTempDir $handleFileUploadsTempDir !== null ? (string) $handleFileUploadsTempDir null;
  302.     }
  303.     /**
  304.      * {@inheritDoc}
  305.      */
  306.     public function getHandleFileUploadsTempDir() {
  307.         return $this->handleFileUploadsTempDir;
  308.     }
  309.     public function setAllowRedirectAfterSubmit($allowRedirectAfterSubmit) {
  310.         $this->allowRedirectAfterSubmit = (bool) $allowRedirectAfterSubmit;
  311.     }
  312.     /**
  313.      * {@inheritDoc}
  314.      */
  315.     public function isAllowRedirectAfterSubmit() {
  316.         return $this->allowRedirectAfterSubmit;
  317.     }
  318.     public function setDynamicStepNavigationInstanceParameter($dynamicStepNavigationInstanceParameter) {
  319.         $this->dynamicStepNavigationInstanceParameter $dynamicStepNavigationInstanceParameter;
  320.     }
  321.     public function getDynamicStepNavigationInstanceParameter() {
  322.         return $this->dynamicStepNavigationInstanceParameter;
  323.     }
  324.     public function setDynamicStepNavigationStepParameter($dynamicStepNavigationStepParameter) {
  325.         $this->dynamicStepNavigationStepParameter $dynamicStepNavigationStepParameter;
  326.     }
  327.     public function getDynamicStepNavigationStepParameter() {
  328.         return $this->dynamicStepNavigationStepParameter;
  329.     }
  330.     public function setGenericFormOptions(array $genericFormOptions) {
  331.         $this->genericFormOptions $genericFormOptions;
  332.     }
  333.     public function getGenericFormOptions() {
  334.         return $this->genericFormOptions;
  335.     }
  336.     /**
  337.      * {@inheritDoc}
  338.      */
  339.     public function isStepSkipped($stepNumber) {
  340.         return $this->getStep($stepNumber)->isSkipped();
  341.     }
  342.     /**
  343.      * @param int $stepNumber Assumed step to which skipped steps shall be applied to.
  344.      * @param int $direction Either 1 (to skip forwards) or -1 (to skip backwards).
  345.      * @param int $boundsReached Internal counter to avoid endlessly bouncing back and forth.
  346.      * @return int Target step number with skipping applied.
  347.      * @throws \InvalidArgumentException If the value of <code>$direction</code> is invalid.
  348.      */
  349.     protected function applySkipping($stepNumber$direction 1$boundsReached 0) {
  350.         if ($direction !== && $direction !== -1) {
  351.             throw new \InvalidArgumentException(sprintf('Argument of either -1 or 1 expected, "%s" given.'$direction));
  352.         }
  353.         $stepNumber $this->ensureStepNumberRange($stepNumber);
  354.         if ($this->isStepSkipped($stepNumber)) {
  355.             $stepNumber += $direction;
  356.             // change direction if outer bounds are reached
  357.             if ($direction === && $stepNumber $this->getStepCount()) {
  358.                 $direction = -1;
  359.                 ++$boundsReached;
  360.             } elseif ($direction === -&& $stepNumber 1) {
  361.                 $direction 1;
  362.                 ++$boundsReached;
  363.             }
  364.             if ($boundsReached 2) {
  365.                 throw new AllStepsSkippedException();
  366.             }
  367.             return $this->applySkipping($stepNumber$direction$boundsReached);
  368.         }
  369.         return $stepNumber;
  370.     }
  371.     /**
  372.      * {@inheritDoc}
  373.      */
  374.     public function reset() {
  375.         $this->dataManager->drop($this);
  376.         $this->currentStepNumber $this->getFirstStepNumber();
  377.         // re-evaluate to not keep steps marked as skipped when resetting
  378.         foreach ($this->getSteps() as $step) {
  379.             $step->evaluateSkipping($this->currentStepNumber$this);
  380.         }
  381.     }
  382.     /**
  383.      * {@inheritDoc}
  384.      */
  385.     public function getFirstStepNumber() {
  386.         return $this->applySkipping(1);
  387.     }
  388.     /**
  389.      * {@inheritDoc}
  390.      */
  391.     public function getLastStepNumber() {
  392.         return $this->applySkipping($this->getStepCount(), -1);
  393.     }
  394.     /**
  395.      * {@inheritDoc}
  396.      */
  397.     public function nextStep() {
  398.         $currentStepNumber $this->currentStepNumber 1;
  399.         foreach ($this->getSteps() as $step) {
  400.             $step->evaluateSkipping($currentStepNumber$this);
  401.         }
  402.         // There is no "next" step as the target step exceeds the actual step count.
  403.         if ($currentStepNumber $this->getLastStepNumber()) {
  404.             return false;
  405.         }
  406.         $currentStepNumber $this->applySkipping($currentStepNumber);
  407.         if ($currentStepNumber <= $this->getStepCount()) {
  408.             $this->currentStepNumber $currentStepNumber;
  409.             return true;
  410.         }
  411.         return false// should never be reached, but just in case
  412.     }
  413.     /**
  414.      * {@inheritDoc}
  415.      */
  416.     public function isStepDone($stepNumber) {
  417.         if ($this->isStepSkipped($stepNumber)) {
  418.             return true;
  419.         }
  420.         return array_key_exists($stepNumber$this->retrieveStepData());
  421.     }
  422.     public function getRequestedTransition() {
  423.         if (!is_string($this->transition) || $this->transition === '') {
  424.             $this->transition strtolower($this->getRequest()->request->get($this->getFormTransitionKey(), ''));
  425.         }
  426.         return $this->transition;
  427.     }
  428.     protected function getRequestedStepNumber() {
  429.         $defaultStepNumber 1;
  430.         $request $this->getRequest();
  431.         switch ($request->getMethod()) {
  432.             case 'PUT':
  433.             case 'POST':
  434.                 return intval($request->request->get($this->getFormStepKey(), $defaultStepNumber));
  435.             case 'GET':
  436.                 return $this->allowDynamicStepNavigation || $this->allowRedirectAfterSubmit ?
  437.                         intval($request->get($this->dynamicStepNavigationStepParameter$defaultStepNumber)) :
  438.                         $defaultStepNumber;
  439.         }
  440.         return $defaultStepNumber;
  441.     }
  442.     /**
  443.      * Finds out which step is the current one.
  444.      * @return int
  445.      */
  446.     protected function determineCurrentStepNumber() {
  447.         $requestedStepNumber $this->getRequestedStepNumber();
  448.         if ($this->getRequestedTransition() === self::TRANSITION_BACK) {
  449.             --$requestedStepNumber;
  450.         }
  451.         $requestedStepNumber $this->ensureStepNumberRange($requestedStepNumber);
  452.         $requestedStepNumber $this->refineCurrentStepNumber($requestedStepNumber);
  453.         if ($this->getRequestedTransition() === self::TRANSITION_BACK) {
  454.             $requestedStepNumber $this->applySkipping($requestedStepNumber, -1);
  455.             // re-evaluate to not keep following steps marked as skipped (after skipping them while going back)
  456.             foreach ($this->getSteps() as $step) {
  457.                 $step->evaluateSkipping($requestedStepNumber$this);
  458.             }
  459.         } else {
  460.             $requestedStepNumber $this->applySkipping($requestedStepNumber);
  461.         }
  462.         return $requestedStepNumber;
  463.     }
  464.     /**
  465.      * Ensures that the step number is within the range of defined steps to avoid a possible OutOfBoundsException.
  466.      * @param int $stepNumber
  467.      * @return int
  468.      */
  469.     private function ensureStepNumberRange($stepNumber) {
  470.         return max(min($stepNumber$this->getStepCount()), 1);
  471.     }
  472.     /**
  473.      * Refines the current step number by evaluating and considering skipped steps.
  474.      * @param int $refinedStepNumber
  475.      * @return int
  476.      */
  477.     protected function refineCurrentStepNumber($refinedStepNumber) {
  478.         foreach ($this->getSteps() as $step) {
  479.             $step->evaluateSkipping($refinedStepNumber$this);
  480.         }
  481.         return $refinedStepNumber;
  482.     }
  483.     /**
  484.      * {@inheritDoc}
  485.      */
  486.     public function bind($formData) {
  487.         $this->setInstanceId($this->determineInstanceId());
  488.         if ($this->hasListeners(FormFlowEvents::PRE_BIND)) {
  489.             $this->dispatchEvent(new PreBindEvent($this), FormFlowEvents::PRE_BIND);
  490.         }
  491.         $this->formData $formData;
  492.         $this->bindFlow();
  493.         if ($this->hasListeners(FormFlowEvents::POST_BIND_FLOW)) {
  494.             $this->dispatchEvent(new PostBindFlowEvent($this$this->formData), FormFlowEvents::POST_BIND_FLOW);
  495.         }
  496.         if (!$this->dataManager->exists($this)) {
  497.             // initialize storage slot
  498.             $this->dataManager->save($this, []);
  499.         }
  500.     }
  501.     protected function determineInstanceId() {
  502.         $request $this->getRequest();
  503.         $instanceId null;
  504.         if ($this->allowDynamicStepNavigation || $this->allowRedirectAfterSubmit) {
  505.             $instanceId $request->get($this->getDynamicStepNavigationInstanceParameter());
  506.         }
  507.         if ($instanceId === null) {
  508.             $instanceId $request->request->get($this->getInstanceKey());
  509.         }
  510.         $instanceIdLength 10;
  511.         if ($instanceId === null || !StringUtil::isRandomString($instanceId$instanceIdLength)) {
  512.             $instanceId StringUtil::generateRandomString($instanceIdLength);
  513.         }
  514.         return $instanceId;
  515.     }
  516.     protected function bindFlow() {
  517.         $request $this->getRequest();
  518.         $reset false;
  519.         if (!$this->allowDynamicStepNavigation && !$this->allowRedirectAfterSubmit && $request->isMethod('GET')) {
  520.             $reset true;
  521.         }
  522.         if ($this->getRequestedTransition() === self::TRANSITION_RESET) {
  523.             $reset true;
  524.         }
  525.         if (in_array($request->getMethod(), ['POST''PUT'], true) && $request->get($this->getFormStepKey()) !== null && !$this->dataManager->exists($this)) {
  526.             // flow is expired, drop posted data and reset
  527.             $request->request->replace();
  528.             $reset true;
  529.             $this->expired true;
  530.             // Regenerate instance ID so resubmits of the form will continue to give error. Otherwise, submitting
  531.             // the new form, then backing up to the old form won't give the error.
  532.             $this->setInstanceId($this->determineInstanceId());
  533.         }
  534.         if (!$reset) {
  535.             $this->applyDataFromSavedSteps();
  536.         }
  537.         $requestedStepNumber $this->determineCurrentStepNumber();
  538.         if ($reset) {
  539.             $this->reset();
  540.             return;
  541.         }
  542.         // ensure that the requested step fits the current progress
  543.         if ($requestedStepNumber $this->getFirstStepNumber()) {
  544.             for ($step $this->getFirstStepNumber(); $step $requestedStepNumber; ++$step) {
  545.                 if (!$this->isStepDone($step)) {
  546.                     $this->reset();
  547.                     return;
  548.                 }
  549.             }
  550.         }
  551.         $this->currentStepNumber $requestedStepNumber;
  552.         if (!$this->allowDynamicStepNavigation && $this->getRequestedTransition() === self::TRANSITION_BACK) {
  553.             /*
  554.              * Don't invalidate data for the current step to properly show the filled out form for that step after
  555.              * pressing "back" and refreshing the page. Otherwise, the form would be blank since the data has already
  556.              * been invalidated previously.
  557.              */
  558.             $this->invalidateStepData($this->currentStepNumber 1);
  559.         }
  560.     }
  561.     /**
  562.      * {@inheritDoc}
  563.      */
  564.     public function saveCurrentStepData(FormInterface $form) {
  565.         $stepData $this->retrieveStepData();
  566.         $request $this->getRequest();
  567.         $formName $form->getName();
  568.         if (!\class_exists('Symfony\Component\HttpFoundation\InputBag')) {
  569.             // TODO remove as soon as Symfony >= 5.1 is required
  570.             $currentStepData $request->request->get($formName, []);
  571.         } else {
  572.             $currentStepData $request->request->all($formName);
  573.         }
  574.         if ($this->handleFileUploads) {
  575.             $currentStepData array_replace_recursive($currentStepData$request->files->get($formName, []));
  576.         }
  577.         $stepData[$this->getCurrentStepNumber()] = $currentStepData;
  578.         $this->saveStepData($stepData);
  579.     }
  580.     /**
  581.      * Invalidates data for steps >= $fromStepNumber.
  582.      * @param int $fromStepNumber
  583.      */
  584.     public function invalidateStepData($fromStepNumber) {
  585.         $stepData $this->retrieveStepData();
  586.         for ($step $fromStepNumber$stepCount $this->getStepCount(); $step $stepCount; ++$step) {
  587.             unset($stepData[$step]);
  588.         }
  589.         $this->saveStepData($stepData);
  590.     }
  591.     /**
  592.      * Updates form data class with previously saved form data of all steps.
  593.      */
  594.     protected function applyDataFromSavedSteps() {
  595.         $stepData $this->retrieveStepData();
  596.         $this->stepForms = [];
  597.         $options = [];
  598.         if (!$this->revalidatePreviousSteps) {
  599.             $options['validation_groups'] = false// disable validation
  600.         }
  601.         foreach ($this->getSteps() as $step) {
  602.             $stepNumber $step->getNumber();
  603.             if (array_key_exists($stepNumber$stepData)) {
  604.                 $stepForm $this->createFormForStep($stepNumber$options);
  605.                 $stepForm->submit($stepData[$stepNumber]); // the form is validated here
  606.                 if ($this->revalidatePreviousSteps) {
  607.                     $this->stepForms[$stepNumber] = $stepForm;
  608.                 }
  609.                 if ($this->hasListeners(FormFlowEvents::POST_BIND_SAVED_DATA)) {
  610.                     $this->dispatchEvent(new PostBindSavedDataEvent($this$this->formData$stepNumber), FormFlowEvents::POST_BIND_SAVED_DATA);
  611.                 }
  612.             }
  613.         }
  614.     }
  615.     /**
  616.      * {@inheritDoc}
  617.      */
  618.     public function createForm() {
  619.         $form $this->createFormForStep($this->currentStepNumber);
  620.         if ($this->expired && $this->hasListeners(FormFlowEvents::FLOW_EXPIRED)) {
  621.             $this->dispatchEvent(new FlowExpiredEvent($this$form), FormFlowEvents::FLOW_EXPIRED);
  622.         }
  623.         return $form;
  624.     }
  625.     public function getFormOptions($step, array $options = []) {
  626.         // override options in a specific order
  627.         $options array_merge(
  628.             $this->getGenericFormOptions(),
  629.             $this->getStep($step)->getFormOptions(),
  630.             $options
  631.         );
  632.         // add the generated step-based validation group, unless it's explicitly set to false, a closure, or a GroupSequence
  633.         if (!array_key_exists('validation_groups'$options)) {
  634.             $options['validation_groups'] = [$this->getValidationGroupPrefix() . $step];
  635.         } else {
  636.             $vg $options['validation_groups'];
  637.             if ($vg !== false && !is_a($vg'Closure') && !$vg instanceof GroupSequence) {
  638.                 $options['validation_groups'] = array_merge(
  639.                     [$this->getValidationGroupPrefix() . $step],
  640.                     (array) $vg
  641.                 );
  642.             }
  643.         }
  644.         $options['flow_instance'] = $this->getInstanceId();
  645.         $options['flow_instance_key'] = $this->getInstanceKey();
  646.         $options['flow_step'] = $step;
  647.         $options['flow_step_key'] = $this->getFormStepKey();
  648.         return $options;
  649.     }
  650.     /**
  651.      * {@inheritDoc}
  652.      */
  653.     public function getStep($stepNumber) {
  654.         if (!is_int($stepNumber)) {
  655.             throw new InvalidTypeException($stepNumber'int');
  656.         }
  657.         $steps $this->getSteps();
  658.         $index $stepNumber 1;
  659.         if (array_key_exists($index$steps)) {
  660.             return $steps[$index];
  661.         }
  662.         throw new \OutOfBoundsException(sprintf('The step "%d" does not exist.'$stepNumber));
  663.     }
  664.     /**
  665.      * {@inheritDoc}
  666.      */
  667.     public function getSteps() {
  668.         // The steps have been loaded already.
  669.         if ($this->steps !== null) {
  670.             return $this->steps;
  671.         }
  672.         if ($this->hasListeners(FormFlowEvents::GET_STEPS)) {
  673.             $event = new GetStepsEvent($this);
  674.             $this->dispatchEvent($eventFormFlowEvents::GET_STEPS);
  675.             // A listener has provided the steps for this flow.
  676.             if ($event->isPropagationStopped()) {
  677.                 $this->steps $event->getSteps();
  678.                 return $this->steps;
  679.             }
  680.         }
  681.         // There are either no listeners on the event at all or none created the steps for this flow, so load from configuration.
  682.         $this->steps $this->createStepsFromConfig($this->loadStepsConfig());
  683.         return $this->steps;
  684.     }
  685.     /**
  686.      * {@inheritDoc}
  687.      */
  688.     public function getStepLabels() {
  689.         if ($this->stepLabels === null) {
  690.             $stepLabels = [];
  691.             foreach ($this->getSteps() as $step) {
  692.                 $stepLabels[] = $step->getLabel();
  693.             }
  694.             $this->stepLabels $stepLabels;
  695.         }
  696.         return $this->stepLabels;
  697.     }
  698.     /**
  699.      * {@inheritDoc}
  700.      */
  701.     public function getCurrentStepLabel() {
  702.         return $this->getStep($this->currentStepNumber)->getLabel();
  703.     }
  704.     /**
  705.      * {@inheritDoc}
  706.      */
  707.     public function isValid(FormInterface $form) {
  708.         $request $this->getRequest();
  709.         if (in_array($request->getMethod(), ['POST''PUT'], true) && !in_array($this->getRequestedTransition(), [
  710.             self::TRANSITION_BACK,
  711.             self::TRANSITION_RESET,
  712.         ], true)) {
  713.             $form->handleRequest($request);
  714.             if (!$form->isSubmitted()) {
  715.                 return false;
  716.             }
  717.             if ($this->hasListeners(FormFlowEvents::POST_BIND_REQUEST)) {
  718.                 $this->dispatchEvent(new PostBindRequestEvent($this$form->getData(), $this->currentStepNumber), FormFlowEvents::POST_BIND_REQUEST);
  719.             }
  720.             if ($this->revalidatePreviousSteps) {
  721.                 // check if forms of previous steps are still valid
  722.                 foreach ($this->stepForms as $stepNumber => $stepForm) {
  723.                     // ignore form of the current step
  724.                     if ($this->currentStepNumber === $stepNumber) {
  725.                         break;
  726.                     }
  727.                     // ignore forms of skipped steps
  728.                     if ($this->isStepSkipped($stepNumber)) {
  729.                         break;
  730.                     }
  731.                     if (!$stepForm->isValid()) {
  732.                         if ($this->hasListeners(FormFlowEvents::PREVIOUS_STEP_INVALID)) {
  733.                             $this->dispatchEvent(new PreviousStepInvalidEvent($this$form$stepNumber), FormFlowEvents::PREVIOUS_STEP_INVALID);
  734.                         }
  735.                         return false;
  736.                     }
  737.                 }
  738.             }
  739.             if ($form->isValid()) {
  740.                 if ($this->hasListeners(FormFlowEvents::POST_VALIDATE)) {
  741.                     $this->dispatchEvent(new PostValidateEvent($this$form->getData()), FormFlowEvents::POST_VALIDATE);
  742.                 }
  743.                 return true;
  744.             }
  745.         }
  746.         return false;
  747.     }
  748.     /**
  749.      * @param FormInterface $submittedForm
  750.      * @return bool If a redirection should be performed.
  751.      */
  752.     public function redirectAfterSubmit(FormInterface $submittedForm) {
  753.         if ($this->allowRedirectAfterSubmit && in_array($this->getRequest()->getMethod(), ['POST''PUT'], true)) {
  754.             switch ($this->getRequestedTransition()) {
  755.                 case self::TRANSITION_BACK:
  756.                 case self::TRANSITION_RESET:
  757.                     return true;
  758.                 default:
  759.                     // redirect after submit only if there are no errors for the submitted form
  760.                     return $submittedForm->isSubmitted() && $submittedForm->isValid();
  761.             }
  762.         }
  763.         return false;
  764.     }
  765.     /**
  766.      * Creates the form for the given step number.
  767.      * @param int $stepNumber
  768.      * @param array $options
  769.      * @return FormInterface
  770.      */
  771.     protected function createFormForStep($stepNumber, array $options = []) {
  772.         $formType $this->getStep($stepNumber)->getFormType();
  773.         $options $this->getFormOptions($stepNumber$options);
  774.         if ($formType === null) {
  775.             $formType FormType::class;
  776.         }
  777.         return $this->formFactory->create($formType$this->formData$options);
  778.     }
  779.     /**
  780.      * Creates all steps from the given configuration.
  781.      * @param array $stepsConfig
  782.      * @return StepInterface[] Value with index 0 is step 1.
  783.      */
  784.     public function createStepsFromConfig(array $stepsConfig) {
  785.         $steps = [];
  786.         // fix array indexes not starting at 0
  787.         $stepsConfig array_values($stepsConfig);
  788.         foreach ($stepsConfig as $index => $stepConfig) {
  789.             $steps[] = Step::createFromConfig($index 1$stepConfig);
  790.         }
  791.         return $steps;
  792.     }
  793.     /**
  794.      * Defines the configuration for all steps of this flow.
  795.      * @return array
  796.      */
  797.     protected function loadStepsConfig() {
  798.         return [];
  799.     }
  800.     protected function retrieveStepData() {
  801.         return $this->dataManager->load($this);
  802.     }
  803.     protected function saveStepData(array $data) {
  804.         $this->dataManager->save($this$data);
  805.     }
  806.     /**
  807.      * @param string $eventName
  808.      * @return bool
  809.      */
  810.     protected function hasListeners($eventName) {
  811.         return $this->eventDispatcher !== null && $this->eventDispatcher->hasListeners($eventName);
  812.     }
  813.     /**
  814.      * @param FormFlowEvent $event
  815.      * @param string $eventName
  816.      */
  817.     private function dispatchEvent($event$eventName) {
  818.         $this->eventDispatcher->dispatch($event$eventName);
  819.     }
  820.     /**
  821.      * {@inheritDoc}
  822.      */
  823.     public function getStepsDone() {
  824.         $stepsDone = [];
  825.         foreach ($this->getSteps() as $step) {
  826.             if ($this->isStepDone($step->getNumber())) {
  827.                 $stepsDone[] = $step;
  828.             }
  829.         }
  830.         return $stepsDone;
  831.     }
  832.     /**
  833.      * {@inheritDoc}
  834.      */
  835.     public function getStepsRemaining() {
  836.         $stepsRemaining = [];
  837.         foreach ($this->getSteps() as $step) {
  838.             if (!$this->isStepDone($step->getNumber())) {
  839.                 $stepsRemaining[] = $step;
  840.             }
  841.         }
  842.         return $stepsRemaining;
  843.     }
  844.     /**
  845.      * {@inheritDoc}
  846.      */
  847.     public function getStepsDoneCount() {
  848.         return count($this->getStepsDone());
  849.     }
  850.     /**
  851.      * {@inheritDoc}
  852.      */
  853.     public function getStepsRemainingCount() {
  854.         return count($this->getStepsRemaining());
  855.     }
  856.     // methods for BC with third-party templates (e.g. MopaBootstrapBundle)
  857.     public function getCurrentStep() {
  858.         @trigger_error('Method ' __METHOD__ ' is deprecated since CraueFormFlowBundle 2.0. Use method getCurrentStepNumber instead.'E_USER_DEPRECATED);
  859.         return $this->getCurrentStepNumber();
  860.     }
  861.     public function getCurrentStepDescription() {
  862.         @trigger_error('Method ' __METHOD__ ' is deprecated since CraueFormFlowBundle 2.0. Use method getCurrentStepLabel instead.'E_USER_DEPRECATED);
  863.         return $this->getCurrentStepLabel();
  864.     }
  865.     public function getMaxSteps() {
  866.         @trigger_error('Method ' __METHOD__ ' is deprecated since CraueFormFlowBundle 2.0. Use method getStepCount instead.'E_USER_DEPRECATED);
  867.         return $this->getStepCount();
  868.     }
  869.     public function getStepDescriptions() {
  870.         @trigger_error('Method ' __METHOD__ ' is deprecated since CraueFormFlowBundle 2.0. Use method getStepLabels instead.'E_USER_DEPRECATED);
  871.         return $this->getStepLabels();
  872.     }
  873.     public function getFirstStep() {
  874.         @trigger_error('Method ' __METHOD__ ' is deprecated since CraueFormFlowBundle 2.0. Use method getFirstStepNumber instead.'E_USER_DEPRECATED);
  875.         return $this->getFirstStepNumber();
  876.     }
  877.     public function getLastStep() {
  878.         @trigger_error('Method ' __METHOD__ ' is deprecated since CraueFormFlowBundle 2.0. Use method getLastStepNumber instead.'E_USER_DEPRECATED);
  879.         return $this->getLastStepNumber();
  880.     }
  881.     public function hasSkipStep($stepNumber) {
  882.         @trigger_error('Method ' __METHOD__ ' is deprecated since CraueFormFlowBundle 2.0. Use method isStepSkipped instead.'E_USER_DEPRECATED);
  883.         return $this->isStepSkipped($stepNumber);
  884.     }
  885. }