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

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