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

nette / forms / 26455457147

26 May 2026 02:44PM UTC coverage: 93.345%. Remained the same
26455457147

push

github

dg
fixed PHPStan errors

48 of 51 new or added lines in 12 files covered. (94.12%)

34 existing lines in 10 files now uncovered.

2104 of 2254 relevant lines covered (93.35%)

0.93 hits per line

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

96.1
/src/Forms/Helpers.php
1
<?php declare(strict_types=1);
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
namespace Nette\Forms;
9

10
use Nette;
11
use Nette\Utils\Html;
12
use Nette\Utils\Image;
13
use Nette\Utils\Strings;
14
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;
15

16

17
/**
18
 * Forms helpers.
19
 */
20
final class Helpers
21
{
22
        use Nette\StaticClass;
23

24
        private const UnsafeNames = [
25
                'attributes', 'children', 'elements', 'focus', 'length', 'reset', 'style', 'submit', 'onsubmit', 'form',
26
                'presenter', 'action',
27
        ];
28

29

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

47
                if (str_ends_with($htmlName, '[]')) {
1✔
48
                        if (!is_array($data)) {
1✔
49
                                return [];
1✔
50
                        }
51

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

59
                        if ($type & Form::DataKeys) {
1✔
60
                                return $data;
1✔
61
                        }
62

63
                        return array_values($data);
1✔
64
                } else {
65
                        return static::sanitize($itype, $data);
1✔
66
                }
67
        }
68

69

70
        private static function sanitize(int $type, mixed $value): string|Nette\Http\FileUpload|null
1✔
71
        {
72
                if ($type === Form::DataText) {
1✔
73
                        return is_scalar($value)
1✔
74
                                ? Strings::unixNewLines((string) $value)
1✔
75
                                : null;
1✔
76

77
                } elseif ($type === Form::DataLine) {
1✔
78
                        return is_scalar($value)
1✔
79
                                ? Strings::trim(strtr((string) $value, "\r\n", '  '))
1✔
80
                                : null;
1✔
81

82
                } elseif ($type === Form::DataFile) {
1✔
83
                        return $value instanceof Nette\Http\FileUpload ? $value : null;
1✔
84

85
                } else {
86
                        throw new Nette\InvalidArgumentException('Unknown data type');
×
87
                }
88
        }
89

90

91
        /**
92
         * Converts a component path (e.g. 'form-person-name') to the HTML name attribute format (e.g. 'person[name]').
93
         */
94
        public static function generateHtmlName(string $id): string
1✔
95
        {
96
                $name = str_replace(Nette\ComponentModel\IComponent::NameSeparator, '][', $id, $count);
1✔
97
                if ($count) {
1✔
98
                        $pos = strpos($name, ']');
1✔
99
                        assert($pos !== false);
100
                        $name = substr_replace($name, '', $pos, 1) . ']';
1✔
101
                }
102

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

107
                return $name;
1✔
108
        }
109

110

111
        /**
112
         * Exports validation rules into a JSON-serializable structure for the data-nette-rules attribute.
113
         * @return list<array<string, mixed>>
114
         */
115
        public static function exportRules(Rules $rules): array
1✔
116
        {
117
                $payload = [];
1✔
118
                foreach ($rules as $rule) {
1✔
119
                        if (!$rule->canExport()) {
1✔
120
                                if ($rule->branch) {
1✔
121
                                        continue;
1✔
122
                                }
123

124
                                break;
1✔
125
                        }
126

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

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

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

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

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

164
                return $payload;
1✔
165
        }
166

167

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

179

180
        /**
181
         * Generates an HTML list of labeled inputs (radio buttons or checkboxes).
182
         * @param  mixed[]  $items  value => label pairs
183
         * @param  ?array<string, mixed>  $inputAttrs
184
         * @param  ?array<string, mixed>  $labelAttrs
185
         */
186
        public static function createInputList(
1✔
187
                array $items,
188
                ?array $inputAttrs = null,
189
                ?array $labelAttrs = null,
190
                Html|string|null $wrapper = null,
191
        ): string
192
        {
193
                [$inputAttrs, $inputTag] = self::prepareAttrs($inputAttrs, 'input');
1✔
194
                [$labelAttrs, $labelTag] = self::prepareAttrs($labelAttrs, 'label');
1✔
195
                $res = '';
1✔
196
                $input = Html::el();
1✔
197
                $label = Html::el();
1✔
198
                [$wrapper, $wrapperEnd] = $wrapper instanceof Html ? [$wrapper->startTag(), $wrapper->endTag()] : [(string) $wrapper, ''];
1✔
199

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

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

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

218
                return $res;
1✔
219
        }
220

221

222
        /**
223
         * Generates a <select> HTML element from the items array.
224
         * @param  mixed[]  $items
225
         * @param  ?array<string, mixed>  $optionAttrs
226
         */
227
        public static function createSelectBox(array $items, ?array $optionAttrs = null, mixed $selected = null): Html
1✔
228
        {
229
                if ($selected !== null) {
1✔
230
                        $optionAttrs['selected?'] = $selected;
1✔
231
                }
232

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

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

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

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

264
                        $res .= $tmp;
1✔
265
                        $tmp = '';
1✔
266
                }
267

268
                return Html::el('select')->setHtml($res);
1✔
269
        }
270

271

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

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

296

297
        /** @internal */
298
        public static function iniGetSize(string $name): int
1✔
299
        {
300
                $value = ini_get($name);
1✔
301
                if ($value === false) {
1✔
NEW
302
                        return 0;
×
303
                }
304

305
                $units = ['k' => 10, 'm' => 20, 'g' => 30];
1✔
306
                return isset($units[$ch = strtolower(substr($value, -1))])
1✔
307
                        ? (int) $value << $units[$ch]
1✔
308
                        : (int) $value;
1✔
309
        }
310

311

312
        /**
313
         * Returns the single type name from reflection, or null if no type is defined.
314
         * @internal
315
         */
316
        public static function getSingleType(\ReflectionParameter|\ReflectionProperty $reflection): ?string
1✔
317
        {
318
                $type = Nette\Utils\Type::fromReflection($reflection);
1✔
319
                if (!$type) {
1✔
320
                        return null;
1✔
321
                } elseif ($res = $type->getSingleName()) {
1✔
322
                        return $res;
1✔
323
                } else {
UNCOV
324
                        throw new Nette\InvalidStateException(
×
UNCOV
325
                                Nette\Utils\Reflection::toString($reflection) . " has unsupported type '$type'.",
×
326
                        );
327
                }
328
        }
329

330

331
        /** @internal */
332
        public static function tryEnumConversion(
1✔
333
                mixed $value,
334
                \ReflectionParameter|\ReflectionProperty|null $reflection,
335
        ): mixed
336
        {
337
                if ($value !== null
1✔
338
                        && $reflection
1✔
339
                        && ($type = Nette\Utils\Type::fromReflection($reflection)?->getSingleName())
1✔
340
                        && is_a($type, \BackedEnum::class, allow_string: true)
1✔
341
                ) {
342
                        return $type::from($value);
1✔
343
                }
344

345
                return $value;
1✔
346
        }
347

348

349
        /**
350
         * @internal
351
         * @return list<string>
352
         */
353
        public static function getSupportedImages(): array
354
        {
355
                return array_values(array_map(Image::typeToMimeType(...), Image::getSupportedTypes()));
1✔
356
        }
357
}
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