Passed
Push — master ( c53c3c...c81ad8 )
by Thomas
03:29
created

MultiStepForm::clearTempDataFromSession()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 1
eloc 1
c 1
b 0
f 1
nc 1
nop 1
dl 0
loc 3
rs 10
1
<?php
2
3
namespace LeKoala\MultiStepForm;
4
5
use Exception;
6
use SilverStripe\Forms\Form;
7
use InvalidArgumentException;
8
use SilverStripe\ORM\ArrayList;
9
use SilverStripe\View\ArrayData;
10
use SilverStripe\Control\Session;
11
use SilverStripe\Forms\FieldList;
12
use SilverStripe\Forms\Validator;
13
use SilverStripe\Forms\FormAction;
14
use SilverStripe\View\Requirements;
15
use SilverStripe\Control\Controller;
16
use SilverStripe\Forms\CompositeField;
17
use SilverStripe\Forms\RequiredFields;
18
use SilverStripe\Control\RequestHandler;
19
use SilverStripe\ORM\ValidationException;
20
use SilverStripe\ORM\ValidationResult;
21
22
/**
23
 * Multi step form
24
 *
25
 * - Define a class name with a number in it (MyFormStep1) that extends this class
26
 * - Call definePrevNextActions instead of defining your actions
27
 * - Define a name in getStepTitle for a nicer name
28
 * - In your controller, create the form with classForCurrentStep
29
 *
30
 * @author lekoala
31
 */
32
abstract class MultiStepForm extends Form
33
{
34
    private static $include_css = true;
35
    private static $class_active = "current bg-primary text-white";
36
    private static $class_inactive = "link";
37
    private static $class_completed = "msf-completed bg-primary text-white";
38
    private static $class_not_completed = "msf-not-completed bg-light text-muted";
39
40
    protected $validationExemptActions = ["doPrev"];
41
42
    /**
43
     * @param RequestHandler $controller
44
     * @param mixed $name Extended to allow passing objects directly
45
     * @param FieldList $fields
46
     * @param FieldList $actions
47
     * @param Validator $validator
48
     */
49
    public function __construct(
50
        RequestHandler $controller = null,
51
        $name = null,
52
        FieldList $fields = null,
53
        FieldList $actions = null,
54
        Validator $validator = null
55
    ) {
56
        // Set a default name
57
        if (!$name) {
58
            $name = self::classNameWithoutNumber();
59
        }
60
        if ($fields) {
61
            throw new InvalidArgumentException("Fields should be defined inside MultiStepForm::buildFields method");
62
        }
63
        if ($actions) {
64
            throw new InvalidArgumentException("Actions are automatically defined by MultiStepForm");
65
        }
66
        if ($validator) {
67
            throw new InvalidArgumentException("Validator should be defined inside MultiStepForm::buildValidator method");
68
        }
69
        $this->setController($controller);
70
        $fields = $this->buildFields();
71
        $actions = $this->buildActions();
72
        $validator = $this->buildValidator($fields);
73
        parent::__construct($controller, $name, $fields, $actions, $validator);
74
75
        if (self::config()->include_css) {
76
            Requirements::css("lekoala/silverstripe-multi-step-form:css/multi-step-form.css");
77
        }
78
79
        $session = $controller->getRequest()->getSession();
0 ignored issues
show
Bug introduced by
The method getRequest() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

79
        $session = $controller->/** @scrutinizer ignore-call */ getRequest()->getSession();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
80
81
        // Loads first submitted data
82
        $data = $this->getTempDataFromSession($session);
83
        if (!empty($data)) {
84
            $this->loadDataFrom($data);
85
        } else {
86
            $data = $this->getDataFromSession($session);
87
            if (!empty($data)) {
88
                $this->loadDataFrom($data);
89
            }
90
        }
91
    }
92
93
    /**
94
     * @return FieldList
95
     */
96
    abstract protected function buildFields();
97
98
    /**
99
     * Call this instead of manually creating your actions
100
     *
101
     * You can easily rename actions by calling $actions->fieldByName('action_doNext')->setTitle('...')
102
     *
103
     * @return FieldList
104
     */
105
    protected function buildActions()
106
    {
107
        $actions = new FieldList();
108
109
        $prev = null;
110
        if (self::classNameNumber() > 1) {
111
            $prevLabel = _t('MultiStepForm.doPrev', 'Previous');
112
            $actions->push($prev = new FormAction('doPrev', $prevLabel));
113
            $prev->setUseButtonTag(true);
114
            // this must be supported by your validation client, it works with Zenvalidator
115
            $prev->addExtraClass("ignore-validation");
116
            $prev->addExtraClass("msf-step-prev");
117
        }
118
119
        $label = _t('MultiStepForm.doNext', 'Next');
120
        $actions->push($next = new FormAction('doNext', $label));
121
        $next->setUseButtonTag(true);
122
        $next->addExtraClass('msf-step-next');
123
        if (!$prev) {
124
            $next->addExtraClass('msf-step-next-single');
125
        }
126
        if (self::isLastStep()) {
127
            $next->setTitle(_t('MultiStepForm.doFinish', 'Finish'));
128
            $next->addExtraClass('msf-step-last');
129
        }
130
131
        if ($prev) {
132
            $actions->push($prev);
133
        }
134
135
        $this->addExtraClass('msf');
136
137
        return $actions;
138
    }
139
140
    /**
141
     * @param FieldList $fields
142
     * @return Validator
143
     */
144
    protected function buildValidator(FieldList $fields)
0 ignored issues
show
Unused Code introduced by
The parameter $fields is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

144
    protected function buildValidator(/** @scrutinizer ignore-unused */ FieldList $fields)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
145
    {
146
        return new RequiredFields;
147
    }
148
149
    public function FormAction()
150
    {
151
        $action = parent::FormAction();
152
        $action .= '?step=' . self::classNameNumber();
153
        return $action;
154
    }
155
156
    /**
157
     * Get a class name without namespace
158
     * @return string
159
     */
160
    public static function getClassWithoutNamespace()
161
    {
162
        $parts = explode("\\", get_called_class());
163
        return array_pop($parts);
164
    }
165
166
    /**
167
     * Get class name without any number in it
168
     * @return string
169
     */
170
    public static function classNameWithoutNumber()
171
    {
172
        return preg_replace('/[0-9]+/', '', self::getClassWithoutNamespace());
173
    }
174
175
    /**
176
     * Get number from class name
177
     * @return string
178
     */
179
    public static function classNameNumber()
180
    {
181
        return preg_replace('/[^0-9]+/', '', self::getClassWithoutNamespace());
182
    }
183
184
    /**
185
     * Get class name for current step based on this class name
186
     * @param Controller $controller
187
     * @return string
188
     */
189
    public static function classForCurrentStep($controller = null)
190
    {
191
        if (!$controller) {
192
            $controller = Controller::curr();
193
        }
194
195
        $request = $controller->getRequest();
196
197
        // Defaults to step 1
198
        $step = 1;
199
200
        // Check session
201
        $sessionStep = self::getCurrentStep($request->getSession());
202
        if ($sessionStep) {
203
            $step = $sessionStep;
204
        }
205
        // Override with step set manually
206
        $requestStep = $request->getVar('step');
207
        if ($requestStep) {
208
            $step = $requestStep;
209
        }
210
211
        return str_replace(self::classNameNumber(), $step, self::getClassWithoutNamespace());
212
    }
213
214
    /**
215
     * Get all steps as an ArrayList. To be used for your templates.
216
     * @return ArrayList
217
     */
218
    public function AllSteps()
219
    {
220
        $num = self::classNameNumber();
221
        if (!$num) {
222
            return;
223
        }
224
        $controller = Controller::curr();
225
        $n = 1;
226
        $curr = self::getCurrentStep($controller->getRequest()->getSession());
227
        if (!$curr) {
228
            $curr = 1;
229
        }
230
        $class = str_replace($num, $n, self::getClassWithoutNamespace());
231
        $steps = new ArrayList();
232
233
        $baseAction = parent::FormAction();
234
        $config = self::config();
235
236
        while (class_exists($class)) {
237
            $isCurrent = $isCompleted = $isNotCompleted = false;
238
            $cssClass = $n == $curr ? $config->class_active : $config->class_inactive;
239
            if ($n == 1) {
240
                $isCurrent = true;
241
                $cssClass .= ' first';
242
            }
243
            if ($class::isLastStep()) {
244
                $cssClass .= ' last';
245
            }
246
            if ($n < $curr) {
247
                $isCompleted = true;
248
                $cssClass .= ' ' . $config->class_completed;
249
            }
250
            if ($n > $curr) {
251
                $isNotCompleted = true;
252
                $cssClass .= ' ' . $config->class_not_completed;
253
            }
254
            $link = rtrim($baseAction, '/') . '/gotoStep/?step=' . $n;
255
            $steps->push(new ArrayData(array(
256
                'Title' => $class::getStepTitle(),
257
                'Number' => $n,
258
                'Link' => $isNotCompleted ? null : $link,
259
                'Class' => $cssClass,
260
                'IsCurrent' => $isCurrent,
261
                'IsCompleted' => $isCompleted,
262
                'isNotCompleted' => $isNotCompleted,
263
            )));
264
            $n++;
265
            $class = str_replace(self::classNameNumber(), $n, self::getClassWithoutNamespace());
266
        }
267
        return $steps;
268
    }
269
270
    /**
271
     * @return DBHTMLText
0 ignored issues
show
Bug introduced by
The type LeKoala\MultiStepForm\DBHTMLText was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
272
     */
273
    public function DisplaySteps()
274
    {
275
        return $this->renderWith('MsfSteps');
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->renderWith('MsfSteps') returns the type SilverStripe\ORM\FieldType\DBHTMLText which is incompatible with the documented return type LeKoala\MultiStepForm\DBHTMLText.
Loading history...
276
    }
277
278
    /**
279
     * Clear current step
280
     * @param Session $session
281
     * @return void
282
     */
283
    public static function clearCurrentStep(Session $session)
284
    {
285
        return (int) $session->clear(self::classNameWithoutNumber() . '.step');
0 ignored issues
show
Bug Best Practice introduced by
The expression return (int)$session->cl...houtNumber() . '.step') returns the type integer which is incompatible with the documented return type void.
Loading history...
286
    }
287
288
    /**
289
     * Get current step (defined in session). 0 if not started yet.
290
     * @param Session $session
291
     * @return int
292
     */
293
    public static function getCurrentStep(Session $session)
294
    {
295
        return (int) $session->get(self::classNameWithoutNumber() . '.step');
296
    }
297
298
    /**
299
     * Set max step
300
     * @param Session $session
301
     * @param int $value
302
     * @return void
303
     */
304
    public static function setMaxStep(Session $session, $value)
305
    {
306
        $session->set(self::classNameWithoutNumber() . '.maxStep', (int) $value);
307
    }
308
309
    /**
310
     * Get max step (defined in session). 0 if not started yet.
311
     * @param Session $session
312
     * @return int
313
     */
314
    public static function getMaxStep(Session $session)
315
    {
316
        return (int) $session->get(self::classNameWithoutNumber() . '.maxStep');
317
    }
318
319
    /**
320
     * Set current step
321
     * @param Session $session
322
     * @param int $value
323
     * @return void
324
     */
325
    public static function setCurrentStep(Session $session, $value)
326
    {
327
        $value = (int) $value;
328
329
        // Track highest step for step navigation
330
        if ($value > self::getMaxStep($session)) {
331
            self::setMaxStep($session, $value);
332
        }
333
334
        $session->set(self::classNameWithoutNumber() . '.step', $value);
335
    }
336
337
    /**
338
     * @return int
339
     */
340
    public static function getStepsCount()
341
    {
342
        $class = self::classNameWithoutNumber();
343
        $i = 1;
344
        $stepClass = $class . $i;
345
        while (class_exists($stepClass)) {
346
            $i++;
347
            $stepClass = $class . $i;
348
        }
349
        return --$i;
350
    }
351
352
    /**
353
     * Increment step
354
     * @param Session $session
355
     * @return string
356
     */
357
    public static function incrementStep(Session $session)
358
    {
359
        if (self::isLastStep()) {
360
            return;
361
        }
362
        $next = self::classNameNumber() + 1;
363
        if ($next == 1) {
364
            $next++;
365
        }
366
        return self::setCurrentStep($session, $next);
0 ignored issues
show
Bug introduced by
Are you sure the usage of self::setCurrentStep($session, $next) targeting LeKoala\MultiStepForm\Mu...pForm::setCurrentStep() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
Bug Best Practice introduced by
The expression return self::setCurrentStep($session, $next) returns the type void which is incompatible with the documented return type string.
Loading history...
367
    }
368
369
    /**
370
     * Decrement step
371
     * @param Session $session
372
     * @return string
373
     */
374
    public static function decrementStep(Session $session)
375
    {
376
        $prev = self::classNameNumber() - 1;
377
        if ($prev < 1) {
378
            return;
379
        }
380
        return self::setCurrentStep($session, $prev);
0 ignored issues
show
Bug introduced by
Are you sure the usage of self::setCurrentStep($session, $prev) targeting LeKoala\MultiStepForm\Mu...pForm::setCurrentStep() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
Bug Best Practice introduced by
The expression return self::setCurrentStep($session, $prev) returns the type void which is incompatible with the documented return type string.
Loading history...
381
    }
