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

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 !== 1 && $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 === 1 && $stepNumber > $this->getStepCount()) {
  358. $direction = -1;
  359. ++$boundsReached;
  360. } elseif ($direction === -1 && $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($event, FormFlowEvents::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. }