• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

craue / CraueFormFlowBundle / 15439473581

04 Jun 2025 10:02AM UTC coverage: 98.725% (-0.8%) from 99.482%
15439473581

push

github

craue
dropped support for Symfony 4.4, 6.3, 7.0, 7.1

697 of 706 relevant lines covered (98.73%)

838.74 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

99.36
/Form/FormFlow.php
1
<?php
2

3
namespace Craue\FormFlowBundle\Form;
4

5
use Craue\FormFlowBundle\Event\FlowExpiredEvent;
6
use Craue\FormFlowBundle\Event\FormFlowEvent;
7
use Craue\FormFlowBundle\Event\GetStepsEvent;
8
use Craue\FormFlowBundle\Event\PostBindFlowEvent;
9
use Craue\FormFlowBundle\Event\PostBindRequestEvent;
10
use Craue\FormFlowBundle\Event\PostBindSavedDataEvent;
11
use Craue\FormFlowBundle\Event\PostValidateEvent;
12
use Craue\FormFlowBundle\Event\PreBindEvent;
13
use Craue\FormFlowBundle\Event\PreviousStepInvalidEvent;
14
use Craue\FormFlowBundle\Exception\AllStepsSkippedException;
15
use Craue\FormFlowBundle\Exception\InvalidTypeException;
16
use Craue\FormFlowBundle\Storage\DataManagerInterface;
17
use Craue\FormFlowBundle\Util\StringUtil;
18
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
19
use Symfony\Component\Form\Extension\Core\Type\FormType;
20
use Symfony\Component\Form\FormFactoryInterface;
21
use Symfony\Component\Form\FormInterface;
22
use Symfony\Component\HttpFoundation\Request;
23
use Symfony\Component\HttpFoundation\RequestStack;
24
use Symfony\Component\Validator\Constraints\GroupSequence;
25

26
/**
27
 * @author Christian Raue <christian.raue@gmail.com>
28
 * @author Marcus Stöhr <dafish@soundtrack-board.de>
29
 * @author Toni Uebernickel <tuebernickel@gmail.com>
30
 * @copyright 2011-2024 Christian Raue
31
 * @license http://opensource.org/licenses/mit-license.php MIT License
32
 */
33
abstract class FormFlow implements FormFlowInterface {
34

35
        const TRANSITION_BACK = 'back';
36
        const TRANSITION_RESET = 'reset';
37

38
        /**
39
         * @var FormFactoryInterface
40
         */
41
        protected $formFactory;
42

43
        /**
44
         * @var DataManagerInterface
45
         */
46
        protected $dataManager;
47

48
        /**
49
         * @var EventDispatcherInterface|null
50
         */
51
        protected $eventDispatcher = null;
52

53
        /**
54
         * @var string|null
55
         */
56
        protected $transition;
57

58
        /**
59
         * @var bool
60
         */
61
        protected $revalidatePreviousSteps = true;
62

63
        /**
64
         * @var bool
65
         */
66
        protected $allowDynamicStepNavigation = false;
67

68
        /**
69
         * @var bool If file uploads should be handled by serializing them into the storage.
70
         */
71
        protected $handleFileUploads = true;
72

73
        /**
74
         * @var string|null Directory for storing temporary files while handling uploads. If <code>null</code>, the system's default will be used.
75
         */
76
        protected $handleFileUploadsTempDir = null;
77

78
        /**
79
         * @var bool
80
         */
81
        protected $allowRedirectAfterSubmit = false;
82

83
        /**
84
         * @var string
85
         */
86
        protected $dynamicStepNavigationInstanceParameter = 'instance';
87

88
        /**
89
         * @var string
90
         */
91
        protected $dynamicStepNavigationStepParameter = 'step';
92

93
        /**
94
         * @var RequestStack
95
         */
96
        private $requestStack;
97

98
        /**
99
         * @var string|null Is only null if not yet initialized.
100
         */
101
        private $id = null;
102

103
        /**
104
         * @var string|null Is only null if not yet initialized.
105
         */
106
        private $instanceKey = null;
107

108
        /**
109
         * @var string|null Is only null if not yet initialized.
110
         */
111
        private $instanceId = null;
112

113
        /**
114
         * @var string|null Is only null if not yet initialized.
115
         */
116
        private $formStepKey = null;
117

118
        /**
119
         * @var string|null Is only null if not yet initialized.
120
         */
121
        private $formTransitionKey = null;
122

123
        /**
124
         * @var string|null Is only null if not yet initialized.
125
         */
126
        private $validationGroupPrefix = null;
127

128
        /**
129
         * @var StepInterface[]|null Is only null if not yet initialized.
130
         */
131
        private $steps = null;
132

133
        /**
134
         * @var int|null Is only null if not yet initialized.
135
         */
136
        private $stepCount = null;
137

138
        /**
139
         * @var string[]|null Is only null if not yet initialized.
140
         */
141
        private $stepLabels = null;
142

143
        /**
144
         * @var mixed|null Is only null if not yet initialized.
145
         */
146
        private $formData = null;
147

148
        /**
149
         * @var int|null Is only null if not yet initialized.
150
         */
151
        private $currentStepNumber = null;
152

153
        /**
154
         * @var FormInterface[]
155
         */
156
        private $stepForms = [];
157

158
        /**
159
         * Options applied to forms of all steps.
160
         * @var array
161
         */
162
        private $genericFormOptions = [];
163

164
        /**
165
         * Flow was determined to be expired.
166
         * @var bool
167
         */
168
        private $expired = false;
169

170
        /**
171
         * {@inheritDoc}
172
         */
173
        public function setFormFactory(FormFactoryInterface $formFactory) {
174
                $this->formFactory = $formFactory;
1,404✔
175
        }
176

177
        /**
178
         * {@inheritDoc}
179
         */
180
        public function setRequestStack(RequestStack $requestStack) {
181
                $this->requestStack = $requestStack;
1,422✔
182
        }
183

184
        /**
185
         * @return Request
186
         * @throws \RuntimeException If the request is not available.
187
         */
188
        public function getRequest() {
189
                $currentRequest = $this->requestStack->getCurrentRequest();
1,422✔
190

191
                if ($currentRequest === null) {
1,422✔
192
                        throw new \RuntimeException('The request is not available.');
9✔
193
                }
194

195
                return $currentRequest;
1,413✔
196
        }
197

198
        /**
199
         * {@inheritDoc}
200
         */
201
        public function setDataManager(DataManagerInterface $dataManager) {
202
                $this->dataManager = $dataManager;
1,584✔
203
        }
204

205
        /**
206
         * {@inheritDoc}
207
         */
208
        public function getDataManager() {
209
                return $this->dataManager;
9✔
210
        }
211

212
        /**
213
         * {@inheritDoc}
214
         */
215
        public function setEventDispatcher(EventDispatcherInterface $eventDispatcher) {
216
                $this->eventDispatcher = $eventDispatcher;
1,413✔
217
        }
218

219
        public function setId($id) {
220
                $this->id = $id;
9✔
221
        }
222

223
        /**
224
         * {@inheritDoc}
225
         */
226
        public function getId() {
227
                if ($this->id === null) {
1,827✔
228
                        $this->id = 'flow_' . $this->getName();
1,818✔
229
                }
230

231
                return $this->id;
1,827✔
232
        }
233

234
        /**
235
         * {@inheritDoc}
236
         */
237
        public function getName() {
238
                return StringUtil::fqcnToFlowName(get_class($this));
1,476✔
239
        }
240

241
        public function setInstanceKey($instanceKey) {
242
                $this->instanceKey = $instanceKey;
9✔
243
        }
244

245
        public function getInstanceKey() {
246
                if ($this->instanceKey === null) {
1,485✔
247
                        $this->instanceKey = $this->getId() . '_instance';
1,476✔
248
                }
249

250
                return $this->instanceKey;
1,485✔
251
        }
252

253
        public function setInstanceId($instanceId) {
254
                $this->instanceId = $instanceId;
1,512✔
255
        }
256

257
        /**
258
         * {@inheritDoc}
259
         */
260
        public function getInstanceId() {
261
                if ($this->instanceId === null) {
1,593✔
262
                        $this->instanceId = $this->getId();
81✔
263
                }
264

265
                return $this->instanceId;
1,593✔
266
        }
267

268
        public function setFormStepKey($formStepKey) {
269
                $this->formStepKey = $formStepKey;
9✔
270
        }
271

272
        public function getFormStepKey() {
273
                if ($this->formStepKey === null) {
1,521✔
274
                        $this->formStepKey = $this->getId() . '_step';
1,512✔
275
                }
276

277
                return $this->formStepKey;
1,521✔
278
        }
279

280
        public function setFormTransitionKey($formTransitionKey) {
281
                $this->formTransitionKey = $formTransitionKey;
9✔
282
        }
283

284
        public function getFormTransitionKey() {
285
                if ($this->formTransitionKey === null) {
1,710✔
286
                        $this->formTransitionKey = $this->getId() . '_transition';
1,701✔
287
                }
288

289
                return $this->formTransitionKey;
1,710✔
290
        }
291

292
        public function setValidationGroupPrefix($validationGroupPrefix) {
293
                $this->validationGroupPrefix = $validationGroupPrefix;
9✔
294
        }
295

296
        public function getValidationGroupPrefix() {
297
                if ($this->validationGroupPrefix === null) {
1,458✔
298
                        $this->validationGroupPrefix = $this->getId() . '_step';
1,449✔
299
                }
300

301
                return $this->validationGroupPrefix;
1,458✔
302
        }
303

304
        /**
305
         * {@inheritDoc}
306
         */
307
        public function getStepCount() {
308
                if ($this->stepCount === null) {
1,638✔
309
                        $this->stepCount = count($this->getSteps());
1,638✔
310
                }
311

312
                return $this->stepCount;
1,638✔
313
        }
314

315
        /**
316
         * {@inheritDoc}
317
         */
318
        public function getFormData() {
319
                if ($this->formData === null) {
801✔
320
                        throw new \RuntimeException('Form data has not been evaluated yet and thus cannot be accessed.');
9✔
321
                }
322

323
                return $this->formData;
792✔
324
        }
325

326
        /**
327
         * {@inheritDoc}
328
         */
329
        public function getCurrentStepNumber() {
330
                if ($this->currentStepNumber === null) {
1,593✔
331
                        throw new \RuntimeException('The current step has not been determined yet and thus cannot be accessed.');
9✔
332
                }
333

334
                return $this->currentStepNumber;
1,584✔
335
        }
336

337
        public function setRevalidatePreviousSteps($revalidatePreviousSteps) {
338
                $this->revalidatePreviousSteps = (bool) $revalidatePreviousSteps;
279✔
339
        }
340

341
        /**
342
         * {@inheritDoc}
343
         */
344
        public function isRevalidatePreviousSteps() {
345
                return $this->revalidatePreviousSteps;
45✔
346
        }
347

348
        public function setAllowDynamicStepNavigation($allowDynamicStepNavigation) {
349
                $this->allowDynamicStepNavigation = (bool) $allowDynamicStepNavigation;
99✔
350
        }
351

352
        /**
353
         * {@inheritDoc}
354
         */
355
        public function isAllowDynamicStepNavigation() {
356
                return $this->allowDynamicStepNavigation;
1,476✔
357
        }
358

359
        public function setHandleFileUploads($handleFileUploads) {
360
                $this->handleFileUploads = (bool) $handleFileUploads;
45✔
361
        }
362

363
        /**
364
         * {@inheritDoc}
365
         */
366
        public function isHandleFileUploads() {
367
                return $this->handleFileUploads;
1,512✔
368
        }
369

370
        public function setHandleFileUploadsTempDir($handleFileUploadsTempDir) {
371
                $this->handleFileUploadsTempDir = $handleFileUploadsTempDir !== null ? (string) $handleFileUploadsTempDir : null;
27✔
372
        }
373

374
        /**
375
         * {@inheritDoc}
376
         */
377
        public function getHandleFileUploadsTempDir() {
378
                return $this->handleFileUploadsTempDir;
1,494✔
379
        }
380

381
        public function setAllowRedirectAfterSubmit($allowRedirectAfterSubmit) {
382
                $this->allowRedirectAfterSubmit = (bool) $allowRedirectAfterSubmit;
162✔
383
        }
384

385
        /**
386
         * {@inheritDoc}
387
         */
388
        public function isAllowRedirectAfterSubmit() {
389
                return $this->allowRedirectAfterSubmit;
45✔
390
        }
391

392
        public function setDynamicStepNavigationInstanceParameter($dynamicStepNavigationInstanceParameter) {
393
                $this->dynamicStepNavigationInstanceParameter = $dynamicStepNavigationInstanceParameter;
9✔
394
        }
395

396
        public function getDynamicStepNavigationInstanceParameter() {
397
                return $this->dynamicStepNavigationInstanceParameter;
576✔
398
        }
399

400
        public function setDynamicStepNavigationStepParameter($dynamicStepNavigationStepParameter) {
401
                $this->dynamicStepNavigationStepParameter = $dynamicStepNavigationStepParameter;
9✔
402
        }
403

404
        public function getDynamicStepNavigationStepParameter() {
405
                return $this->dynamicStepNavigationStepParameter;
576✔
406
        }
407

408
        public function setGenericFormOptions(array $genericFormOptions) {
409
                $this->genericFormOptions = $genericFormOptions;
27✔
410
        }
411

412
        public function getGenericFormOptions() {
413
                return $this->genericFormOptions;
1,476✔
414
        }
415

416
        /**
417
         * {@inheritDoc}
418
         */
419
        public function isStepSkipped($stepNumber) {
420
                return $this->getStep($stepNumber)->isSkipped();
1,647✔
421
        }
422

423
        /**
424
         * @param int $stepNumber Assumed step to which skipped steps shall be applied to.
425
         * @param int $direction Either 1 (to skip forwards) or -1 (to skip backwards).
426
         * @param int $boundsReached Internal counter to avoid endlessly bouncing back and forth.
427
         * @return int Target step number with skipping applied.
428
         * @throws \InvalidArgumentException If the value of <code>$direction</code> is invalid.
429
         */
430
        protected function applySkipping($stepNumber, $direction = 1, $boundsReached = 0) {
431
                if ($direction !== 1 && $direction !== -1) {
1,665✔
432
                        throw new \InvalidArgumentException(sprintf('Argument of either -1 or 1 expected, "%s" given.', $direction));
27✔
433
                }
434

435
                $stepNumber = $this->ensureStepNumberRange($stepNumber);
1,638✔
436

437
                if ($this->isStepSkipped($stepNumber)) {
1,638✔
438
                        $stepNumber += $direction;
693✔
439

440
                        // change direction if outer bounds are reached
441
                        if ($direction === 1 && $stepNumber > $this->getStepCount()) {
693✔
442
                                $direction = -1;
54✔
443
                                ++$boundsReached;
54✔
444
                        } elseif ($direction === -1 && $stepNumber < 1) {
693✔
445
                                $direction = 1;
45✔
446
                                ++$boundsReached;
45✔
447
                        }
448

449
                        if ($boundsReached > 2) {
693✔
450
                                throw new AllStepsSkippedException();
9✔
451
                        }
452

453
                        return $this->applySkipping($stepNumber, $direction, $boundsReached);
693✔
454
                }
455

456
                return $stepNumber;
1,629✔
457
        }
458

459
        /**
460
         * {@inheritDoc}
461
         */
462
        public function reset() {
463
                $this->dataManager->drop($this);
1,116✔
464
                $this->currentStepNumber = $this->getFirstStepNumber();
1,116✔
465

466
                // re-evaluate to not keep steps marked as skipped when resetting
467
                foreach ($this->getSteps() as $step) {
1,116✔
468
                        $step->evaluateSkipping($this->currentStepNumber, $this);
1,116✔
469
                }
470
        }
471

472
        /**
473
         * {@inheritDoc}
474
         */
475
        public function getFirstStepNumber() {
476
                return $this->applySkipping(1);
1,548✔
477
        }
478

479
        /**
480
         * {@inheritDoc}
481
         */
482
        public function getLastStepNumber() {
483
                return $this->applySkipping($this->getStepCount(), -1);
1,593✔
484
        }
485

486
        /**
487
         * {@inheritDoc}
488
         */
489
        public function nextStep() {
490
                $currentStepNumber = $this->currentStepNumber + 1;
1,557✔
491

492
                foreach ($this->getSteps() as $step) {
1,557✔
493
                        $step->evaluateSkipping($currentStepNumber, $this);
1,557✔
494
                }
495

496
                // There is no "next" step as the target step exceeds the actual step count.
497
                if ($currentStepNumber > $this->getLastStepNumber()) {
1,557✔
498
                        return false;
396✔
499
                }
500

501
                $currentStepNumber = $this->applySkipping($currentStepNumber);
1,521✔
502

503
                if ($currentStepNumber <= $this->getStepCount()) {
1,521✔
504
                        $this->currentStepNumber = $currentStepNumber;
1,521✔
505

506
                        return true;
1,521✔
507
                }
508

509
                return false; // should never be reached, but just in case
×
510
        }
511

512
        /**
513
         * {@inheritDoc}
514
         */
515
        public function isStepDone($stepNumber) {
516
                if ($this->isStepSkipped($stepNumber)) {
1,350✔
517
                        return true;
261✔
518
                }
519

520
                return array_key_exists($stepNumber, $this->retrieveStepData());
1,350✔
521
        }
522

523
        public function getRequestedTransition() {
524
                if (!is_string($this->transition) || $this->transition === '') {
1,557✔
525
                        $this->transition = strtolower($this->getRequest()->request->get($this->getFormTransitionKey(), ''));
1,557✔
526
                }
527

528
                return $this->transition;
1,557✔
529
        }
530

531
        protected function getRequestedStepNumber() {
532
                $defaultStepNumber = 1;
1,476✔
533

534
                $request = $this->getRequest();
1,476✔
535

536
                switch ($request->getMethod()) {
1,476✔
537
                        case 'PUT':
1,476✔
538
                        case 'POST':
1,458✔
539
                                return intval($request->request->get($this->getFormStepKey(), $defaultStepNumber));
1,440✔
540
                        case 'GET':
1,440✔
541
                                return $this->allowDynamicStepNavigation || $this->allowRedirectAfterSubmit ?
1,431✔
542
                                                intval($request->get($this->dynamicStepNavigationStepParameter, $defaultStepNumber)) :
558✔
543
                                                $defaultStepNumber;
1,431✔
544
                }
545

546
                return $defaultStepNumber;
9✔
547
        }
548

549
        /**
550
         * Finds out which step is the current one.
551
         * @return int
552
         */
553
        protected function determineCurrentStepNumber() {
554
                $requestedStepNumber = $this->getRequestedStepNumber();
1,404✔
555

556
                if ($this->getRequestedTransition() === self::TRANSITION_BACK) {
1,404✔
557
                        --$requestedStepNumber;
324✔
558
                }
559

560
                $requestedStepNumber = $this->ensureStepNumberRange($requestedStepNumber);
1,404✔
561
                $requestedStepNumber = $this->refineCurrentStepNumber($requestedStepNumber);
1,404✔
562

563
                if ($this->getRequestedTransition() === self::TRANSITION_BACK) {
1,404✔
564
                        $requestedStepNumber = $this->applySkipping($requestedStepNumber, -1);
324✔
565

566
                        // re-evaluate to not keep following steps marked as skipped (after skipping them while going back)
567
                        foreach ($this->getSteps() as $step) {
324✔
568
                                $step->evaluateSkipping($requestedStepNumber, $this);
324✔
569
                        }
570
                } else {
571
                        $requestedStepNumber = $this->applySkipping($requestedStepNumber);
1,404✔
572
                }
573

574
                return $requestedStepNumber;
1,404✔
575
        }
576

577
        /**
578
         * Ensures that the step number is within the range of defined steps to avoid a possible OutOfBoundsException.
579
         * @param int $stepNumber
580
         * @return int
581
         */
582
        private function ensureStepNumberRange($stepNumber) {
583
                return max(min($stepNumber, $this->getStepCount()), 1);
1,638✔
584
        }
585

586
        /**
587
         * Refines the current step number by evaluating and considering skipped steps.
588
         * @param int $refinedStepNumber
589
         * @return int
590
         */
591
        protected function refineCurrentStepNumber($refinedStepNumber) {
592
                foreach ($this->getSteps() as $step) {
1,404✔
593
                        $step->evaluateSkipping($refinedStepNumber, $this);
1,404✔
594
                }
595

596
                return $refinedStepNumber;
1,404✔
597
        }
598

599
        /**
600
         * {@inheritDoc}
601
         */
602
        public function bind($formData) {
603
                $this->setInstanceId($this->determineInstanceId());
1,404✔
604

605
                if ($this->hasListeners(FormFlowEvents::PRE_BIND)) {
1,404✔
606
                        $this->dispatchEvent(new PreBindEvent($this), FormFlowEvents::PRE_BIND);
1,296✔
607
                }
608

609
                $this->formData = $formData;
1,404✔
610

611
                $this->bindFlow();
1,404✔
612

613
                if ($this->hasListeners(FormFlowEvents::POST_BIND_FLOW)) {
1,404✔
614
                        $this->dispatchEvent(new PostBindFlowEvent($this, $this->formData), FormFlowEvents::POST_BIND_FLOW);
1,296✔
615
                }
616

617
                if (!$this->dataManager->exists($this)) {
1,404✔
618
                        // initialize storage slot
619
                        $this->dataManager->save($this, []);
1,404✔
620
                }
621
        }
622

623
        protected function determineInstanceId() {
624
                $request = $this->getRequest();
1,404✔
625
                $instanceId = null;
1,404✔
626

627
                if ($this->allowDynamicStepNavigation || $this->allowRedirectAfterSubmit) {
1,404✔
628
                        $instanceId = $request->get($this->getDynamicStepNavigationInstanceParameter());
540✔
629
                }
630

631
                if ($instanceId === null) {
1,404✔
632
                        $instanceId = $request->request->get($this->getInstanceKey());
1,404✔
633
                }
634

635
                $instanceIdLength = 10;
1,404✔
636
                if ($instanceId === null || !StringUtil::isRandomString($instanceId, $instanceIdLength)) {
1,404✔
637
                        $instanceId = StringUtil::generateRandomString($instanceIdLength);
1,404✔
638
                }
639

640
                return $instanceId;
1,404✔
641
        }
642

643
        protected function bindFlow() {
644
                $request = $this->getRequest();
1,404✔
645
                $reset = false;
1,404✔
646

647
                if (!$this->allowDynamicStepNavigation && !$this->allowRedirectAfterSubmit && $request->isMethod('GET')) {
1,404✔
648
                        $reset = true;
864✔
649
                }
650

651
                if ($this->getRequestedTransition() === self::TRANSITION_RESET) {
1,404✔
652
                        $reset = true;
252✔
653
                }
654

655
                if (in_array($request->getMethod(), ['POST', 'PUT'], true) && $request->get($this->getFormStepKey()) !== null && !$this->dataManager->exists($this)) {
1,404✔
656
                        // flow is expired, drop posted data and reset
657
                        $request->request->replace();
72✔
658
                        $reset = true;
72✔
659
                        $this->expired = true;
72✔
660

661
                        // Regenerate instance ID so resubmits of the form will continue to give error. Otherwise, submitting
662
                        // the new form, then backing up to the old form won't give the error.
663
                        $this->setInstanceId($this->determineInstanceId());
72✔
664
                }
665

666
                if (!$reset) {
1,404✔
667
                        $this->applyDataFromSavedSteps();
1,404✔
668
                }
669

670
                $requestedStepNumber = $this->determineCurrentStepNumber();
1,404✔
671

672
                if ($reset) {
1,404✔
673
                        $this->reset();
972✔
674
                        return;
972✔
675
                }
676

677
                // ensure that the requested step fits the current progress
678
                if ($requestedStepNumber > $this->getFirstStepNumber()) {
1,404✔
679
                        for ($step = $this->getFirstStepNumber(); $step < $requestedStepNumber; ++$step) {
828✔
680
                                if (!$this->isStepDone($step)) {
828✔
681
                                        $this->reset();
72✔
682
                                        return;
72✔
683
                                }
684
                        }
685
                }
686

687
                $this->currentStepNumber = $requestedStepNumber;
1,368✔
688

689
                if (!$this->allowDynamicStepNavigation && $this->getRequestedTransition() === self::TRANSITION_BACK) {
1,368✔
690
                        /*
691
                         * Don't invalidate data for the current step to properly show the filled out form for that step after
692
                         * pressing "back" and refreshing the page. Otherwise, the form would be blank since the data has already
693
                         * been invalidated previously.
694
                         */
695
                        $this->invalidateStepData($this->currentStepNumber + 1);
324✔
696
                }
697
        }
698

699
        /**
700
         * {@inheritDoc}
701
         */
702
        public function saveCurrentStepData(FormInterface $form) {
703
                $stepData = $this->retrieveStepData();
1,386✔
704

705
                $request = $this->getRequest();
1,386✔
706
                $formName = $form->getName();
1,386✔
707

708
                if (!\class_exists('Symfony\Component\HttpFoundation\InputBag')) {
1,386✔
709
                        // TODO remove as soon as Symfony >= 5.1 is required
710
                        $currentStepData = $request->request->get($formName, []);
×
711
                } else {
712
                        $currentStepData = $request->request->all($formName);
1,386✔
713
                }
714

715
                if ($this->handleFileUploads) {
1,386✔
716
                        $currentStepData = array_replace_recursive($currentStepData, $request->files->get($formName, []));
1,386✔
717
                }
718

719
                $stepData[$this->getCurrentStepNumber()] = $currentStepData;
1,386✔
720

721
                $this->saveStepData($stepData);
1,386✔
722
        }
723

724
        /**
725
         * Invalidates data for steps >= $fromStepNumber.
726
         * @param int $fromStepNumber
727
         */
728
        public function invalidateStepData($fromStepNumber) {
729
                $stepData = $this->retrieveStepData();
324✔
730

731
                for ($step = $fromStepNumber, $stepCount = $this->getStepCount(); $step < $stepCount; ++$step) {
324✔
732
                        unset($stepData[$step]);
216✔
733
                }
734

735
                $this->saveStepData($stepData);
324✔
736
        }
737

738
        /**
739
         * Updates form data class with previously saved form data of all steps.
740
         */
741
        protected function applyDataFromSavedSteps() {
742
                $stepData = $this->retrieveStepData();
1,404✔
743

744
                $this->stepForms = [];
1,404✔
745

746
                $options = [];
1,404✔
747
                if (!$this->revalidatePreviousSteps) {
1,404✔
748
                        $options['validation_groups'] = false; // disable validation
72✔
749
                }
750

751
                foreach ($this->getSteps() as $step) {
1,404✔
752
                        $stepNumber = $step->getNumber();
1,404✔
753

754
                        if (array_key_exists($stepNumber, $stepData)) {
1,404✔
755
                                $stepForm = $this->createFormForStep($stepNumber, $options);
1,008✔
756
                                $stepForm->submit($stepData[$stepNumber]); // the form is validated here
1,008✔
757

758
                                if ($this->revalidatePreviousSteps) {
1,008✔
759
                                        $this->stepForms[$stepNumber] = $stepForm;
936✔
760
                                }
761

762
                                if ($this->hasListeners(FormFlowEvents::POST_BIND_SAVED_DATA)) {
1,008✔
763
                                        $this->dispatchEvent(new PostBindSavedDataEvent($this, $this->formData, $stepNumber), FormFlowEvents::POST_BIND_SAVED_DATA);
900✔
764
                                }
765
                        }
766
                }
767
        }
768

769
        /**
770
         * {@inheritDoc}
771
         */
772
        public function createForm() {
773
                $form = $this->createFormForStep($this->currentStepNumber);
1,404✔
774

775
                if ($this->expired && $this->hasListeners(FormFlowEvents::FLOW_EXPIRED)) {
1,404✔
776
                        $this->dispatchEvent(new FlowExpiredEvent($this, $form), FormFlowEvents::FLOW_EXPIRED);
72✔
777
                }
778

779
                return $form;
1,404✔
780
        }
781

782
        public function getFormOptions($step, array $options = []) {
783
                // override options in a specific order
784
                $options = array_merge(
1,476✔
785
                        $this->getGenericFormOptions(),
1,476✔
786
                        $this->getStep($step)->getFormOptions(),
1,476✔
787
                        $options
1,476✔
788
                );
1,476✔
789

790
                // add the generated step-based validation group, unless it's explicitly set to false, a closure, or a GroupSequence
791
                if (!array_key_exists('validation_groups', $options)) {
1,476✔
792
                        $options['validation_groups'] = [$this->getValidationGroupPrefix() . $step];
1,440✔
793
                } else {
794
                        $vg = $options['validation_groups'];
108✔
795

796
                        if ($vg !== false && !is_a($vg, 'Closure') && !$vg instanceof GroupSequence) {
108✔
797
                                $options['validation_groups'] = array_merge(
9✔
798
                                        [$this->getValidationGroupPrefix() . $step],
9✔
799
                                        (array) $vg
9✔
800
                                );
9✔
801
                        }
802
                }
803

804
                $options['flow_instance'] = $this->getInstanceId();
1,476✔
805
                $options['flow_instance_key'] = $this->getInstanceKey();
1,476✔
806

807
                $options['flow_step'] = $step;
1,476✔
808
                $options['flow_step_key'] = $this->getFormStepKey();
1,476✔
809

810
                return $options;
1,476✔
811
        }
812

813
        /**
814
         * {@inheritDoc}
815
         */
816
        public function getStep($stepNumber) {
817
                if (!is_int($stepNumber)) {
1,755✔
818
                        throw new InvalidTypeException($stepNumber, 'int');
18✔
819
                }
820

821
                $steps = $this->getSteps();
1,737✔
822
                $index = $stepNumber - 1;
1,737✔
823

824
                if (array_key_exists($index, $steps)) {
1,737✔
825
                        return $steps[$index];
1,728✔
826
                }
827

828
                throw new \OutOfBoundsException(sprintf('The step "%d" does not exist.', $stepNumber));
9✔
829
        }
830

831
        /**
832
         * {@inheritDoc}
833
         */
834
        public function getSteps() {
835
                // The steps have been loaded already.
836
                if ($this->steps !== null) {
1,746✔
837
                        return $this->steps;
1,647✔
838
                }
839

840
                if ($this->hasListeners(FormFlowEvents::GET_STEPS)) {
1,746✔
841
                        $event = new GetStepsEvent($this);
1,305✔
842
                        $this->dispatchEvent($event, FormFlowEvents::GET_STEPS);
1,305✔
843

844
                        // A listener has provided the steps for this flow.
845
                        if ($event->isPropagationStopped()) {
1,305✔
846
                                $this->steps = $event->getSteps();
9✔
847

848
                                return $this->steps;
9✔
849
                        }
850
                }
851

852
                // There are either no listeners on the event at all or none created the steps for this flow, so load from configuration.
853
                $this->steps = $this->createStepsFromConfig($this->loadStepsConfig());
1,737✔
854

855
                return $this->steps;
1,737✔
856
        }
857

858
        /**
859
         * {@inheritDoc}
860
         */
861
        public function getStepLabels() {
862
                if ($this->stepLabels === null) {
1,431✔
863
                        $stepLabels = [];
1,431✔
864

865
                        foreach ($this->getSteps() as $step) {
1,431✔
866
                                $stepLabels[] = $step->getLabel();
1,431✔
867
                        }
868

869
                        $this->stepLabels = $stepLabels;
1,431✔
870
                }
871

872
                return $this->stepLabels;
1,431✔
873
        }
874

875
        /**
876
         * {@inheritDoc}
877
         */
878
        public function getCurrentStepLabel() {
879
                return $this->getStep($this->currentStepNumber)->getLabel();
9✔
880
        }
881

882
        /**
883
         * {@inheritDoc}
884
         */
885
        public function isValid(FormInterface $form) {
886
                $request = $this->getRequest();
1,512✔
887

888
                if (in_array($request->getMethod(), ['POST', 'PUT'], true) && !in_array($this->getRequestedTransition(), [
1,512✔
889
                        self::TRANSITION_BACK,
1,512✔
890
                        self::TRANSITION_RESET,
1,512✔
891
                ], true)) {
1,512✔
892
                        $form->handleRequest($request);
1,422✔
893

894
                        if (!$form->isSubmitted()) {
1,422✔
895
                                return false;
126✔
896
                        }
897

898
                        if ($this->hasListeners(FormFlowEvents::POST_BIND_REQUEST)) {
1,404✔
899
                                $this->dispatchEvent(new PostBindRequestEvent($this, $form->getData(), $this->currentStepNumber), FormFlowEvents::POST_BIND_REQUEST);
1,260✔
900
                        }
901

902
                        if ($this->revalidatePreviousSteps) {
1,404✔
903
                                // check if forms of previous steps are still valid
904
                                foreach ($this->stepForms as $stepNumber => $stepForm) {
1,314✔
905
                                        // ignore form of the current step
906
                                        if ($this->currentStepNumber === $stepNumber) {
720✔
907
                                                break;
288✔
908
                                        }
909

910
                                        // ignore forms of skipped steps
911
                                        if ($this->isStepSkipped($stepNumber)) {
684✔
912
                                                break;
36✔
913
                                        }
914

915
                                        if (!$stepForm->isValid()) {
684✔
916
                                                if ($this->hasListeners(FormFlowEvents::PREVIOUS_STEP_INVALID)) {
72✔
917
                                                        $this->dispatchEvent(new PreviousStepInvalidEvent($this, $form, $stepNumber), FormFlowEvents::PREVIOUS_STEP_INVALID);
72✔
918
                                                }
919

920
                                                return false;
72✔
921
                                        }
922
                                }
923
                        }
924

925
                        if ($form->isValid()) {
1,404✔
926
                                if ($this->hasListeners(FormFlowEvents::POST_VALIDATE)) {
1,404✔
927
                                        $this->dispatchEvent(new PostValidateEvent($this, $form->getData()), FormFlowEvents::POST_VALIDATE);
1,260✔
928
                                }
929

930
                                return true;
1,404✔
931
                        }
932
                }
933

934
                return false;
1,458✔
935
        }
936

937
        /**
938
         * @param FormInterface $submittedForm
939
         * @return bool If a redirection should be performed.
940
         */
941
        public function redirectAfterSubmit(FormInterface $submittedForm) {
942
                if ($this->allowRedirectAfterSubmit && in_array($this->getRequest()->getMethod(), ['POST', 'PUT'], true)) {
1,485✔
943
                        switch ($this->getRequestedTransition()) {
99✔
944
                                case self::TRANSITION_BACK:
33✔
945
                                case self::TRANSITION_RESET:
30✔
946
                                        return true;
54✔
947
                                default:
948
                                        // redirect after submit only if there are no errors for the submitted form
949
                                        return $submittedForm->isSubmitted() && $submittedForm->isValid();
81✔
950
                        }
951
                }
952

953
                return false;
1,422✔
954
        }
955

956
        /**
957
         * Creates the form for the given step number.
958
         * @param int $stepNumber
959
         * @param array $options
960
         * @return FormInterface
961
         */
962
        protected function createFormForStep($stepNumber, array $options = []) {
963
                $formType = $this->getStep($stepNumber)->getFormType();
1,404✔
964
                $options = $this->getFormOptions($stepNumber, $options);
1,404✔
965

966
                if ($formType === null) {
1,404✔
967
                        $formType = FormType::class;
936✔
968
                }
969

970
                return $this->formFactory->create($formType, $this->formData, $options);
1,404✔
971
        }
972

973
        /**
974
         * Creates all steps from the given configuration.
975
         * @param array $stepsConfig
976
         * @return StepInterface[] Value with index 0 is step 1.
977
         */
978
        public function createStepsFromConfig(array $stepsConfig) {
979
                $steps = [];
1,737✔
980

981
                // fix array indexes not starting at 0
982
                $stepsConfig = array_values($stepsConfig);
1,737✔
983

984
                foreach ($stepsConfig as $index => $stepConfig) {
1,737✔
985
                        $steps[] = Step::createFromConfig($index + 1, $stepConfig);
1,728✔
986
                }
987

988
                return $steps;
1,737✔
989
        }
990

991
        /**
992
         * Defines the configuration for all steps of this flow.
993
         * @return array
994
         */
995
        protected function loadStepsConfig() {
996
                return [];
18✔
997
        }
998

999
        protected function retrieveStepData() {
1000
                return $this->dataManager->load($this);
1,413✔
1001
        }
1002

1003
        protected function saveStepData(array $data) {
1004
                $this->dataManager->save($this, $data);
1,368✔
1005
        }
1006

1007
        /**
1008
         * @param string $eventName
1009
         * @return bool
1010
         */
1011
        protected function hasListeners($eventName) {
1012
                return $this->eventDispatcher !== null && $this->eventDispatcher->hasListeners($eventName);
1,782✔
1013
        }
1014

1015
        /**
1016
         * @param FormFlowEvent $event
1017
         * @param string $eventName
1018
         */
1019
        private function dispatchEvent($event, $eventName) {
1020
                $this->eventDispatcher->dispatch($event, $eventName);
1,341✔
1021
        }
1022

1023
        /**
1024
         * {@inheritDoc}
1025
         */
1026
        public function getStepsDone() {
1027
                $stepsDone = [];
9✔
1028

1029
                foreach ($this->getSteps() as $step) {
9✔
1030
                        if ($this->isStepDone($step->getNumber())) {
9✔
1031
                                $stepsDone[] = $step;
9✔
1032
                        }
1033
                }
1034

1035
                return $stepsDone;
9✔
1036
        }
1037

1038
        /**
1039
         * {@inheritDoc}
1040
         */
1041
        public function getStepsRemaining() {
1042
                $stepsRemaining = [];
9✔
1043

1044
                foreach ($this->getSteps() as $step) {
9✔
1045
                        if (!$this->isStepDone($step->getNumber())) {
9✔
1046
                                $stepsRemaining[] = $step;
9✔
1047
                        }
1048
                }
1049

1050
                return $stepsRemaining;
9✔
1051
        }
1052

1053
        /**
1054
         * {@inheritDoc}
1055
         */
1056
        public function getStepsDoneCount() {
1057
                return count($this->getStepsDone());
9✔
1058
        }
1059

1060
        /**
1061
         * {@inheritDoc}
1062
         */
1063
        public function getStepsRemainingCount() {
1064
                return count($this->getStepsRemaining());
9✔
1065
        }
1066

1067
        // methods for BC with third-party templates (e.g. MopaBootstrapBundle)
1068

1069
        public function getCurrentStep() {
1070
                @trigger_error('Method ' . __METHOD__ . ' is deprecated since CraueFormFlowBundle 2.0. Use method getCurrentStepNumber instead.', E_USER_DEPRECATED);
9✔
1071
                return $this->getCurrentStepNumber();
9✔
1072
        }
1073

1074
        public function getCurrentStepDescription() {
1075
                @trigger_error('Method ' . __METHOD__ . ' is deprecated since CraueFormFlowBundle 2.0. Use method getCurrentStepLabel instead.', E_USER_DEPRECATED);
9✔
1076
                return $this->getCurrentStepLabel();
9✔
1077
        }
1078

1079
        public function getMaxSteps() {
1080
                @trigger_error('Method ' . __METHOD__ . ' is deprecated since CraueFormFlowBundle 2.0. Use method getStepCount instead.', E_USER_DEPRECATED);
9✔
1081
                return $this->getStepCount();
9✔
1082
        }
1083

1084
        public function getStepDescriptions() {
1085
                @trigger_error('Method ' . __METHOD__ . ' is deprecated since CraueFormFlowBundle 2.0. Use method getStepLabels instead.', E_USER_DEPRECATED);
9✔
1086
                return $this->getStepLabels();
9✔
1087
        }
1088

1089
        public function getFirstStep() {
1090
                @trigger_error('Method ' . __METHOD__ . ' is deprecated since CraueFormFlowBundle 2.0. Use method getFirstStepNumber instead.', E_USER_DEPRECATED);
9✔
1091
                return $this->getFirstStepNumber();
9✔
1092
        }
1093

1094
        public function getLastStep() {
1095
                @trigger_error('Method ' . __METHOD__ . ' is deprecated since CraueFormFlowBundle 2.0. Use method getLastStepNumber instead.', E_USER_DEPRECATED);
9✔
1096
                return $this->getLastStepNumber();
9✔
1097
        }
1098

1099
        public function hasSkipStep($stepNumber) {
1100
                @trigger_error('Method ' . __METHOD__ . ' is deprecated since CraueFormFlowBundle 2.0. Use method isStepSkipped instead.', E_USER_DEPRECATED);
9✔
1101
                return $this->isStepSkipped($stepNumber);
9✔
1102
        }
1103

1104
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc