Passed
Push — master ( 6c5e1b...f0d813 )
by Thomas
03:10
created

MultiStepForm::restoreData()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 2
eloc 5
c 1
b 0
f 1
nc 2
nop 1
dl 0
loc 8
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
            $this->restoreData($session);
87
        }
88
    }
89
90
    /**
91
     * @return FieldList
92
     */
93
    abstract protected function buildFields();
94
95
    /**
96
     * Call this instead of manually creating your actions
97
     *
98
     * You can easily rename actions by calling $actions->fieldByName('action_doNext')->setTitle('...')
99
     *
100
     * @return FieldList
101
     */
102
    protected function buildActions()
103
    {
104
        $actions = new FieldList();
105
106
        $prev = null;
107
        if (self::classNameNumber() > 1) {
108
            $prevLabel = _t('MultiStepForm.doPrev', 'Previous');
109
            $actions->push($prev = new FormAction('doPrev', $prevLabel));
110
            $prev->setUseButtonTag(true);
111
            // this must be supported by your validation client, it works with Zenvalidator
112
            $prev->addExtraClass("ignore-validation");
113
            $prev->addExtraClass("msf-step-prev");
114
        }
115
116
        $label = _t('MultiStepForm.doNext', 'Next');
117
        $actions->push($next = new FormAction('doNext', $label));
118
        $next->setUseButtonTag(true);
119
        $next->addExtraClass('msf-step-next');
120
        if (!$prev) {
121
            $next->addExtraClass('msf-step-next-single');
122
        }
123
        if (self::isLastStep()) {
124
            $next->setTitle(_t('MultiStepForm.doFinish', 'Finish'));
125
            $next->addExtraClass('msf-step-last');
126
        }
127
128
        if ($prev) {
129
            $actions->push($prev);
130
        }
131
132
        $this->addExtraClass('msf');
133
134
        return $actions;
135
    }
136
137
    /**
138
     * @param FieldList $fields
139
     * @return Validator
140
     */
141
    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

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

444
    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...
445
    {
446
    }
447
448
    /**
449
     * A basic previous action that decrements the current step
450
     * @param array $data
451
     * @return HTTPResponse
452
     */
453
    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

453
    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...
454
    {
455
        $controller = $this->getController();
456
        self::decrementStep($controller->getRequest()->getSession());
457
        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...
458
    }
459
460
    /**
461
     * A basic next action that increments the current step and save the data to the session
462
     * @param array $data
463
     * @return HTTPResponse
464
     */
465
    public function doNext($data)
466
    {
467
        $controller = $this->getController();
468
        $session = $controller->getRequest()->getSession();
469
470
        try {
471
            $this->validateData($data);
472
        } catch (ValidationException $ex) {
473
            $this->saveTempDataInSession($session, $data);
474
            $this->sessionError($ex->getMessage());
475
            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...
476
        }
477
478
        $data = $this->processData($data);
479
480
        self::incrementStep($session);
481
        $this->clearTempDataFromSession($session);
482
        $this->saveDataInSession($session, $data);
483
484
        if (self::isLastStep()) {
485
            // You will need to clear the current step and redirect to something else on the last step
486
            throw new Exception("Not implemented: please override doNext in your class");
487
        }
488
489
        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...
490
    }
491
492
    /**
493
     * @param Session $session
494
     * @param int $step
495
     * @return array
496
     */
497
    public static function getDataFromStep(Session $session, $step)
498
    {
499
        return $session->get(self::classNameWithoutNumber() . ".step_" . $step);
500
    }
501
502
    /**
503
     * @param Session $session
504
     * @param array $data
505
     */
506
    public function saveDataInSession(Session $session, array $data = null)
507
    {
508
        if (!$data) {
509
            $data = $this->getData();
510
        }
511
        $session->set(
512
            self::classNameWithoutNumber() . ".step_" . self::classNameNumber(),
513
            $data
514
        );
515
    }
516
517
    /**
518
     * @param Session $session
519
     * @param array $data
520
     */
521
    public function saveTempDataInSession(Session $session, array $data = null)
522
    {
523
        if (!$data) {
524
            $data = $this->getData();
525
        }
526
        $session->set(
527
            self::classNameWithoutNumber() . ".temp",
528
            $data
529
        );
530
    }
531
532
    /**
533
     * @param Session $session
534
     * @return array
535
     */
536
    public function getDataFromSession(Session $session)
537
    {
538
        return $session->get(self::classNameWithoutNumber() . ".step_" . self::classNameNumber());
539
    }
540
541
    /**
542
     * This is the data as submitted by the user
543
     *
544
     * @param Session $session
545
     * @return array
546
     */
547
    public function getTempDataFromSession(Session $session)
548
    {
549
        return $session->get(self::classNameWithoutNumber() . ".temp");
550
    }
551
552
    /**
553
     * @param Session $session
554
     * @param boolean $merge Merge everything into a flat array (true by default) or return a multi dimensional array
555
     * @return array
556
     */
557
    public static function getAllDataFromSession(Session $session, $merge = true)
558
    {
559
        $arr = [];
560
        $class = self::classNameWithoutNumber();
561
        foreach (range(1, self::getStepsCount()) as $i) {
562
            if ($merge) {
563
                $step = $session->get($class . ".step_" . $i);
564
                if ($step) {
565
                    $arr = array_merge($arr, $step);
566
                }
567
            } else {
568
                $arr[$i] = $session->get($class . ".step_" . $i);
569
            }
570
        }
571
        return $arr;
572
    }
573
574
    /**
575
     * Utility to quickly scaffold cms facing fields
576
     *
577
     * @param FieldList $fields
578
     * @param array $data
579
     * @param array $ignore
580
     * @return void
581
     */
582
    public static function getAsTabbedFields(FieldList $fields, $data = [], $ignore = [])
583
    {
584
        $controller = Controller::curr();
585
        $class = self::classNameWithoutNumber();
586
        foreach (range(1, self::getStepsCount()) as $i) {
587
            $classname = $class . $i;
588
            $inst = new $classname($controller);
589
590
            $stepFields = $inst->Fields();
591
592
            foreach ($stepFields as $sf) {
593
                $name = $sf->getName();
594
                if (in_array($name, $ignore)) {
595
                    continue;
596
                }
597
598
                $sf->setReadonly(true);
599
                if (!empty($data[$name])) {
600
                    $sf->setValue($data[$name]);
601
                }
602
603
                if ($sf instanceof CompositeField) {
604
                    foreach ($sf->getChildren() as $child) {
605
                        $childName = $child->getName();
606
                        if (in_array($childName, $ignore)) {
607
                            continue;
608
                        }
609
610
                        if (!empty($data[$childName])) {
611
                            $child->setValue($data[$childName]);
612
                        }
613
                    }
614
                }
615
616
                $fields->addFieldsToTab('Root.Step' . $i, $sf);
617
            }
618
        }
619
    }
620
621
    /**
622
     * @param Session $session
623
     * @param int $step
624
     * @return array
625
     */
626
    public function clearTempDataFromSession(Session $session)
627
    {
628
        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...
629
    }
630
631
    /**
632
     * @param Session $session
633
     * @param int $step
634
     * @return array
635
     */
636
    public function clearDataFromSession(Session $session)
637
    {
638
        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...
639
    }
640
641
    /**
642
     * Clear all infos stored in the session from all steps
643
     * @param Session $session
644
     */
645
    public function clearAllDataFromSession(Session $session)
646
    {
647
        self::clearCurrentStep($session);
648
        $session->clear(self::classNameWithoutNumber());
649
    }
650
651
    public function buildRequestHandler()
652
    {
653
        return new MultiStepFormRequestHandler($this);
654
    }
655
}
656