• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

nette / forms / 21851792823

10 Feb 2026 04:31AM UTC coverage: 93.412% (-0.07%) from 93.481%
21851792823

push

github

dg
component/model 4 WIP

2070 of 2216 relevant lines covered (93.41%)

0.93 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.69
/src/Forms/Helpers.php
1
<?php
2

3
/**
4
 * This file is part of the Nette Framework (https://nette.org)
5
 * Copyright (c) 2004 David Grudl (https://davidgrudl.com)
6
 */
7

8
declare(strict_types=1);
9

10
namespace Nette\Forms;
11

12
use Nette;
13
use Nette\Utils\Html;
14
use Nette\Utils\Image;
15
use Nette\Utils\Strings;
16
use function array_fill_keys, array_map, array_values, explode, html_entity_decode, htmlspecialchars, in_array, ini_get, is_a, is_array, is_numeric, is_scalar, is_string, str_ends_with, str_replace, strip_tags, strpos, strtolower, strtr, substr, substr_replace;
17
use const ENT_HTML5, ENT_NOQUOTES, ENT_QUOTES;
18

19

20
/**
21
 * Forms helpers.
22
 */
23
final class Helpers
24
{
25
        use Nette\StaticClass;
26

27
        private const UnsafeNames = [
28
                'attributes', 'children', 'elements', 'focus', 'length', 'reset', 'style', 'submit', 'onsubmit', 'form',
29
                'presenter', 'action',
30
        ];
31

32

33
        /**
34
         * Extracts and sanitizes submitted form data for single control.
35
         * @param  mixed[]  $data
36
         * @param  int  $type  type Form::DataText, DataLine, DataFile, DataKeys
37
         * @return string|mixed[]|Nette\Http\FileUpload|null
38
         * @internal
39
         */
40
        public static function extractHttpData(
1✔
41
                array $data,
42
                string $htmlName,
43
                int $type,
44
        ): string|array|Nette\Http\FileUpload|null
45
        {
46
                $name = explode('[', str_replace(['[]', ']', '.'], ['', '', '_'], $htmlName));
1✔
47
                $data = Nette\Utils\Arrays::get($data, $name, null);
1✔
48
                $itype = $type & ~Form::DataKeys;
1✔
49

50
                if (str_ends_with($htmlName, '[]')) {
1✔
51
                        if (!is_array($data)) {
1✔
52
                                return [];
1✔
53
                        }
54

55
                        foreach ($data as $k => $v) {
1✔
56
                                $data[$k] = $v = static::sanitize($itype, $v);
1✔
57
                                if ($v === null) {
1✔
58
                                        unset($data[$k]);
1✔
59
                                }
60
                        }
61

62
                        if ($type & Form::DataKeys) {
1✔
63
                                return $data;
1✔
64
                        }
65

66
                        return array_values($data);
1✔
67
                } else {
68
                        return static::sanitize($itype, $data);
1✔
69
                }
70
        }
71

72

73
        /** @return string|mixed[]|Nette\Http\FileUpload|null */
74
        private static function sanitize(int $type, mixed $value): string|array|Nette\Http\FileUpload|null
1✔
75
        {
76
                if ($type === Form::DataText) {
1✔
77
                        return is_scalar($value)
1✔
78
                                ? Strings::unixNewLines((string) $value)
1✔
79
                                : null;
1✔
80

81
                } elseif ($type === Form::DataLine) {
1✔
82
                        return is_scalar($value)
1✔
83
                                ? Strings::trim(strtr((string) $value, "\r\n", '  '))
1✔
84
                                : null;
1✔
85

86
                } elseif ($type === Form::DataFile) {
1✔
87
                        return $value instanceof Nette\Http\FileUpload ? $value : null;
1✔
88

89
                } else {
90
                        throw new Nette\InvalidArgumentException('Unknown data type');
×
91
                }
92
        }
93

94

95
        /**
96
         * Converts control name to HTML name.
97
         */
98
        public static function generateHtmlName(string $id): string
1✔
99
        {
100
                $name = str_replace(Nette\ComponentModel\IComponent::NameSeparator, '][', $id, $count);
1✔
101
                if ($count) {
1✔
102
                        $name = substr_replace($name, '', strpos($name, ']'), 1) . ']';
1✔
103
                }
104

105
                if (is_numeric($name) || in_array($name, self::UnsafeNames, strict: true)) {
1✔
106
                        $name = '_' . $name;
1✔
107
                }
108

109
                return $name;
1✔
110
        }
111

112

113
        /** @return list<array<string, mixed>> */
114
        public static function exportRules(Rules $rules): array
1✔
115
        {
116
                $payload = [];
1✔
117
                foreach ($rules as $rule) {
1✔
118
                        if (!$rule->canExport()) {
1✔
119
                                if ($rule->branch) {
1✔
120
                                        continue;
1✔
121
                                }
122

123
                                break;
1✔
124
                        }
125

126
                        $op = $rule->validator;
1✔
127
                        if (!is_string($op)) {
1✔
128
                                $op = Nette\Utils\Callback::toString($op);
×
129
                        }
130

131
                        if ($rule->branch) {
1✔
132
                                $item = [
1✔
133
                                        'op' => ($rule->isNegative ? '~' : '') . $op,
1✔
134
                                        'rules' => static::exportRules($rule->branch),
1✔
135
                                        'control' => $rule->control->getHtmlName(),
1✔
136
                                ];
137
                                if ($rule->branch->getToggles()) {
1✔
138
                                        $item['toggle'] = $rule->branch->getToggles();
1✔
139
                                } elseif (!$item['rules']) {
1✔
140
                                        continue;
1✔
141
                                }
142
                        } else {
143
                                $msg = Validator::formatMessage($rule, withValue: false);
1✔
144
                                if ($msg instanceof Nette\HtmlStringable) {
1✔
145
                                        $msg = html_entity_decode(strip_tags((string) $msg), ENT_QUOTES | ENT_HTML5, 'UTF-8');
×
146
                                }
147

148
                                $item = ['op' => ($rule->isNegative ? '~' : '') . $op, 'msg' => $msg];
1✔
149
                        }
150

151
                        if (is_array($rule->arg)) {
1✔
152
                                $item['arg'] = [];
1✔
153
                                foreach ($rule->arg as $key => $value) {
1✔
154
                                        $item['arg'][$key] = self::exportArgument($value, $rule->control);
1✔
155
                                }
156
                        } elseif ($rule->arg !== null) {
1✔
157
                                $item['arg'] = self::exportArgument($rule->arg, $rule->control);
1✔
158
                        }
159

160
                        $payload[] = $item;
1✔
161
                }
162

163
                return $payload;
1✔
164
        }
165

166

167
        private static function exportArgument(mixed $value, Control $control): mixed
1✔
168
        {
169
                if ($value instanceof Control) {
1✔
170
                        return ['control' => $value->getHtmlName()];
1✔
171
                } elseif ($control instanceof Controls\DateTimeControl) {
1✔
172
                        return $control->formatHtmlValue($value);
1✔
173
                } else {
174
                        return $value;
1✔
175
                }
176
        }
177

178

179
        /**
180
         * @param  mixed[]  $items
181
         * @param  ?array<string, mixed>  $inputAttrs
182
         * @param  ?array<string, mixed>  $labelAttrs
183
         */
184
        public static function createInputList(
1✔
185
                array $items,
186
                ?array $inputAttrs = null,
187
                ?array $labelAttrs = null,
188
                Html|string|null $wrapper = null,
189
        ): string
190
        {
191
                [$inputAttrs, $inputTag] = self::prepareAttrs($inputAttrs, 'input');
1✔
192
                [$labelAttrs, $labelTag] = self::prepareAttrs($labelAttrs, 'label');
1✔
193
                $res = '';
1✔
194
                $input = Html::el();
1✔
195
                $label = Html::el();
1✔
196
                [$wrapper, $wrapperEnd] = $wrapper instanceof Html ? [$wrapper->startTag(), $wrapper->endTag()] : [(string) $wrapper, ''];
1✔
197

198
                foreach ($items as $value => $caption) {
1✔
199
                        foreach ($inputAttrs as $k => $v) {
1✔
200
                                $input->attrs[$k] = $v[$value] ?? null;
1✔
201
                        }
202

203
                        foreach ($labelAttrs as $k => $v) {
1✔
204
                                $label->attrs[$k] = $v[$value] ?? null;
1✔
205
                        }
206

207
                        $input->value = $value;
1✔
208
                        $res .= ($res === '' && $wrapperEnd === '' ? '' : $wrapper)
1✔
209
                                . $labelTag . $label->attributes() . '>'
1✔
210
                                . $inputTag . $input->attributes() . '>'
1✔
211
                                . ($caption instanceof Nette\HtmlStringable ? $caption : htmlspecialchars((string) $caption, ENT_NOQUOTES, 'UTF-8'))
1✔
212
                                . '</label>'
1✔
213
                                . $wrapperEnd;
1✔
214
                }
215

216
                return $res;
1✔
217
        }
218

219

220
        /**
221
         * @param  mixed[]  $items
222
         * @param  ?array<string, mixed>  $optionAttrs
223
         */
224
        public static function createSelectBox(array $items, ?array $optionAttrs = null, mixed $selected = null): Html
1✔
225
        {
226
                if ($selected !== null) {
1✔
227
                        $optionAttrs['selected?'] = $selected;
1✔
228
                }
229

230
                [$optionAttrs, $optionTag] = self::prepareAttrs($optionAttrs, 'option');
1✔
231
                $option = Html::el();
1✔
232
                $res = $tmp = '';
1✔
233
                foreach ($items as $group => $subitems) {
1✔
234
                        if (is_array($subitems)) {
1✔
235
                                $res .= Html::el('optgroup')->label($group)->startTag();
1✔
236
                                $tmp = '</optgroup>';
1✔
237
                        } else {
238
                                $subitems = [$group => $subitems];
1✔
239
                        }
240

241
                        foreach ($subitems as $value => $caption) {
1✔
242
                                $option->value = $value;
1✔
243
                                foreach ($optionAttrs as $k => $v) {
1✔
244
                                        $option->attrs[$k] = $v[$value] ?? null;
1✔
245
                                }
246

247
                                if ($caption instanceof Html) {
1✔
248
                                        $caption = clone $caption;
1✔
249
                                        $res .= $caption->setName('option')->addAttributes($option->attrs);
1✔
250
                                } else {
251
                                        $res .= $optionTag . $option->attributes() . '>'
1✔
252
                                                . htmlspecialchars((string) $caption, ENT_NOQUOTES, 'UTF-8')
1✔
253
                                                . '</option>';
1✔
254
                                }
255

256
                                if ($selected === $value) {
1✔
257
                                        unset($optionAttrs['selected'], $option->attrs['selected']);
1✔
258
                                }
259
                        }
260

261
                        $res .= $tmp;
1✔
262
                        $tmp = '';
1✔
263
                }
264

265
                return Html::el('select')->setHtml($res);
1✔
266
        }
267

268

269
        /**
270
         * @param  ?array<string, mixed>  $attrs
271
         * @return array{array<string, mixed>, string}
272
         */
273
        private static function prepareAttrs(?array $attrs, string $name): array
1✔
274
        {
275
                $dynamic = [];
1✔
276
                foreach ((array) $attrs as $k => $v) {
1✔
277
                        if ($k[-1] === '?' || $k[-1] === ':') {
1✔
278
                                $p = substr($k, 0, -1);
1✔
279
                                unset($attrs[$k], $attrs[$p]);
1✔
280
                                if ($k[-1] === '?') {
1✔
281
                                        $dynamic[$p] = array_fill_keys((array) $v, value: true);
1✔
282
                                } elseif (is_array($v) && $v) {
1✔
283
                                        $dynamic[$p] = $v;
1✔
284
                                } else {
285
                                        $attrs[$p] = $v;
1✔
286
                                }
287
                        }
288
                }
289

290
                return [$dynamic, '<' . $name . Html::el(null, $attrs)->attributes()];
1✔
291
        }
292

293

294
        /** @internal */
295
        public static function iniGetSize(string $name): int
1✔
296
        {
297
                $value = ini_get($name);
1✔
298
                $units = ['k' => 10, 'm' => 20, 'g' => 30];
1✔
299
                return isset($units[$ch = strtolower(substr($value, -1))])
1✔
300
                        ? (int) $value << $units[$ch]
1✔
301
                        : (int) $value;
1✔
302
        }
303

304

305
        /**
306
         * @internal
307
         * @return ?class-string
308
         */
309
        public static function getSingleType(\ReflectionParameter|\ReflectionProperty $reflection): ?string
1✔
310
        {
311
                $type = Nette\Utils\Type::fromReflection($reflection);
1✔
312
                if (!$type) {
1✔
313
                        return null;
1✔
314
                } elseif ($res = $type->getSingleName()) {
1✔
315
                        return $res;
1✔
316
                } else {
317
                        throw new Nette\InvalidStateException(
×
318
                                Nette\Utils\Reflection::toString($reflection) . " has unsupported type '$type'.",
×
319
                        );
320
                }
321
        }
322

323

324
        /** @internal */
325
        public static function tryEnumConversion(
1✔
326
                mixed $value,
327
                \ReflectionParameter|\ReflectionProperty|null $reflection,
328
        ): mixed
329
        {
330
                if ($value !== null
1✔
331
                        && $reflection
1✔
332
                        && ($type = Nette\Utils\Type::fromReflection($reflection)?->getSingleName())
1✔
333
                        && is_a($type, \BackedEnum::class, allow_string: true)
1✔
334
                ) {
335
                        return $type::from($value);
1✔
336
                }
337

338
                return $value;
1✔
339
        }
340

341

342
        /**
343
         * @internal
344
         * @return string[]
345
         */
346
        public static function getSupportedImages(): array
347
        {
348
                return array_values(array_map(Image::typeToMimeType(...), Image::getSupportedTypes()));
1✔
349
        }
350
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc