Normalizer::input()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 2
dl 0
loc 7
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace GeminiLabs\Castor\Services;
4
5
class Normalizer
6
{
7
    const BOOLEAN_ATTRIBUTES = [
8
        'autofocus', 'capture', 'checked', 'disabled', 'draggable', 'formnovalidate', 'hidden',
9
        'multiple', 'novalidate', 'readonly', 'required', 'selected', 'spellcheck',
10
        'webkitdirectory',
11
    ];
12
13
    const FORM_ATTRIBUTES = [
14
        'accept', 'accept-charset', 'action', 'autocapitalize', 'autocomplete', 'enctype',
15
        'method', 'name', 'novalidate', 'target',
16
    ];
17
18
    const GLOBAL_ATTRIBUTES = [
19
        'accesskey', 'class', 'contenteditable', 'contextmenu', 'dir', 'draggable', 'dropzone',
20
        'hidden', 'id', 'lang', 'spellcheck', 'style', 'tabindex', 'title',
21
    ];
22
23
    const GLOBAL_WILDCARD_ATTRIBUTES = [
24
        'aria-', 'data-', 'item', 'on',
25
    ];
26
27
    const INPUT_ATTRIBUTES = [
28
        'accept', 'autocapitalize', 'autocomplete', 'autocorrect', 'autofocus', 'capture',
29
        'checked', 'disabled', 'form', 'formaction', 'formenctype', 'formmethod',
30
        'formnovalidate', 'formtarget', 'height', 'incremental', 'inputmode', 'list', 'max',
31
        'maxlength', 'min', 'minlength', 'mozactionhint', 'multiple', 'name', 'pattern',
32
        'placeholder', 'readonly', 'required', 'results', 'selectionDirection', 'size', 'src',
33
        'step', 'type', 'value', 'webkitdirectory', 'width', 'x-moz-errormessage',
34
    ];
35
36
    const INPUT_TYPES = [
37
        'button', 'checkbox', 'color', 'date', 'datetime', 'datetime-local', 'email', 'file',
38
        'hidden', 'image', 'max', 'min', 'month', 'number', 'password', 'radio', 'range',
39
        'reset', 'search', 'step', 'submit', 'tel', 'text', 'time', 'url', 'value', 'week',
40
    ];
41
42
    const SELECT_ATTRIBUTES = [
43
        'autofocus', 'disabled', 'form', 'multiple', 'name', 'required', 'size',
44
    ];
45
46
    const TEXTAREA_ATTRIBUTES = [
47
        'autocapitalize', 'autocomplete', 'autofocus', 'cols', 'disabled', 'form', 'maxlength',
48
        'minlength', 'name', 'placeholder', 'readonly', 'required', 'rows',
49
        'selectionDirection', 'selectionEnd', 'selectionStart', 'wrap',
50
    ];
51
52
    /**
53
     * @var array
54
     */
55
    protected $args;
56
57
    public function __construct()
58
    {
59
        $this->args = [];
60
    }
61
62
    /**
63
     * Normalize form attributes.
64
     *
65
     * @return array|string
66
     */
67
    public function form(array $args = [], $implode = false)
68
    {
69
        $attributes = $this->parseAttributes(self::FORM_ATTRIBUTES, $args);
70
71
        return $this->maybeImplode($attributes, $implode);
72
    }
73
74
    /**
75
     * Normalize input attributes.
76
     *
77
     * @return array|string
78
     */
79
    public function input(array $args = [], $implode = false)
80
    {
81
        $this->filterInputType();
82
83
        $attributes = $this->parseAttributes(self::INPUT_ATTRIBUTES, $args);
84
85
        return $this->maybeImplode($attributes, $implode);
86
    }
87
88
    /**
89
     * Possibly implode attributes into a string.
90
     *
91
     * @param bool|string $implode
92
     *
93
     * @return array|string
94
     */
95
    public function maybeImplode(array $attributes, $implode = true)
96
    {
97
        if (!$implode || 'implode' !== $implode) {
98
            return $attributes;
99
        }
100
        $results = [];
101
        foreach ($attributes as $key => $value) {
102
            // if data attributes, use single quotes in case of json encoded values
103
            $quotes = false !== stripos($key, 'data-') ? "'" : '"';
104
            if (is_array($value)) {
105
                $value = json_encode($value);
106
                $quotes = "'";
107
            }
108
            $results[] = is_string($key)
109
                ? sprintf('%1$s=%3$s%2$s%3$s', $key, $value, $quotes)
110
                : $value;
111
        }
112
        return implode(' ', $results);
113
    }
114
115
    /**
116
     * Normalize select attributes.
117
     *
118
     * @return array|string
119
     */
120
    public function select(array $args = [], $implode = false)
121
    {
122
        $attributes = $this->parseAttributes(self::SELECT_ATTRIBUTES, $args);
123
124
        return $this->maybeImplode($attributes, $implode);
125
    }
126
127
    /**
128
     * Normalize textarea attributes.
129
     *
130
     * @return array|string
131
     */
132
    public function textarea(array $args = [], $implode = false)
133
    {
134
        $attributes = $this->parseAttributes(self::TEXTAREA_ATTRIBUTES, $args);
135
136
        return $this->maybeImplode($attributes, $implode);
137
    }
138
139
    /**
140
     * Filter attributes by the provided attrribute keys and remove any non-boolean keys
141
     * with empty values.
142
     *
143
     * @return array
144
     */
145
    protected function filterAttributes(array $attributeKeys)
146
    {
147
        $filtered = array_intersect_key($this->args, array_flip($attributeKeys));
148
149
        // normalize truthy boolean attributes
150
        foreach ($filtered as $key => $value) {
151
            if (!in_array($key, self::BOOLEAN_ATTRIBUTES)) {
152
                continue;
153
            }
154
155
            if (false !== $value) {
156
                $filtered[$key] = '';
157
                continue;
158
            }
159
160
            unset($filtered[$key]);
161
        }
162
163
        $filteredKeys = array_filter(array_keys($filtered), function ($key) use ($filtered) {
164
            return !(
165
                empty($filtered[$key])
166
                && !is_numeric($filtered[$key])
167
                && !in_array($key, self::BOOLEAN_ATTRIBUTES)
168
            );
169
        });
170
171
        return array_intersect_key($filtered, array_flip($filteredKeys));
172
    }
173
174
    /**
175
     * @return array
176
     */
177
    protected function filterGlobalAttributes()
178
    {
179
        $global = $this->filterAttributes(self::GLOBAL_ATTRIBUTES);
180
181
        $wildcards = [];
182
183
        foreach (self::GLOBAL_WILDCARD_ATTRIBUTES as $wildcard) {
184
            foreach ($this->args as $key => $value) {
185
                $length = strlen($wildcard);
186
                $result = substr($key, 0, $length) === $wildcard;
187
188
                if ($result) {
189
                    // only allow data attributes to have an empty value
190
                    if ('data-' != $wildcard && empty($value)) {
191
                        continue;
192
                    }
193
194
                    if (is_array($value)) {
195
                        if ('data-' != $wildcard) {
196
                            continue;
197
                        }
198
199
                        $value = json_encode($value);
200
                    }
201
202
                    $wildcards[$key] = $value;
203
                }
204
            }
205
        }
206
207
        return array_merge($global, $wildcards);
208
    }
209
210
    /**
211
     * @return void
212
     */
213
    protected function filterInputType()
214
    {
215
        if (!isset($this->args['type']) || !in_array($this->args['type'], self::INPUT_TYPES)) {
216
            $this->args['type'] = 'text';
217
        }
218
    }
219
220
    /**
221
     * @return array
222
     */
223
    protected function parseAttributes(array $attributes, array $args = [])
224
    {
225
        if (!empty($args)) {
226
            $this->args = array_change_key_case($args);
227
        }
228
229
        $global = $this->filterGlobalAttributes();
230
        $local = $this->filterAttributes($attributes);
231
232
        return array_merge($global, $local);
233
    }
234
}
235