ZervWizard   F
last analyzed

Complexity

Total Complexity 76

Size/Duplication

Total Lines 558
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 157
dl 0
loc 558
rs 2.32
c 0
b 0
f 0
wmc 76

26 Methods

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