Passed
Push — master ( fabe0c...c53c3c )
by Thomas
02:22
created

MultiStepForm::getAsTabbedFields()   B

Complexity

Conditions 7
Paths 6

Size

Total Lines 28
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

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

78
        $data = $this->getDataFromSession($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...
79
        if (!empty($data)) {
80
            $this->loadDataFrom($data);
81
        }
82
    }
83
84
    /**
85
     * @return FieldList
86
     */
87
    abstract protected function buildFields();
88
89
    /**
90
     * Call this instead of manually creating your actions
91
     *
92
     * You can easily rename actions by calling $actions->fieldByName('action_doNext')->setTitle('...')
93
     *
94
     * @return FieldList
95
     */
96
    protected function buildActions()
97
    {
98
        $actions = new FieldList();
99
100
        $prev = null;
101
        if (self::classNameNumber() > 1) {
102
            $prevLabel = _t('MultiStepForm.doPrev', 'Previous');
103
            $actions->push($prev = new FormAction('doPrev', $prevLabel));
104
            $prev->setUseButtonTag(true);
105
            // this must be supported by your validation client, it works with Zenvalidator
106
            $prev->addExtraClass("ignore-validation");
107
            $prev->addExtraClass("msf-step-prev");
108
        }
109
110
        $label = _t('MultiStepForm.doNext', 'Next');
111
        $actions->push($next = new FormAction('doNext', $label));
112
        $next->setUseButtonTag(true);
113
        $next->addExtraClass('msf-step-next');
114
        if (!$prev) {
115
            $next->addExtraClass('msf-step-next-single');
116
        }
117
        if (self::isLastStep()) {
118
            $next->setTitle(_t('MultiStepForm.doFinish', 'Finish'));
119
            $next->addExtraClass('msf-step-last');
120
        }
121
122
        if ($prev) {
123
            $actions->push($prev);
124
        }
125
126
        $this->addExtraClass('msf');
127
128
        return $actions;
129
    }
130
131
    /**
132
     * @param FieldList $fields
133
     * @return Validator
134
     */
135
    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

135
    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...
136
    {
137
        return new RequiredFields;
138
    }
139
140
    public function FormAction()
141
    {
142
        $action = parent::FormAction();
143
        $action .= '?step=' . self::classNameNumber();
144
        return $action;
145
    }
146
147
    /**
148
     * Get a class name without namespace
149
     * @return string
150
     */
151
    public static function getClassWithoutNamespace()
152
    {
153
        $parts = explode("\\", get_called_class());
154
        return array_pop($parts);
155
    }
156
157
    /**
158
     * Get class name without any number in it
159
     * @return string
160
     */
161
    public static function classNameWithoutNumber()
162
    {
163
        return preg_replace('/[0-9]+/', '', self::getClassWithoutNamespace());
164
    }
165
166
    /**
167
     * Get number from class name
168
     * @return string
169
     */
170
    public static function classNameNumber()
171
    {
172
        return preg_replace('/[^0-9]+/', '', self::getClassWithoutNamespace());
173
    }
174
175
    /**
176
     * Get class name for current step based on this class name
177
     * @param Controller $controller
178
     * @return string
179
     */
180
    public static function classForCurrentStep($controller = null)
181
    {
182
        if (!$controller) {
183
            $controller = Controller::curr();
184
        }
185
186
        $request = $controller->getRequest();
187
188
        // Defaults to step 1
189
        $step = 1;
190
191
        // Check session
192
        $sessionStep = self::getCurrentStep($request->getSession());
193
        if ($sessionStep) {
194
            $step = $sessionStep;
195
        }
196
        // Override with step set manually
197
        $requestStep = $request->getVar('step');
198
        if ($requestStep) {
199
            $step = $requestStep;
200
        }
201
202
        return str_replace(self::classNameNumber(), $step, self::getClassWithoutNamespace());
203
    }
204
205
    /**
206
     * Get all steps as an ArrayList. To be used for your templates.
207
     * @return ArrayList
208
     */
209
    public function AllSteps()