382
383
    /**
384
     * Goto a step
385
     * @param Session $session
386
     * @return HTTPResponse
0 ignored issues
show
Bug introduced by
The type LeKoala\MultiStepForm\HTTPResponse was not found. Did you mean HTTPResponse? If so, make sure to prefix the type with \.
Loading history...
387
     */
388
    public function gotoStep(Session $session)
389
    {
390
        $step = $this->getController()->getRequest()->getVar('step');
391
        if ($step > 0 && $step <= self::getMaxStep($session)) {
392
            self::setCurrentStep($session, $step);
393
        }
394
        return $this->getController()->redirectBack();
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->getController()->redirectBack() returns the type SilverStripe\Control\HTTPResponse which is incompatible with the documented return type LeKoala\MultiStepForm\HTTPResponse.
Loading history...
395
    }
396
397
    /**
398
     * Check if this is the last step
399
     * @return bool
400
     */
401
    public static function isLastStep()
402
    {
403
        $n = self::classNameNumber();
404
        $n1 = $n + 1;
405
        $class = str_replace($n, $n1, self::getClassWithoutNamespace());
406
        return !class_exists($class);
407
    }
408
409
    /**
410
     * Return the step name
411
     * @return string
412
     */
413
    abstract public static function getStepTitle();
414
415
    /**
416
     * Can be overwritten in child classes to update submitted data
417
     *
418
     * @param array $data
419
     * @return array
420
     */
421
    protected function processData(array $data)
422
    {
423
        return $data;
424
    }
425
426
    /**
427
     * Can be overwritten in child classes to apply custom step validation
428
     *
429
     * @throws ValidationException
430
     * @param array $data
431
     * @return void
432
     */
433
    protected function validateData(array $data)
0 ignored issues
show
Unused Code introduced by
The parameter $data is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

433
    protected function validateData(/** @scrutinizer ignore-unused */ array $data)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
434
    {
435
    }
436
437
    /**
438
     * A basic previous action that decrements the current step
439
     * @param array $data
440
     * @return HTTPResponse
441
     */
442
    public function doPrev($data)
0 ignored issues
show
Unused Code introduced by
The parameter $data is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

442
    public function doPrev(/** @scrutinizer ignore-unused */ $data)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
443
    {
444
        $controller = $this->getController();
445
        self::decrementStep($controller->getRequest()->getSession());
446
        return $controller->redirectBack();
0 ignored issues
show
Bug Best Practice introduced by
The expression return $controller->redirectBack() returns the type SilverStripe\Control\HTTPResponse which is incompatible with the documented return type LeKoala\MultiStepForm\HTTPResponse.
Loading history...
447
    }
448
449
    /**
450
     * A basic next action that increments the current step and save the data to the session
451
     * @param array $data
452
     * @return HTTPResponse
453
     */
454
    public function doNext($data)
455
    {
456
        $controller = $this->getController();
457
        $session = $controller->getRequest()->getSession();
458
459
        try {
460
            $this->validateData($data);
461
        } catch (ValidationException $ex) {
462
            $this->saveTempDataInSession($session, $data);
463
            $this->sessionError($ex->getMessage());
464
            return $controller->redirectBack();
0 ignored issues
show
Bug Best Practice introduced by
The expression return $controller->redirectBack() returns the type SilverStripe\Control\HTTPResponse which is incompatible with the documented return type LeKoala\MultiStepForm\HTTPResponse.
Loading history...
465
        }
466
467
        $data = $this->processData($data);
468
469
        self::incrementStep($session);
470
        $this->clearTempDataInSession();
0 ignored issues
show
Bug introduced by
The method clearTempDataInSession() does not exist on LeKoala\MultiStepForm\MultiStepForm. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

470
        $this->/** @scrutinizer ignore-call */ 
471
               clearTempDataInSession();
Loading history...
471
        $this->saveDataInSession($session, $data);
472
473
        if (self::isLastStep()) {
474
            // You will need to clear the current step and redirect to something else on the last step
475
            throw new Exception("Not implemented: please override doNext in your class");
476
        }
477
478
        return $controller->redirectBack();
0 ignored issues
show
Bug Best Practice introduced by
The expression return $controller->redirectBack() returns the type SilverStripe\Control\HTTPResponse which is incompatible with the documented return type LeKoala\MultiStepForm\HTTPResponse.
Loading history...
479
    }
480
481
    /**
482
     * @param Session $session
483
     * @param int $step
484
     * @return array
485
     */
486
    public static function getDataFromStep(Session $session, $step)
487
    {
488
        return $session->get(self::classNameWithoutNumber() . ".step_" . $step);
489
    }
