Passed
Branch master (48d769)
by Michael
02:30
created

ZervWizard   F

Complexity

Total Complexity 76

Size/Duplication

Total Lines 563
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
dl 0
loc 563
rs 2.2388
c 0
b 0
f 0
wmc 76

26 Methods

Rating   Name   Duplication   Size   Complexity  
A getFollowingStep() 0 18 4
A setValue() 0 3 1
A isComplete() 0 3 1
B getStepNumber() 0 17 5
A getExpectedStep() 0 8 2
A stepExists() 0 3 1
D process() 0 78 17
A clearContainer() 0 4 2
A addStep() 0 16 4
A stepCanBeProcessed() 0 17 4
A getFirstStep() 0 5 2
A __construct() 0 19 4
A completeCallback() 0 2 1
A isError() 0 7 2
A setCurrentStep() 0 8 3
A getValue() 0 3 1
A getError() 0 3 2
A coalesce() 0 3 3
A isFirstStep() 0 5 2
A isLastStep() 0 5 2
A doRedirect() 0 7 2
A getStepProperty() 0 8 2
A getFirstIncompleteStep() 0 15 4
A getStepName() 0 3 1
A getPreviousStep() 0 15 3
A addError() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like ZervWizard often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ZervWizard, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 *  Copyright 2005 Zervaas Enterprises (www.zervaas.com.au)
4
 *
5
 *  Licensed under the Apache License, Version 2.0 (the "License");
6
 *  you may not use this file except in compliance with the License.
7
 *  You may obtain a copy of the License at
8
 *
9
 *      http://www.apache.org/licenses/LICENSE-2.0
10
 *
11
 *  Unless required by applicable law or agreed to in writing, software
12
 *  distributed under the License is distributed on an "AS IS" BASIS,
13
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
 *  See the License for the specific language governing permissions and
15
 *  limitations under the License.
16
 */
17
18
/**
19
 * ZervWizard
20
 *
21
 * A class to manage multi-step forms or wizards. This involves managing
22
 * the various steps, storing its values and switching between each
23
 * step
24
 *
25
 * @author  Quentin Zervaas
26
 */
27
class ZervWizard
28
{
29
    // whether or not all steps of the form are complete
30
    public $_complete = false;
31
32
    // internal array to store the various steps
33
    public $_steps = [];
34
35
    // the current step
36
    public $_currentStep = null;
37
38
    // the prefix of the container key where form values are stored
39
    public $_containerPrefix = '__wiz_';
40
41
    // an array of any errors that have occurred
42
    public $_errors = [];
43
44
    // key in container where step status is stored
45
    public $_step_status_key = '__step_complete';
46
47
    // key in container where expected action is stored
48
    public $_step_expected_key = '__expected_action';
49
50
    // options to use for the wizard
51
    public $options = ['redirectAfterPost' => true];
52
53
    // action that resets the container
54
    public $resetAction = '__reset';
55
56
    /**
57
     * ZervWizard
58
     *
59
     * Constructor. Primarily sets up the container
60
     *
61
     * @param array  &$container Reference to container array
62
     * @param string $name       A unique name for the wizard for container storage
63
     */
64
    public function __construct($container, $name)
65
    {
66
        if (!is_array($container)) {
0 ignored issues
show
introduced by
The condition is_array($container) is always true.
Loading history...
67
            $this->addError('container', 'Container not valid');
68
69
            return;
70
        }
71
72
        $containerKey = $this->_containerPrefix . $name;
73
        if (!array_key_exists($containerKey, $container)) {
74
            $container[$containerKey] = [];
75
        }
76
77
        $this->container =& $container[$containerKey];
0 ignored issues
show
Bug Best Practice introduced by
The property container does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
78
79
        if (!array_key_exists('_errors', $this->container)) {
80
            $this->container['_errors'] = [];
81
        }
82
        $this->_errors =& $this->container['_errors'];
83
    }
84
85
    /**
86
     * process
87
     *
88
     * Processes the form for the specified step. If the processed step
89
     * is complete, then the wizard is set to use the next step. If this
90
     * is the initial call to process, then the wizard is set to use the
91
     * first step. Once the next step is determined, the prepare method
92
     * is called for the step. This has the method name prepare_[step name]()
93
     *
94
     * @todo    Need a way to jump between steps, e.g. from step 2 to 4 and validating all data
95
     *
96
     * @param string $action  The step being processed. This should correspond
97
     *                        to a step created in addStep()
98
     * @param array  &$form   The unmodified form values to process
99
     * @param bool   $process True if the step is being processed, false if being prepared
100
     */
101
    public function process($action, $form, $process = true)
102
    {
103
        if ($action == $this->resetAction) {
104
            $this->clearContainer();
105
            $this->setCurrentStep($this->getFirstIncompleteStep());
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->getFirstIncompleteStep() targeting ZervWizard::getFirstIncompleteStep() 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...
106
        } elseif (isset($form['reset'])) {
107
            $this->clearContainer();
108
            $this->setCurrentStep($this->getFirstIncompleteStep());
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->getFirstIncompleteStep() targeting ZervWizard::getFirstIncompleteStep() 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...
109
        } elseif (isset($form['previous']) && !$this->isFirstStep()) {
110
            // clear out errors
111
            $this->_errors = [];
112
113
            $this->setCurrentStep($this->getPreviousStep($action));
114
            $this->doRedirect();
115
        } elseif (isset($form['addvalue']) && !$this->isFirstStep()) {
116
            // clear out errors
117
            $this->_errors = [];
118
119
            // processing callback must exist and validate to proceed
120
            $callback = 'process' . $action;
121
            $complete = method_exists($this, $callback) && $this->$callback($form);
122
123
            $this->container[$this->_step_status_key][$action] = $complete;
0 ignored issues
show
Bug Best Practice introduced by
The property container does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
124
            $this->setCurrentStep($action);
125
        } else {
126
            $proceed = false;
127
128
            // check if the step to be processed is valid
129
            if ('' === $action) {
130
                $action = $this->getExpectedStep();
131
            }
132
133
            if ($this->stepCanBeProcessed($action)) {
134
                if ($this->getStepNumber($action) <= $this->getStepNumber($this->getExpectedStep())) {
135
                    $proceed = true;
136
                } else {
137
                    $proceed = false;
138
                }
139
            }
140
141
            if ($proceed) {
142
                if ($process) {
143
                    // clear out errors
144
                    $this->_errors = [];
145
146
                    // processing callback must exist and validate to proceed
147
                    $callback = 'process' . $action;
148
                    $complete = method_exists($this, $callback) && $this->$callback($form);
149
150
                    $this->container[$this->_step_status_key][$action] = $complete;
151
152
                    if ($complete) {
153
                        $this->setCurrentStep($this->getFollowingStep($action));
154
                    } // all ok, go to next step
155
                    else {
156
                        $this->setCurrentStep($action);
157
                    } // error occurred, redo step
158
159
                    // final processing once complete
160
                    if ($this->isComplete()) {
161
                        $this->completeCallback();
162
                    }
163
164
                    $this->doRedirect();
165
                } else {
166
                    $this->setCurrentStep($action);
167
                }
168
            } else {
169
                // when initally starting the wizard
170
171
                $this->setCurrentStep($this->getFirstIncompleteStep());
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->getFirstIncompleteStep() targeting ZervWizard::getFirstIncompleteStep() 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...
172
            }
173
        }
174
175
        // setup any required data for this step
176
        $callback = 'prepare' . $this->getStepName();
177
        if (method_exists($this, $callback)) {
178
            $this->$callback();
179
        }
180
    }
181
182
    /**
183
     * completeCallback
184
     *
185
     * Function to run once the final step has been processed and is valid.
186
     * This should be overwritten in child classes
187
     */
188
    public function completeCallback()
189
    {
190
    }
191
192
    public function doRedirect()
193
    {
194
        if ($this->coalesce($this->options['redirectAfterPost'], false)) {
195
            $redir = $_SERVER['REQUEST_URI'];
196
            $redir = preg_replace('/\?' . preg_quote($_SERVER['QUERY_STRING'], '/') . '$/', '', $redir);
197
            header('Location: ' . $redir);
198
            exit;
199
        }
200
    }
201
202
    /**
203
     * isComplete
204
     *
205
     * Check if the form is complete. This can only be properly determined
206
     * after process() has been called.
207
     *
208
     * @return bool True if the form is complete and valid, false if not
209
     */
210
    public function isComplete()
211
    {
212
        return $this->_complete;
213
    }
214
215
    /**
216
     * setCurrentStep
217
     *
218
     * Sets the current step in the form. This should generally only be
219
     * called internally but you may have reason to change the current
220
     * step.
221
     *
222
     * @param string $step The step to set as current
223
     */
224
    public function setCurrentStep($step)
225
    {
226
        if (null === $step || !$this->stepExists($step)) {
227
            $this->_complete                            = true;
228
            $this->container[$this->_step_expected_key] = null;
0 ignored issues
show
Bug Best Practice introduced by
The property container does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
229
        } else {
230
            $this->_currentStep                         = $step;
231
            $this->container[$this->_step_expected_key] = $step;
232
        }
233
    }
234
235
    /**
236
     * @return mixed|null
237
     */
238
    public function getExpectedStep()
239
    {
240
        $step = $this->coalesce($this->container[$this->_step_expected_key], null);
241
        if ($this->stepExists($step)) {
242
            return $step;
243
        }
244
245
        return null;
246
    }
247
248
    /**
249
     * stepExists
250
     *
251
     * Check if the given step exists
252
     *
253
     * @param string $stepname The name of the step to check for
254
     *
255
     * @return bool True if the step exists, false if not
256
     */
257
    public function stepExists($stepname)
258
    {
259
        return array_key_exists($stepname, $this->_steps);
260
    }
261
262
    /**
263
     * getStepName
264
     *
265
     * Get the name of the current step
266
     *
267
     * @return string The name of the current step
268
     */
269
    public function getStepName()
270
    {
271
        return $this->_currentStep;
272
    }
273
274
    /**
275
     * getStepNumber
276
     *
277
     * Gets the step number (from 1 to N where N is the number of steps
278
     * in the wizard) of the current step
279
     *
280
     * @param string $step Optional. The step to get the number for. If null then uses current step
281
     *
282
     * @return int The number of the step. 0 if something went wrong
283
     */
284
    public function getStepNumber($step = null)
285
    {
286
        $steps    = array_keys($this->_steps);
287
        $numSteps = count($steps);
288
289
        if ('' === $step) {
290
            $step = $this->getStepName();
291
        }
292
293
        $ret = 0;
294
        for ($n = 1; $n <= $numSteps && 0 == $ret; ++$n) {
295
            if ($step == $steps[$n - 1]) {
296
                $ret = $n;
297
            }
298
        }
299
300
        return $ret;
301
    }
302
303
    /**
304
     * @param $step
305
     *
306
     * @return bool
307
     */
308
    public function stepCanBeProcessed($step)
309
    {
310
        $steps    = array_keys($this->_steps);
311
        $numSteps = count($steps);
312
313
        for ($i = 0; $i < $numSteps; ++$i) {
314
            $_step = $steps[$i];
315
            if ($_step == $step) {
316
                break;
317
            }
318
319
            if (!$this->container[$this->_step_status_key][$_step]) {
320
                return false;
321
            }
322
        }
323
324
        return true;
325
    }
326
327
    /**
328
     * getStepProperty
329
     *
330
     * Retrieve a property for a given step. At this stage, the only
331
     * property steps have is a title property.
332
     *
333
     * @param string $key     The key to get a property for
334
     * @param mixed  $default The value to return if the key isn't found
335
     *
336
     * @return mixed The property value or the default value
337
     */
338
    public function getStepProperty($key, $default = null)
339
    {
340
        $step = $this->getStepName();
341
        if (isset($this->_steps[$step][$key])) {
342
            return $this->_steps[$step][$key];
343
        }
344
345
        return $default;
346
    }
347
348
    /**
349
     * getFirstStep
350
     *
351
     * Get the step name of the first step
352
     *
353
     * @return string The name of the first step, or null if no steps
354
     */
355
    public function getFirstStep()
356
    {
357
        $steps = array_keys($this->_steps);
358
359
        return count($steps) > 0 ? $steps[0] : null;
360
    }
361
362
    /**
363
     * @return null
364
     */
365
    public function getFirstIncompleteStep()
366
    {
367
        $steps    = array_keys($this->_steps);
368
        $numSteps = count($steps);
369
370
        for ($i = 0; $i < $numSteps; ++$i) {
371
            $_step = $steps[$i];
372
373
            if (!array_key_exists($this->_step_status_key, $this->container)
374
                || !$this->container[$this->_step_status_key][$_step]) {
375
                return $_step;
376
            }
377
        }
378
379
        return null;
380
    }
381
382
    /**
383
     * getPreviousStep
384
     *
385
     * Gets the step name of the previous step. If the current
386
     * step is the first step, then null is returned
387
     *
388
     * @param $step
389
     *
390
     * @return string The name of the previous step, or null
391
     */
392
    public function getPreviousStep($step)
393
    {
394
        $ret   = null;
395
        $steps = array_keys($this->_steps);
396
397
        $done = false;
0 ignored issues
show
Unused Code introduced by
The assignment to $done is dead and can be removed.
Loading history...
398
        foreach ($steps as $s) {
399
            if ($s == $step) {
400
                $done = true;
401
                break;
402
            }
403
            $ret = $s;
404
        }
405
406
        return $ret;
407
    }
408
409
    /**
410
     * getFollowingStep
411
     *
412
     * Get the step name of the next step. If the current
413
     * step is the last step, returns null
414
     *
415
     * @param $step
416
     *
417
     * @return string The name of the next step, or null
418
     */
419
    public function getFollowingStep($step)
420
    {
421
        $ret   = null;
422
        $steps = array_keys($this->_steps);
423
424
        $ready = false;
425
        foreach ($steps as $s) {
426
            if ($s == $step) {
427
                $ready = true;
428
            } else {
429
                if ($ready) {
430
                    $ret = $s;
431
                    break;
432
                }
433
            }
434
        }
435
436
        return $ret;
437
    }
438
439
    /**
440
     * addStep
441
     *
442
     * Adds a step to the wizard
443
     *
444
     * @param string $stepname The name of the step
445
     * @param string $title    The title of the current step
446
     */
447
    public function addStep($stepname, $title)
448
    {
449
        if (array_key_exists($stepname, $this->_steps)) {
450
            $this->addError('step', 'Step with name ' . $stepname . ' already exists');
451
452
            return;
453
        }
454
455
        $this->_steps[$stepname] = ['title' => $title];
456
457
        if (!array_key_exists($this->_step_status_key, $this->container)) {
458
            $this->container[$this->_step_status_key] = [];
0 ignored issues
show
Bug Best Practice introduced by
The property container does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
459
        }
460
461
        if (!array_key_exists($stepname, $this->container[$this->_step_status_key])) {
462
            $this->container[$this->_step_status_key][$stepname] = false;
463
        }
464
    }
465
466
    /**
467
     * isFirstStep
468
     *
469
     * Check if the current step is the first step
470
     *
471
     * @return bool True if the current step is the first step
472
     */
473
    public function isFirstStep()
474
    {
475
        $steps = array_keys($this->_steps);
476
477
        return count($steps) > 0 && $steps[0] == $this->getStepName();
478
    }
479
480
    /**
481
     * isLastStep
482
     *
483
     * Check if the current step is the last step
484
     *
485
     * @return bool True if the current step is the last step
486
     */
487
    public function isLastStep()
488
    {
489
        $steps = array_keys($this->_steps);
490
491
        return count($steps) > 0 && array_pop($steps) == $this->getStepName();
492
    }
493
494
    /**
495
     * setValue
496
     *
497
     * Sets a value in the container
498
     *
499
     * @param string $key The key for the value to set
500
     * @param mixed  $val The value
501
     */
502
    public function setValue($key, $val)
503
    {
504
        $this->container[$key] = $val;
0 ignored issues
show
Bug Best Practice introduced by
The property container does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
505
    }
506
507
    /**
508
     * getValue
509
     *
510
     * Gets a value from the container
511
     *
512
     * @param string $key     The key for the value to get
513
     * @param mixed  $default The value to return if the key doesn't exist
514
     *
515
     * @return mixed Either the key's value or the default value
516
     */
517
    public function getValue($key, $default = null)
518
    {
519
        return $this->coalesce($this->container[$key], $default);
520
    }
521
522
    /**
523
     * clearContainer
524
     *
525
     * Removes all data from the container. This is primarily used
526
     * to reset the wizard data completely
527
     */
528
    public function clearContainer()
529
    {
530
        foreach ($this->container as $k => $v) {
531
            unset($this->container[$k]);
532
        }
533
    }
534
535
    /**
536
     * coalesce
537
     *
538
     * Initializes a variable, by returning either the variable
539
     * or a default value
540
     *
541
     * @param mixed &$var    The variable to fetch
542
     * @param mixed $default The value to return if variable doesn't exist or is null
543
     *
544
     * @return mixed The variable value or the default value
545
     */
546
    public function coalesce(&$var, $default = null)
547
    {
548
        return isset($var) && null !== $var ? $var : $default;
549
    }
550
551
    /**
552
     * addError
553
     *
554
     * Add an error
555
     *
556
     * @param string $key An identifier for the error (e.g. the field name)
557
     * @param string $val An error message
558
     */
559
    public function addError($key, $val)
560
    {
561
        $this->_errors[$key] = $val;
562
    }
563
564
    /**
565
     * isError
566
     *
567
     * Check if an error has occurred
568
     *
569
     * @param string $key The field to check for error. If none specified checks for any error
570
     *
571
     * @return bool True if an error has occurred, false if not
572
     */
573
    public function isError($key = null)
574
    {
575
        if (null !== $key) {
576
            return array_key_exists($key, $this->_errors);
577
        }
578
579
        return count($this->_errors) > 0;
580
    }
581
582
    /**
583
     * @param $key
584
     *
585
     * @return null
586
     */
587
    public function getError($key)
588
    {
589
        return array_key_exists($key, $this->_errors) ? $this->_errors[$key] : null;
590
    }
591
}
592