210
    {
211
        $num = self::classNameNumber();
212
        if (!$num) {
213
            return;
214
        }
215
        $controller = Controller::curr();
216
        $n = 1;
217
        $curr = self::getCurrentStep($controller->getRequest()->getSession());
218
        if (!$curr) {
219
            $curr = 1;
220
        }
221
        $class = str_replace($num, $n, self::getClassWithoutNamespace());
222
        $steps = new ArrayList();
223
224
        $baseAction = parent::FormAction();
225
        $config = self::config();
226
227
        while (class_exists($class)) {
228
            $isCurrent = $isCompleted = $isNotCompleted = false;
229
            $cssClass = $n == $curr ? $config->class_active : $config->class_inactive;
230
            if ($n == 1) {
231
                $isCurrent = true;
232
                $cssClass .= ' first';
233
            }
234
            if ($class::isLastStep()) {
235
                $cssClass .= ' last';
236
            }
237
            if ($n < $curr) {
238
                $isCompleted = true;
239
                $cssClass .= ' ' . $config->class_completed;
240
            }
241
            if ($n > $curr) {
242
                $isNotCompleted = true;
243
                $cssClass .= ' ' . $config->class_not_completed;
244
            }
245
            $link = rtrim($baseAction, '/') . '/gotoStep/?step=' . $n;
246
            $steps->push(new ArrayData(array(
247
                'Title' => $class::getStepTitle(),
248
                'Number' => $n,
249
                'Link' => $isNotCompleted ? null : $link,
250
                'Class' => $cssClass,
251
                'IsCurrent' => $isCurrent,
252
                'IsCompleted' => $isCompleted,
253
                'isNotCompleted' => $isNotCompleted,
254
            )));
255
            $n++;
256
            $class = str_replace(self::classNameNumber(), $n, self::getClassWithoutNamespace());
257
        }
258
        return $steps;
259
    }
260
261
    /**
262
     * @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...
263
     */
264
    public function DisplaySteps()
265
    {
266
        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...
267
    }
268
269
    /**
270
     * Clear current step
271
     * @param Session $session
272
     * @return void
273
     */
274
    public static function clearCurrentStep($session)
275
    {
276
        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...
277
    }
278
279
    /**
280
     * Get current step (defined in session). 0 if not started yet.
281
     * @param Session $session
282
     * @return int
283
     */
284
    public static function getCurrentStep($session)
285
    {
286
        return (int) $session->get(self::classNameWithoutNumber() . '.step');
287
    }
288
289
    /**
290
     * Set max step
291
     * @param Session $session
292
     * @param int $value
293
     * @return void
294
     */
295
    public static function setMaxStep($session, $value)
296
    {
297
        $session->set(self::classNameWithoutNumber() . '.maxStep', (int) $value);
298
    }
299
300
    /**
301
     * Get max step (defined in session). 0 if not started yet.
302
     * @param Session $session
303
     * @return int
304
     */
305
    public static function getMaxStep($session)
306
    {
307
        return (int) $session->get(self::classNameWithoutNumber() . '.maxStep');
308
    }
309
310
    /**
311
     * Set current step
312
     * @param Session $session
313
     * @param int $value
314
     * @return void
315
     */
316
    public static function setCurrentStep($session, $value)
317
    {
318
        $value = (int) $value;
319
320
        // Track highest step for step navigation
321
        if ($value > self::getMaxStep($session)) {
322
            self::setMaxStep($session, $value);
323
        }
324
325
        $session->set(self::classNameWithoutNumber() . '.step', $value);
326
    }
327
328
    /**
329
     * @return int
330
     */
331
    public static function getStepsCount()
332
    {
333
        $class = self::classNameWithoutNumber();
334
        $i = 1;
335
        $stepClass = $class . $i;
336
        while (class_exists($stepClass)) {
337
            $i++;
338
            $stepClass = $class . $i;
339
        }
340
        return --$i;
341
    }
342
343
    /**
344
     * Increment step
345
     * @param Session $session
346
     * @return string
347
     */
348
    public static function incrementStep($session)
349
    {
350
        if (self::isLastStep()) {
351
            return;
352
        }
353
        $next = self::classNameNumber() + 1;
354
        if ($next == 1) {
355
            $next++;
356
        }
357
        return self::setCurrentStep($session, $next);
0 ignored issues
show
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...
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...
358
    }
359
360
    /**
361
     * Decrement step
362
     * @param Session $session
363
     * @return string
364
     */
365
    public static function decrementStep($session)
366
    {
367
        $prev = self::classNameNumber() - 1;
368
        if ($prev < 1) {
369
            return;
370
        }
371
        return self::setCurrentStep($session, $prev);
0 ignored issues
show
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...
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...
372
    }
373
374
    /**
375
     * Goto a step
376
     * @param Session $session
377
     * @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...
378
     */
379
    public function gotoStep($session)
380
    {
381
        $step = $this->getController()->getRequest()->getVar('step');
382
        if ($step > 0 && $step <= self::getMaxStep($session)) {
383
            self::setCurrentStep($session, $step);
384
        }
385
        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...
386
    }
387
388
    /**
389
     * Check if this is the last step
390
     * @return bool
391
     */
392
    public static function isLastStep()
393
    {
394
        $n = self::classNameNumber();
395
        $n1 = $n + 1;
396
        $class = str_replace($n, $n1, self::getClassWithoutNamespace());
397
        return !class_exists($class);
398
    }
399
400
    /**
401
     * Return the step name
402
     * @return string
403
     */
404
    abstract public static function getStepTitle();
405
406
    /**
407
     * A basic previous action that decrements the current step
408
     * @param array $data
409
     * @return HTTPResponse
410
     */
411
    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

411
    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...
412
    {
413
        $controller = $this->getController();
414
        self::decrementStep($controller->getRequest()->getSession());
415
        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...
416
    }
417
418
    /**
419
     * A basic next action that increments the current step and save the data to the session
420
     * @param array $data
421
     * @return HTTPResponse
422
     */
423
    public function doNext($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

423
    public function doNext(/** @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...
424
    {
425
        $controller = $this->getController();
426
        $session = $controller->getRequest()->getSession();
427
        self::incrementStep($session);
428
        $this->saveDataInSession($session);
429
430
        if (self::isLastStep()) {
431
            // You will need to clear the current step and redirect to something else on the last step
432
            throw new Exception("Not implemented: please override doNext in your class");
433
        }
434
435
        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...
436
    }
437
438
    /**
439
     * @param Session $session
440
     * @param int $step
441
     * @return array
442
     */
443
    public static function getDataFromStep($session, $step)
444
    {
445
        return $session->get(self::classNameWithoutNumber() . ".step_" . $step);
446
    }
447
448
    /**
449
     * @param Session $session
450
     */
451
    public function saveDataInSession($session)
452
    {
453
        $session->set(
454
            self::classNameWithoutNumber() . ".step_" . self::classNameNumber(),
455
            $this->getData()
456
        );
457
    }
458
459
    /**
460
     * @param Session $session
461
     * @return array
462
     */
463
    public function getDataFromSession($session)
464
    {
465
        return $session->get(self::classNameWithoutNumber() . ".step_" . self::classNameNumber());
466
    }
467
468
    /**
469
     * @param Session $session
470
     * @param boolean $merge Merge everything into a flat array (true by default) or return a multi dimensional array
471
     * @return array
472
     */
473
    public static function getAllDataFromSession($session, $merge = true)
474
    {
475
        $arr = [];
476
        $class = self::classNameWithoutNumber();
477
        foreach (range(1, self::getStepsCount()) as $i) {
478
            if ($merge) {
479
                $step = $session->get($class . ".step_" . $i);
480
                if ($step) {
481
                    $arr = array_merge($arr, $step);
482
                }
483
            } else {
484
                $arr[$i] = $session->get($class . ".step_" . $i);
485
            }
486
        }
487
        return $arr;
488
    }
489
490
    /**
491
     * Utility to quickly scaffold cms facing fields
492
     *
493
     * @param FieldList $fields
494
     * @param array $data
495
     * @return void
496
     */
497
    public static function getAsTabbedFields(FieldList $fields, $data = [])
498
    {
499
        $controller = Controller::curr();
500
        $class = self::classNameWithoutNumber();
501
        foreach (range(1, self::getStepsCount()) as $i) {
502
            $classname = $class . $i;
503
            $inst = new $classname($controller);
504
505
            $stepFields = $inst->Fields();
506
507
            foreach ($stepFields as $sf) {
508
                $name = $sf->getName();
509
510
                $sf->setReadonly(true);
511
                if (!empty($data[$name])) {
512
                    $sf->setValue($data[$name]);
513
                }
514
515
                if ($sf instanceof CompositeField) {
516
                    foreach ($sf->getChildren() as $child) {
517
                        $childName = $child->getName();
518
                        if (!empty($data[$childName])) {
519
                            $child->setValue($data[$childName]);
520
                        }
521
                    }
522
                }
523
524
                $fields->addFieldsToTab('Root.Step' . $i, $sf);
525
            }
526
        }
527
    }
528
529
    /**
530
     * @param Session $session
531
     * @param int $step
532
     * @return array
533
     */
534
    public function clearDataFromSession($session)
535
    {
536
        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...
537
    }
538
539
    /**
540
     * Clear all infos stored in the session from all steps
541
     * @param Session $session
542
     */
543
    public function clearAllDataFromSession($session)
544
    {
545
        self::clearCurrentStep($session);
546
        $session->clear(self::classNameWithoutNumber());
547
    }
548
549
    public function buildRequestHandler()
550
    {
551
        return new MultiStepFormRequestHandler($this);
552
    }
553
}
554