490
491
    /**
492
     * @param Session $session
493
     * @param array $data
494
     */
495
    public function saveDataInSession(Session $session, array $data = null)
496
    {
497
        if (!$data) {
498
            $data = $this->getData();
499
        }
500
        $session->set(
501
            self::classNameWithoutNumber() . ".step_" . self::classNameNumber(),
502
            $data
503
        );
504
    }
505
506
    /**
507
     * @param Session $session
508
     * @param array $data
509
     */
510
    public function saveTempDataInSession(Session $session, array $data = null)
511
    {
512
        if (!$data) {
513
            $data = $this->getData();
514
        }
515
        $session->set(
516
            self::classNameWithoutNumber() . ".temp",
517
            $data
518
        );
519
    }
520
521
    /**
522
     * @param Session $session
523
     * @return array
524
     */
525
    public function getDataFromSession(Session $session)
526
    {
527
        return $session->get(self::classNameWithoutNumber() . ".step_" . self::classNameNumber());
528
    }
529
530
    /**
531
     * This is the data as submitted by the user
532
     *
533
     * @param Session $session
534
     * @return array
535
     */
536
    public function getTempDataFromSession(Session $session)
537
    {
538
        return $session->get(self::classNameWithoutNumber() . ".temp");
539
    }
540
541
    /**
542
     * @param Session $session
543
     * @param boolean $merge Merge everything into a flat array (true by default) or return a multi dimensional array
544
     * @return array
545
     */
546
    public static function getAllDataFromSession(Session $session, $merge = true)
547
    {
548
        $arr = [];
549
        $class = self::classNameWithoutNumber();
550
        foreach (range(1, self::getStepsCount()) as $i) {
551
            if ($merge) {
552
                $step = $session->get($class . ".step_" . $i);
553
                if ($step) {
554
                    $arr = array_merge($arr, $step);
555
                }
556
            } else {
557
                $arr[$i] = $session->get($class . ".step_" . $i);
558
            }
559
        }
560
        return $arr;
561
    }
562
563
    /**
564
     * Utility to quickly scaffold cms facing fields
565
     *
566
     * @param FieldList $fields
567
     * @param array $data
568
     * @return void
569
     */
570
    public static function getAsTabbedFields(FieldList $fields, $data = [])
571
    {
572
        $controller = Controller::curr();
573
        $class = self::classNameWithoutNumber();
574
        foreach (range(1, self::getStepsCount()) as $i) {
575
            $classname = $class . $i;
576
            $inst = new $classname($controller);
577
578
            $stepFields = $inst->Fields();
579
580
            foreach ($stepFields as $sf) {
581
                $name = $sf->getName();
582
583
                $sf->setReadonly(true);
584
                if (!empty($data[$name])) {
585
                    $sf->setValue($data[$name]);
586
                }
587
588
                if ($sf instanceof CompositeField) {
589
                    foreach ($sf->getChildren() as $child) {
590
                        $childName = $child->getName();
591
592
                        if (!empty($data[$childName])) {
593
                            $child->setValue($data[$childName]);
594
                        }
595
                    }
596
                }
597
598
                $fields->addFieldsToTab('Root.Step' . $i, $sf);
599
            }
600
        }
601
    }
602
603
    /**
604
     * @param Session $session
605
     * @param int $step
606
     * @return array
607
     */
608
    public function clearTempDataFromSession(Session $session)
609
    {
610
        return $session->clear(self::classNameWithoutNumber() . ".temp");
0 ignored issues
show
Bug Best Practice introduced by
The expression return $session->clear(s...houtNumber() . '.temp') returns the type SilverStripe\Control\Session which is incompatible with the documented return type array.
Loading history...
611
    }
612
613
    /**
614
     * @param Session $session
615
     * @param int $step
616
     * @return array
617
     */
618
    public function clearDataFromSession(Session $session)
619
    {
620
        return $session->clear(self::classNameWithoutNumber() . ".step_" . self::classNameNumber());
0 ignored issues
show
Bug Best Practice introduced by
The expression return $session->clear(s...elf::classNameNumber()) returns the type SilverStripe\Control\Session which is incompatible with the documented return type array.
Loading history...
621
    }
622
623
    /**
624
     * Clear all infos stored in the session from all steps
625
     * @param Session $session
626
     */
627
    public function clearAllDataFromSession(Session $session)
628
    {
629
        self::clearCurrentStep($session);
630
        $session->clear(self::classNameWithoutNumber());
631
    }
632
633
    public function buildRequestHandler()
634
    {
635
        return new MultiStepFormRequestHandler($this);
636
    }
637
}
638