Test Failed
Push — master ( 194522...eda1b6 )
by Mikael
01:43
created

Form::offsetUnset()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 1
dl 0
loc 4
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace Anax\HTMLForm;
4
5
/**
6
 * A utility class to easy creating and handling of forms
7
 */
8
class Form implements \ArrayAccess
9
{
10
11
    /**
12
     * Properties
13
     */
14
    public $form;     // array with settings for the form
15
    public $elements; // array with all form elements
16
    public $output;   // array with messages to display together with the form
17
    public $session;  // array with key values for the session
18
19
20
21
    /**
22
     * Constructor
23
     *
24
     * @param array $form     details for the form
25
     * @param array $elements all the elements
26
     */
27
    public function __construct($form = [], $elements = [])
28
    {
29
        $this->create($form, $elements);
30
    }
31
32
33
34
    /**
35
     * Implementing ArrayAccess for this->elements
36
     */
37
    public function offsetSet($offset, $value)
38
    {
39 View Code Duplication
        if (is_null($offset)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
40
            $this->elements[] = $value;
41
        } else {
42
            $this->elements[$offset] = $value;
43
        }
44
    }
45
    
46
    public function offsetExists($offset)
47
    {
48
        return isset($this->elements[$offset]);
49
    }
50
    
51
    public function offsetUnset($offset)
52
    {
53
        unset($this->elements[$offset]);
54
    }
55
    
56
    public function offsetGet($offset)
57
    {
58
        return isset($this->elements[$offset])
59
            ? $this->elements[$offset]
60
            : null;
61
    }
62
63
64
65
    /**
66
     * Add a form element
67
     *
68
     * @param array $form     details for the form
69
     * @param array $elements all the elements
70
     *
71
     * @return $this
72
     */
73
    public function create($form = [], $elements = [])
74
    {
75
        $defaults = [
76
            // Always have a id for the form
77
            "id" => "anax/htmlform",
78
        ];
79
        $this->form = array_merge($defaults, $form);
80
81
        $this->elements = [];
82
        if (!empty($elements)) {
83
            foreach ($elements as $key => $element) {
84
                $this->elements[$key] = FormElement::Create($key, $element);
85
            }
86
        }
87
88
        $this->output = [];
89
90
        // Setting keys used in the session
91
        $generalKey = "anax/htmlform-" . $this->form["id"] . "#";
92
        $this->session = [
93
            "save"      => $generalKey . "save",
94
            "output"    => $generalKey . "output",
95
            "failed"    => $generalKey . "failed",
96
            "remember"  => $generalKey . "remember",
97
        ];
98
99
        return $this;
100
    }
101
102
103
104
    /**
105
     * Add a form element
106
     *
107
     * @param FormElement $element the formelement to add.
108
     *
109
     * @return $this
110
     */
111
    public function addElement($element)
112
    {
113
        $this[$element['name']] = $element;
114
        return $this;
115
    }
116
117
118
119
    /**
120
     * Remove an form element
121
     *
122
     * @param string $name the name of the element to remove from the form.
123
     *
124
     * @return $this
125
     */
126
    public function removeElement($name)
127
    {
128
        unset($this->elements[$name]);
129
        return $this;
130
    }
131
132
133
134
    /**
135
     * Set validation to a form element
136
     *
137
     * @param string $element the name of the formelement to add validation rules to.
138
     * @param array  $rules   array of validation rules.
139
     *
140
     * @return $this
141
     */
142
    public function setValidation($element, $rules)
143
    {
144
        $this[$element]['validation'] = $rules;
145
        return $this;
146
    }
147
148
149
150
    /**
151
     * Add output to display to the user what happened whith the form.
152
     *
153
     * @param string $str the string to add as output.
154
     *
155
     * @return $this.
0 ignored issues
show
Documentation introduced by
The doc-type $this. could not be parsed: Unknown type name "$this." at position 0. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
156
     */
157
    public function addOutput($str)
158
    {
159
        $key = $this->session["output"];
160
        if (isset($_SESSION[$key])) {
161
            $_SESSION[$key] .= " $str";
162
        } else {
163
            $_SESSION[$key] = $str;
164
        }
165
        return $this;
166
    }
167
168
169
170
    /**
171
     * Get value of a form element
172
     *
173
     * @param string $element the name of the formelement.
174
     *
175
     * @return mixed the value of the element.
176
     */
177
    public function value($element)
178
    {
179
        return $this[$element]->value();
180
    }
181
182
183
184
    /**
185
     * Return HTML for the form or the formdefinition.
186
     *
187
     * @param array $options with options affecting the form output.
188
     *
189
     * @return string with HTML for the form.
190
     */
191
    public function getHTML($options = [])
192
    {
193
        $defaults = [
194
            // Only return the start of the form element
195
            'start'         => false,
196
            
197
            // Layout all elements in one column
198
            'columns'       => 1,
199
            
200
            // Layout consequtive buttons as one element wrapped in <p>
201
            'use_buttonbar' => true,
202
203
            // Wrap fields within <fieldset>
204
            'use_fieldset'  => true,
205
206
            // Use legend for fieldset
207
            'legend'        => isset($this->form['legend']) ?
208
                $this->form['legend']
209
                : null,
210
        ];
211
        $options = array_merge($defaults, $options);
212
213
        $form = array_merge($this->form, $options);
214
        $id      = isset($form['id'])      ? " id='{$form['id']}'" : null;
215
        $class   = isset($form['class'])   ? " class='{$form['class']}'" : null;
216
        $name    = isset($form['name'])    ? " name='{$form['name']}'" : null;
217
        $action  = isset($form['action'])  ? " action='{$form['action']}'" : null;
218
        $method  = isset($form['method'])  ? " method='{$form['method']}'" : " method='post'";
219
        $enctype = isset($form['enctype']) ? " enctype='{$form['enctype']}'" : null;
220
        $cformId = isset($form['id'])      ? "{$form['id']}" : null;
221
222
        if ($options['start']) {
223
            return "<form{$id}{$class}{$name}{$action}{$method}>\n";
224
        }
225
226
        $fieldsetStart  = '<fieldset>';
227
        $legend         = null;
228
        $fieldsetEnd    = '</fieldset>';
229
        if (!$options['use_fieldset']) {
230
            $fieldsetStart = $fieldsetEnd = null;
231
        }
232
233
        if ($options['use_fieldset'] && $options['legend']) {
234
            $legend = "<legend>{$options['legend']}</legend>";
235
        }
236
237
        $elementsArray  = $this->GetHTMLForElements($options);
238
        $elements       = $this->GetHTMLLayoutForElements($elementsArray, $options);
239
        $output         = $this->GetOutput();
240
241
        $html = <<< EOD
242
\n<form{$id}{$class}{$name}{$action}{$method}{$enctype}>
243
<input type="hidden" name="anax/htmlform-id" value="$cformId" />
244
{$fieldsetStart}
245
{$legend}
246
{$elements}
247
{$output}
248
{$fieldsetEnd}
249
</form>\n
250
EOD;
251
252
        return $html;
253
    }
254
255
256
257
    /**
258
     * Return HTML for the elements
259
     *
260
     * @param array $options with options affecting the form output.
261
     *
262
     * @return array with HTML for the formelements.
263
     */
264
    public function getHTMLForElements($options = [])
265
    {
266
        $defaults = [
267
            'use_buttonbar' => true,
268
        ];
269
        $options = array_merge($defaults, $options);
270
271
        $elements = array();
272
        reset($this->elements);
273
        while (list($key, $element) = each($this->elements)) {
0 ignored issues
show
Unused Code introduced by
The assignment to $key is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
274
            
275
            if (in_array($element['type'], array('submit', 'reset', 'button'))
276
                && $options['use_buttonbar']
277
            ) {
278
279
                // Create a buttonbar
280
                $name = 'buttonbar';
281
                $html = "<p class='buttonbar'>\n" . $element->GetHTML() . '&nbsp;';
282
283
                // Get all following submits (and buttons)
284
                while (list($key, $element) = each($this->elements)) {
0 ignored issues
show
Unused Code introduced by
The assignment to $key is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
285
                    if (in_array($element['type'], array('submit', 'reset', 'button'))) {
286
                        $html .= $element->GetHTML();
287
                    } else {
288
                        prev($this->elements);
289
                        break;
290
                    }
291
                }
292
                $html .= "\n</p>";
293
294
            } else {
295
296
                // Just add the element
297
                $name = $element['name'];
298
                $html = $element->GetHTML();
299
            }
300
301
            $elements[] = array('name'=>$name, 'html'=> $html);
302
        }
303
304
        return $elements;
305
    }
306
307
308
309
310
    /**
311
     * Place the elements according to a layout and return the HTML
312
     *
313
     * @param array $elements as returned from GetHTMLForElements().
314
     * @param array $options  with options affecting the layout.
315
     *
316
     * @return array with HTML for the formelements.
317
     */
318
    public function getHTMLLayoutForElements($elements, $options = [])
319
    {
320
        $defaults = [
321
            'columns' => 1,
322
            'wrap_at_element' => false,  // Wraps column in equal size or at the set number of elements
323
        ];
324
        $options = array_merge($defaults, $options);
325
326
        $html = null;
327
        if ($options['columns'] === 1) {
328
329
            foreach ($elements as $element) {
330
                $html .= $element['html'];
331
            }
332
333
        } elseif ($options['columns'] === 2) {
334
335
            $buttonbar = null;
336
            $col1 = null;
337
            $col2 = null;
338
339
            $e = end($elements);
340
            if ($e['name'] == 'buttonbar') {
341
                $e = array_pop($elements);
342
                $buttonbar = "<div class='cform-buttonbar'>\n{$e['html']}</div>\n";
343
            }
344
345
            $size = count($elements);
346
            $wrapAt = $options['wrap_at_element'] ? $options['wrap_at_element'] : round($size/2);
347
            for ($i=0; $i<$size; $i++) {
348
                if ($i < $wrapAt) {
349
                    $col1 .= $elements[$i]['html'];
350
                } else {
351
                    $col2 .= $elements[$i]['html'];
352
                }
353
            }
354
355
            $html = <<<EOD
356
<div class='cform-columns-2'>
357
<div class='cform-column-1'>
358
{$col1}
359
</div>
360
<div class='cform-column-2'>
361
{$col2}
362
</div>
363
{$buttonbar}</div>
364
EOD;
365
        }
366
367
        return $html;
368
    }
369
370
371
372
    /**
373
     * Get an array with all elements that failed validation together with their id and validation message.
374
     *
375
     * @return array with elements that failed validation.
376
     */
377
    public function getValidationErrors()
378
    {
379
        $errors = [];
380
        foreach ($this->elements as $name => $element) {
381
            if ($element['validation-pass'] === false) {
382
                $errors[$name] = [
383
                    'id' => $element->GetElementId(),
384
                    'label' => $element['label'],
385
                    'message' => implode(' ', $element['validation-messages'])
386
                ];
387
            }
388
        }
389
        return $errors;
390
    }
391
392
393
394
    /**
395
     * Get output messages as <output>.
396
     *
397
     * @return string|null with the complete <output> element or null if no output.
398
     */
399
    public function getOutput()
400
    {
401
        return !empty($this->output)
402
            ? "<output>{$this->output}</output>"
403
            : null;
404
    }
405
406
407
408
    /**
409
     * Init all element with values from session, clear all and fill in with values from the session.
410
     *
411
     * @param array $values retrieved from session
412
     *
413
     * @return void
414
     */
415
    protected function initElements($values)
416
    {
417
        // First clear all
418
        foreach ($this->elements as $key => $val) {
419
            // Do not reset value for buttons
420
            if (in_array($this[$key]['type'], array('submit', 'reset', 'button'))) {
421
                continue;
422
            }
423
424
            // Reset the value
425
            $this[$key]['value'] = null;
426
427
            // Checkboxes must be cleared
428
            if (isset($this[$key]['checked'])) {
429
                $this[$key]['checked'] = false;
430
            }
431
        }
432
433
        // Now build up all values from $values (session)
434
        foreach ($values as $key => $val) {
435
436
            // Take care of arrays as values (multiple-checkbox)
437
            if (isset($val['values'])) {
438
                $this[$key]['checked'] = $val['values'];
439
                //$this[$key]['values']  = $val['values'];
440
            } elseif (isset($val['value'])) {
441
                $this[$key]['value'] = $val['value'];
442
            }
443
444
            if ($this[$key]['type'] === 'checkbox') {
445
                $this[$key]['checked'] = true;
446
            } elseif ($this[$key]['type'] === 'radio') {
447
                $this[$key]['checked'] = $val['value'];
448
            }
449
450
            // Keep track on validation messages if set
451
            if (isset($val['validation-messages'])) {
452
                $this[$key]['validation-messages'] = $val['validation-messages'];
453
                $this[$key]['validation-pass'] = false;
454
            }
455
        }
456
    }
457
458
459
460
    /**
461
     * Check if a form was submitted and perform validation and call callbacks.
462
     * The form is stored in the session if validation or callback fails. The page should then be redirected
463
     * to the original form page, the form will populate from the session and should be rendered again.
464
     * Form elements may remember their value if 'remember' is set and true.
465
     *
466
     * @param callable $callIfSuccess handler to call if function returns true.
467
     * @param callable $callIfFail    handler to call if function returns true.
468
     *
469
     * @return boolean|null $callbackStatus if submitted&validates, false if not validate, null if not submitted.
470
     *         If submitted the callback function will return the actual value which should be true or false.
471
     */
472
    public function check($callIfSuccess = null, $callIfFail = null)
473
    {
474
        $remember = null;
475
        $validates = null;
0 ignored issues
show
Unused Code introduced by
$validates is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
476
        $callbackStatus = null;
477
        $values = [];
478
479
        // Remember output messages in session
480
        $output = $this->session["output"];
481
        if (isset($_SESSION[$output])) {
482
            $this->output = $_SESSION[$output];
483
            unset($_SESSION[$output]);
484
        }
485
486
        // Check if this was a post request
487
        if ($_SERVER["REQUEST_METHOD"] !== "POST") {
488
            // Its not posted, but check if values should be used from session
489
            $failed   = $this->session["failed"];
490
            $remember = $this->session["remember"];
491
            $save     = $this->session["save"];
492
            
493
            if (isset($_SESSION[$failed])) {
494
495
                // Read form data from session if the previous post failed during validation.
496
                $this->InitElements($_SESSION[$failed]);
497
                unset($_SESSION[$failed]);
498
499
            } elseif (isset($_SESSION[$remember])) {
500
501
                // Read form data from session if some form elements should be remembered
502
                foreach ($_SESSION[$remember] as $key => $val) {
503
                    $this[$key]['value'] = $val['value'];
504
                }
505
                unset($_SESSION[$remember]);
506
507
            } elseif (isset($_SESSION[$save])) {
508
509
                // Read form data from session,
510
                // useful during test where the original form is displayed with its posted values
511
                $this->InitElements($_SESSION[$save]);
512
                unset($_SESSION[$save]);
513
            }
514
            
515
            return null;
516
        }
517
        $request = $_POST;
518
519
        // Check if its a form we are dealing with
520
        if (!isset($request["anax/htmlform-id"])) {
521
            return null;
522
        }
523
524
        // Check if its this form that was posted
525
        if ($this->form["id"] !== $request["anax/htmlform-id"]) {
526
            return null;
527
        }
528
529
        // This form was posted, process it
530
        if (isset($_SESSION[$this->session["failed"]])) {
531
            unset($_SESSION[$this->session["failed"]]);
532
        }
533
        
534
        $validates = true;
535
        foreach ($this->elements as $element) {
536
537
            $elementName = $element['name'];
538
            $elementType = $element['type'];
539
540
            // The form element has a value set
541
            if (isset($request[$elementName])) {
542
543
                // Multiple choices comes in the form of an array
544
                if (is_array($request[$elementName])) {
545
                    $values[$elementName]['values'] = $element['checked'] = $request[$elementName];
546
                } else {
547
                    $values[$elementName]['value'] = $element['value'] = $request[$elementName];
548
                }
549
550
                // If the element is a password, do not remember.
551
                if ($elementType === 'password') {
552
                    $values[$elementName]['value'] = null;
553
                }
554
555
                // If the element is a checkbox, set its value of checked.
556
                if ($elementType === 'checkbox') {
557
                    $element['checked'] = true;
558
                }
559
560
                // If the element is a radio, set the value to checked.
561
                if ($elementType === 'radio') {
562
                    $element['checked'] = $element['value'];
563
                }
564
565
                // Do validation of form element
566 View Code Duplication
                if (isset($element['validation'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
567
568
                    $element['validation-pass'] = $element->Validate($element['validation'], $this);
569
570
                    if ($element['validation-pass'] === false) {
571
572
                        $values[$elementName] = [
573
                            'value' => $element['value'],
574
                            'validation-messages' => $element['validation-messages']
575
                        ];
576
                        $validates = false;
577
578
                    }
579
                }
580
581
                // Hmmm.... Why did I need this remember thing?
582
                if (isset($element['remember'])
583
                    && $element['remember']
584
                ) {
585
                    $values[$elementName] = ['value' => $element['value']];
586
                    $remember = true;
587
                }
588
589
                // Carry out the callback if the form element validates
590
                // Hmmm, what if the element with the callback is not the last element?
591
                if (isset($element['callback'])
592
                    && $validates
593
                ) {
594
595
                    if (isset($element['callback-args'])) {
596
597
                        $callbackStatus = call_user_func_array(
598
                            $element['callback'],
599
                            array_merge([$this]),
600
                            $element['callback-args']
601
                        );
602
603
                    } else {
604
605
                        $callbackStatus = call_user_func($element['callback'], $this);
606
                    }
607
                }
608
609
            } else {
610
611
                // The form element has no value set
612
613
                // Set element to null, then we know it was not set.
614
                //$element['value'] = null;
615
                //echo $element['type'] . ':' . $elementName . ':' . $element['value'] . '<br>';
616
617
                // If the element is a checkbox, clear its value of checked.
618
                if ($element['type'] === 'checkbox'
619
                    || $element['type'] === 'checkbox-multiple'
620
                ) {
621
622
                    $element['checked'] = false;
623
                }
624
625
                // Do validation even when the form element is not set?
626
                // Duplicate code, revise this section and move outside
627
                // this if-statement?
628 View Code Duplication
                if (isset($element['validation'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
629
630
                    $element['validation-pass'] = $element->Validate($element['validation'], $this);
631
632
                    if ($element['validation-pass'] === false) {
633
634
                        $values[$elementName] = [
635
                            'value' => $element['value'], 'validation-messages' => $element['validation-messages']
636
                        ];
637
                        $validates = false;
638
                    }
639
                }
640
            }
641
        }
642
643
        // Prepare if data should be stored in the session during redirects
644
        // Did form validation or the callback fail?
645
        if ($validates === false
646
            || $callbackStatus === false
647
        ) {
648
649
            $_SESSION[$this->session["failed"]] = $values;
650
651
        } elseif ($remember) {
652
653
            // Hmmm, why do I want to use this
654
            $_SESSION[$this->session["remember"]] = $values;
655
        }
656
657
        if (isset($this->saveInSession) && $this->saveInSession) {
0 ignored issues
show
Bug introduced by
The property saveInSession does not seem to exist. Did you mean session?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
658
659
            // Remember all posted values
660
            $_SESSION[$this->session["save"]] = $values;
661
        }
662
663
        // Lets se what the return value should be
664
        $ret = $validates
665
            ? $callbackStatus
666
            : $validates;
667
668
669
        if ($ret === true && isset($callIfSuccess)) {
670
671
            // Use callback for success, if defined
672
            if (is_callable($callIfSuccess)) {
673
                call_user_func_array($callIfSuccess, [$this]);
674
            } else {
675
                throw new \Exception("Form, success-method is not callable.");
676
            }
677
678
        } elseif ($ret === false && isset($callIfFail)) {
679
680
            // Use callback for fail, if defined
681
            if (is_callable($callIfFail)) {
682
                call_user_func_array($callIfFail, [$this]);
683
            } else {
684
                throw new \Exception("Form, success-method is not callable.");
685
            }
686
        }
687
688
        return $ret;
689
    }
690
}
691