Completed
Push — master ( 5978c7...5a86a1 )
by Gabor
12:24
created

FormElement::setOption()   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 27
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 0
Metric Value
dl 0
loc 27
ccs 0
cts 20
cp 0
rs 8.5806
c 0
b 0
f 0
cc 4
eloc 14
nc 4
nop 4
crap 20
1
<?php
2
/**
3
 * WebHemi.
4
 *
5
 * PHP version 5.6
6
 *
7
 * @copyright 2012 - 2016 Gixx-web (http://www.gixx-web.com)
8
 * @license   https://opensource.org/licenses/MIT The MIT License (MIT)
9
 *
10
 * @link      http://www.gixx-web.com
11
 */
12
namespace WebHemi\Form;
13
14
use Iterator;
15
use InvalidArgumentException;
16
use RuntimeException;
17
use WebHemi\Form\Validator\FormValidatorInterface;
18
19
/**
20
 * Class FormElement
21
 */
22
final class FormElement implements Iterator
23
{
24
    /** HTML5 form elements */
25
    const TAG_FORM = 'form';
26
    const TAG_INPUT_CHECKBOX = 'checkbox';
27
    const TAG_INPUT_COLOR = 'color';
28
    const TAG_INPUT_DATA = 'date';
29
    const TAG_INPUT_DATETIME = 'datetime';
30
    const TAG_INPUT_DATETIME_LOCAL = 'datetime-local';
31
    const TAG_INPUT_EMAIL = 'email';
32
    const TAG_INPUT_FILE = 'file';
33
    const TAG_INPUT_HIDDEN = 'hidden';
34
    const TAG_INPUT_IMAGE = 'image';
35
    const TAG_INPUT_MONTH = 'month';
36
    const TAG_INPUT_NUMBER = 'number';
37
    const TAG_INPUT_PASSWORD = 'password';
38
    const TAG_INPUT_RADIO = 'radio';
39
    const TAG_INPUT_RANGE = 'range';
40
    const TAG_INPUT_SEARCH = 'search';
41
    const TAG_INPUT_TEL = 'tel';
42
    const TAG_INPUT_TEXT = 'text';
43
    const TAG_INPUT_TIME = 'time';
44
    const TAG_INPUT_URL = 'url';
45
    const TAG_INPUT_WEEK = 'week';
46
    const TAG_TEXTAREA = 'textarea';
47
    const TAG_FIELDSET = 'fieldset';
48
    const TAG_LEGEND = 'legend';
49
    const TAG_LABEL = 'label';
50
    const TAG_BUTTON_SUBMIT = 'submit';
51
    const TAG_BUTTON_RESET = 'reset';
52
    const TAG_BUTTON = 'button';
53
    const TAG_DATALIST = 'datalist';
54
    const TAG_SELECT = 'select';
55
    const TAG_OPTION_GROUP = 'optgroup';
56
    const TAG_OPTION = 'option';
57
    const TAG_KEYGEN = 'keygen';
58
    const TAG_OUTPUT = 'output';
59
60
    /** @var int */
61
    protected static $tabIndex = 1;
62
    /** @var string */
63
    private $tagName;
64
    /** @var string */
65
    private $name;
66
    /** @var string */
67
    private $uniqueFormNamePostfix = '';
68
    /** @var string */
69
    private $label;
70
    /** @var mixed */
71
    private $value;
72
    /** @var array */
73
    private $options = [];
74
    /** @var array */
75
    private $optionGroups = [];
76
    /** @var array */
77
    private $attributes;
78
    /** @var FormElement */
79
    private $parentNode;
80
    /** @var array<FormElement> */
81
    private $childNodes;
82
    /** @var array<FormValidatorInterface> */
83
    private $validators;
84
    /** @var array */
85
    private $mandatoryTagParents = [
86
        self::TAG_FORM => [],
87
        self::TAG_LEGEND => [
88
            self::TAG_FIELDSET
89
        ],
90
        self::TAG_OPTION => [
91
            self::TAG_DATALIST,
92
            self::TAG_OPTION_GROUP,
93
            self::TAG_SELECT
94
        ],
95
        self::TAG_OPTION_GROUP => [
96
            self::TAG_SELECT
97
        ],
98
    ];
99
    /** @var array */
100
    private $multiOptionTags = [
101
        self::TAG_SELECT,
102
        self::TAG_INPUT_RADIO,
103
        self::TAG_INPUT_CHECKBOX,
104
        self::TAG_DATALIST
105
    ];
106
    /** @var array */
107
    private $tabIndexableTags = [
108
        self::TAG_INPUT_CHECKBOX,
109
        self::TAG_INPUT_COLOR,
110
        self::TAG_INPUT_DATA,
111
        self::TAG_INPUT_DATETIME,
112
        self::TAG_INPUT_DATETIME_LOCAL,
113
        self::TAG_INPUT_EMAIL,
114
        self::TAG_INPUT_FILE,
115
        self::TAG_INPUT_IMAGE,
116
        self::TAG_INPUT_MONTH,
117
        self::TAG_INPUT_NUMBER,
118
        self::TAG_INPUT_PASSWORD,
119
        self::TAG_INPUT_RADIO,
120
        self::TAG_INPUT_RANGE,
121
        self::TAG_INPUT_SEARCH,
122
        self::TAG_INPUT_TEL,
123
        self::TAG_INPUT_TEXT,
124
        self::TAG_INPUT_TIME,
125
        self::TAG_INPUT_URL,
126
        self::TAG_INPUT_WEEK,
127
        self::TAG_TEXTAREA,
128
        self::TAG_BUTTON_SUBMIT,
129
        self::TAG_BUTTON_RESET,
130
        self::TAG_BUTTON,
131
        self::TAG_DATALIST,
132
        self::TAG_SELECT,
133
        self::TAG_KEYGEN,
134
    ];
135
136
    /**
137
     * FormElement constructor.
138
     *
139
     * @param string $tagName
140
     * @param string $name
141
     * @param string $label
142
     */
143
    public function __construct($tagName, $name, $label = '')
144
    {
145
        $this->tagName = $tagName;
146
        $this->name = preg_replace('/[^a-z0-9]/', '_', strtolower($name));
147
        $this->label = $label;
148
149
        if (in_array($tagName, $this->tabIndexableTags)) {
150
            $this->attributes['tabindex'] = self::$tabIndex++;
151
        }
152
    }
153
154
    /**
155
     * Set unique identifier for the form.
156
     *
157
     * @param string $uniqueFormNamePostfix
158
     * @return FormElement
159
     */
160
    public function setUniqueFormNamePostfix($uniqueFormNamePostfix)
161
    {
162
        if ($this->tagName != self::TAG_FORM) {
163
            throw new RuntimeException('This method can be applied only fot the <form> element.');
164
        }
165
166
        $this->uniqueFormNamePostfix = $uniqueFormNamePostfix;
167
168
        return $this;
169
    }
170
171
    /**
172
     * Returns the element tag name.
173
     *
174
     * @return string
175
     */
176
    public function getTagName()
177
    {
178
        return $this->tagName;
179
    }
180
181
    /**
182
     * Sets parent element name
183
     *
184
     * @param FormElement $formElement
185
     * @throws RuntimeException
186
     * @return FormElement
187
     */
188
    public function setParentNode(FormElement $formElement)
189
    {
190
        $parentTagName = $formElement->getTagName();
191
192
        if (isset($this->mandatoryTagParents[$this->tagName])
193
            && !in_array($parentTagName, $this->mandatoryTagParents[$this->tagName])
194
        ) {
195
            throw new RuntimeException(
196
                sprintf(
197
                    'Cannot set `%s` as child element of `%s`.',
198
                    $this->tagName,
199
                    $parentTagName
200
                )
201
            );
202
        }
203
204
        $this->parentNode = $formElement;
205
206
        return $this;
207
    }
208
209
    /**
210
     * Returns the element name.
211
     *
212
     * @return string
213
     */
214
    public function getName()
215
    {
216
        $name = $this->name;
217
218
        if (isset($this->parentNode)) {
219
            $name = $this->parentNode->getName() . '[' . $this->name . ']';
220
        } elseif (!empty($this->uniqueFormNamePostfix)) {
221
            $name .= '_' . $this->uniqueFormNamePostfix;
222
        }
223
224
        if (count($this->options) > 1
225
            && $this->tagName  == self::TAG_SELECT
226
            && !empty($this->attributes['multiple'])
227
        ) {
228
            $name .= '[]';
229
        }
230
231
        return $name;
232
    }
233
234
    /**
235
     * Gets element Id.
236
     *
237
     * @return string
238
     */
239
    public function getId()
240
    {
241
        return 'id_' . trim(preg_replace('/[^a-z0-9]/', '_', $this->getName()), '_');
242
    }
243
244
    /**
245
     * Returns the element label.
246
     *
247
     * @return string
248
     */
249
    public function getLabel()
250
    {
251
        return $this->label;
252
    }
253
254
    /**
255
     * Sets element value.
256
     *
257
     * @param mixed $value
258
     * @return FormElement
259
     */
260
    public function setValue($value)
261
    {
262
        $this->value = $value;
263
264
        return $this;
265
    }
266
267
    /**
268
     * Returns element value.
269
     *
270
     * @return mixed
271
     */
272
    public function getValue()
273
    {
274
        return $this->value;
275
    }
276
277
    /**
278
     * Set label-value options for the element.
279
     *
280
     * @param array $options
281
     * @throws RuntimeException
282
     * @return FormElement
283
     */
284
    public function setOptions(array $options)
285
    {
286
        foreach ($options as $option) {
287
            $checked = !empty($option['checked']);
288
            $group = !empty($option['group']) ? $option['group'] : 'Default';
289
            $this->setOption($option['label'], $option['value'], $checked, $group);
290
        }
291
292
        return $this;
293
    }
294
295
    /**
296
     * Sets label-value option for the element.
297
     *
298
     * @param string  $label
299
     * @param string  $value
300
     * @param boolean $checked
301
     * @param string  $group
302
     * @return FormElement
303
     */
304
    public function setOption($label, $value, $checked = false, $group = 'Default')
305
    {
306
        if (!in_array($this->tagName, $this->multiOptionTags)) {
307
            throw new RuntimeException(sprintf('Cannot set value options for `%s` element.', $this->tagName));
308
        }
309
310
        $option = &$this->options;
311
312
        // For <select> tag, the option groupping is allowed.
313
        if ($this->tagName == self::TAG_SELECT) {
314
            if (!isset($this->options[$group])) {
315
                $this->options[$group] = [];
316
            }
317
318
            $option = &$this->options[$group];
319
320
            $this->optionGroups[$group] = $group;
321
        }
322
323
        $option[$label] = [
324
            'label' => $label,
325
            'value' => $value,
326
            'checked' => $checked
327
        ];
328
329
        return $this;
330
    }
331
332
    /**
333
     * Checks if the element has value options.
334
     *
335
     * @return bool
336
     */
337
    public function hasOptions()
338
    {
339
        return !empty($this->options);
340
    }
341
342
    /**
343
     * Checks if the Select box has groupped options.
344
     *
345
     * @return bool
346
     */
347
    public function isGrouppedSelect()
348
    {
349
        return count($this->optionGroups) > 1;
350
    }
351
352
    /**
353
     * Gets element value options.
354
     *
355
     * @return array
356
     */
357
    public function getOptions()
358
    {
359
        return $this->options;
360
    }
361
362
    /**
363
     * Set child node for the element.
364
     *
365
     * @param FormElement $childNode
366
     * @return FormElement
367
     */
368
    public function addChildNode(FormElement $childNode)
369
    {
370
        $childNode->setParentNode($this);
371
372
        $this->childNodes[] = $childNode;
373
374
        return $this;
375
    }
376
377
    /**
378
     * Gets the child nodes of the element.
379
     *
380
     * @return array<FormElement>
381
     */
382
    public function getChildNodes()
383
    {
384
        return $this->childNodes;
385
    }
386
387
    /**
388
     * Sets element attribute.
389
     *
390
     * @param string $key
391
     * @param string $value
392
     * @throws InvalidArgumentException
393
     * @return FormElement
394
     */
395
    public function setAttribute($key, $value)
396
    {
397
        if ($key == 'name') {
398
            throw new InvalidArgumentException('Cannot change element name after it has been initialized.');
399
        }
400
401
        if ($key == 'id') {
402
            throw new InvalidArgumentException('Element ID is generated from name. Call $element->getId();');
403
        }
404
405
        if (!is_scalar($value)) {
406
            throw new InvalidArgumentException('Element attribute can hold scalar data only.');
407
        }
408
409
        $this->attributes[$key] = $value;
410
411
        return $this;
412
    }
413
414
    /**
415
     * Sets multiple attributes.
416
     *
417
     * @param array $attributes
418
     * @return FormElement
419
     */
420
    public function setAttributes(array $attributes)
421
    {
422
        foreach ($attributes as $key => $value) {
423
            $this->setAttribute($key, $value);
424
        }
425
426
        return $this;
427
    }
428
429
    /**
430
     * Gets element attribute.
431
     *
432
     * @param string $name
433
     * @return mixed
434
     */
435
    public function getAttribute($name)
436
    {
437
        if (!isset($this->attributes[$name])) {
438
            throw new RuntimeException(sprintf('Invalid attribute: `%s`', $name));
439
        }
440
441
        return $this->attributes[$name];
442
    }
443
444
    /**
445
     * Gets all the attributes.
446
     *
447
     * @return array
448
     */
449
    public function getAttributes()
450
    {
451
        return $this->attributes;
452
    }
453
454
    /**
455
     * Adds validator to the form.
456
     *
457
     * @param FormValidatorInterface $validator
458
     * @return FormElement
459
     */
460
    public function addValidator(FormValidatorInterface $validator)
461
    {
462
        $this->validators[] = $validator;
463
464
        return $this;
465
    }
466
467
    /**
468
     * Validates element value.
469
     *
470
     * @return bool
471
     */
472
    public function isValid()
473
    {
474
        foreach ($this->validators as $validator) {
475
            if (!$validator->validate($this->value)) {
476
                return false;
477
            }
478
        }
479
480
        return true;
481
    }
482
483
    /**
484
     * Return the current element.
485
     *
486
     * @return FormElement
487
     */
488
    final public function current()
0 ignored issues
show
Coding Style introduced by
Unnecessary FINAL modifier in FINAL class
Loading history...
489
    {
490
        return current($this->childNodes);
491
    }
492
493
    /**
494
     * Moves the pointer forward to next element.
495
     *
496
     * @return void
497
     */
498
    final public function next()
0 ignored issues
show
Coding Style introduced by
Unnecessary FINAL modifier in FINAL class
Loading history...
499
    {
500
        next($this->childNodes);
501
    }
502
503
    /**
504
     * Returns the key of the current element.
505
     *
506
     * @return mixed
507
     */
508
    final public function key()
0 ignored issues
show
Coding Style introduced by
Unnecessary FINAL modifier in FINAL class
Loading history...
509
    {
510
        return key($this->childNodes);
511
    }
512
513
    /**
514
     * Checks if current position is valid.
515
     *
516
     * @return boolean
517
     */
518
    final public function valid()
0 ignored issues
show
Coding Style introduced by
Unnecessary FINAL modifier in FINAL class
Loading history...
519
    {
520
        $key = key($this->childNodes);
521
522
        return ($key !== null && $key !== false);
523
    }
524
525
    /**
526
     * Rewinds the Iterator to the first element.
527
     *
528
     * @return void
529
     */
530
    final public function rewind()
0 ignored issues
show
Coding Style introduced by
Unnecessary FINAL modifier in FINAL class
Loading history...
531
    {
532
        reset($this->childNodes);
533
    }
534
}
535