• 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

91.92
/src/Forms/Rendering/DefaultFormRenderer.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\Rendering;
9

10
use Nette;
11
use Nette\HtmlStringable;
12
use Nette\Utils\Html;
13
use function array_merge, count, explode, implode, parse_url, preg_split, str_replace, strtolower, urldecode;
14
use const PHP_URL_QUERY;
15

16

17
/**
18
 * Converts a form into HTML output using a table-based layout configurable via the $wrappers array.
19
 */
20
class DefaultFormRenderer implements Nette\Forms\FormRenderer
21
{
22
        /**
23
         *  /--- form.container
24
         *
25
         *    /--- error.container
26
         *      .... error.item [.class]
27
         *    \---
28
         *
29
         *    /--- hidden.container
30
         *      .... HIDDEN CONTROLS
31
         *    \---
32
         *
33
         *    /--- group.container
34
         *      .... group.label
35
         *      .... group.description
36
         *
37
         *      /--- controls.container
38
         *
39
         *        /--- pair.container [.required .optional .odd]
40
         *
41
         *          /--- label.container
42
         *            .... LABEL
43
         *            .... label.suffix
44
         *            .... label.requiredsuffix
45
         *          \---
46
         *
47
         *          /--- control.container [.odd .multi]
48
         *            .... CONTROL [.required .error .text .password .file .submit .button]
49
         *            .... control.requiredsuffix
50
         *            .... control.description
51
         *            .... control.errorcontainer + control.erroritem
52
         *          \---
53
         *        \---
54
         *      \---
55
         *    \---
56
         *  \--
57
         * @var array<string, array<string, Html|string|null>>
58
         */
59
        public array $wrappers = [
60
                'form' => [
61
                        'container' => null,
62
                ],
63

64
                'error' => [
65
                        'container' => 'ul class=error',
66
                        'item' => 'li',
67
                ],
68

69
                'group' => [
70
                        'container' => 'fieldset',
71
                        'label' => 'legend',
72
                        'description' => 'p',
73
                ],
74

75
                'controls' => [
76
                        'container' => 'table',
77
                ],
78

79
                'pair' => [
80
                        'container' => 'tr',
81
                        '.required' => 'required',
82
                        '.optional' => null,
83
                        '.odd' => null,
84
                        '.error' => null,
85
                ],
86

87
                'control' => [
88
                        'container' => 'td',
89
                        '.odd' => null,
90
                        '.multi' => null,
91

92
                        'description' => 'small',
93
                        'requiredsuffix' => '',
94
                        'errorcontainer' => 'span class=error',
95
                        'erroritem' => '',
96

97
                        '.required' => 'required',
98
                        '.error' => null,
99
                        '.text' => 'text',
100
                        '.password' => 'text',
101
                        '.file' => 'text',
102
                        '.email' => 'text',
103
                        '.number' => 'text',
104
                        '.submit' => 'button',
105
                        '.image' => 'imagebutton',
106
                        '.button' => 'button',
107
                ],
108

109
                'label' => [
110
                        'container' => 'th',
111
                        'suffix' => null,
112
                        'requiredsuffix' => '',
113
                ],
114

115
                'hidden' => [
116
                        'container' => null,
117
                ],
118
        ];
119

120
        protected Nette\Forms\Form $form;
121
        protected int $counter = 0;
122

123

124
        /**
125
         * Provides complete form rendering.
126
         * @param  ?string  $mode  'begin', 'errors', 'ownerrors', 'body', 'end' or empty to render all
127
         */
128
        public function render(Nette\Forms\Form $form, ?string $mode = null): string
1✔
129
        {
130
                $this->form = $form;
1✔
131

132
                $s = '';
1✔
133
                if (!$mode || $mode === 'begin') {
1✔
134
                        $s .= $this->renderBegin();
1✔
135
                }
136

137
                if (!$mode || strtolower($mode) === 'ownerrors') {
1✔
138
                        $s .= $this->renderErrors();
1✔
139

140
                } elseif ($mode === 'errors') {
1✔
141
                        $s .= $this->renderErrors(own: false);
×
142
                }
143

144
                if (!$mode || $mode === 'body') {
1✔
145
                        $s .= $this->renderBody();
1✔
146
                }
147

148
                if (!$mode || $mode === 'end') {
1✔
149
                        $s .= $this->renderEnd();
1✔
150
                }
151

152
                return $s;
1✔
153
        }
154

155

156
        /**
157
         * Renders form begin.
158
         */
159
        public function renderBegin(): string
160
        {
161
                $this->counter = 0;
1✔
162

163
                foreach ($this->form->getControls() as $control) {
1✔
164
                        $control->setOption('rendered', false);
1✔
165
                }
166

167
                if ($this->form->isMethod('get')) {
1✔
168
                        $el = clone $this->form->getElementPrototype();
1✔
169
                        $el->action = (string) $el->action;
1✔
170
                        $query = parse_url($el->action, PHP_URL_QUERY) ?: '';
1✔
171
                        $el->action = str_replace("?$query", '', $el->action);
1✔
172
                        $s = '';
1✔
173
                        foreach (preg_split('#[;&]#', $query, -1, PREG_SPLIT_NO_EMPTY) as $param) {
1✔
174
                                $parts = explode('=', $param, 2);
1✔
175
                                $name = urldecode($parts[0]);
1✔
176
                                $prefix = explode('[', $name, 2)[0];
1✔
177
                                if (!isset($this->form[$prefix])) {
1✔
178
                                        $s .= Html::el('input', ['type' => 'hidden', 'name' => $name, 'value' => urldecode($parts[1])]);
1✔
179
                                }
180
                        }
181

182
                        return $el->startTag() . ($s ? "\n\t" . $this->getWrapper('hidden container')->setHtml($s) : '');
1✔
183

184
                } else {
185
                        return $this->form->getElementPrototype()->startTag();
1✔
186
                }
187
        }
188

189

190
        /**
191
         * Renders form end.
192
         */
193
        public function renderEnd(): string
194
        {
195
                $s = '';
1✔
196
                foreach ($this->form->getControls() as $control) {
1✔
197
                        if ($control->getOption('type') === 'hidden' && !$control->getOption('rendered')) {
1✔
198
                                $s .= $control->getControl();
1✔
199
                        }
200
                }
201

202
                if ($s) {
1✔
203
                        $s = $this->getWrapper('hidden container')->setHtml($s) . "\n";
1✔
204
                }
205

206
                return $s . $this->form->getElementPrototype()->endTag() . "\n";
1✔
207
        }
208

209

210
        /**
211
         * Renders validation errors (per form or per control).
212
         */
213
        public function renderErrors(?Nette\Forms\Control $control = null, bool $own = true): string
1✔
214
        {
215
                $errors = $control
1✔
216
                        ? $control->getErrors()
×
217
                        : ($own ? $this->form->getOwnErrors() : $this->form->getErrors());
1✔
218
                return $this->doRenderErrors($errors, (bool) $control);
1✔
219
        }
220

221

222
        /** @param  list<string|\Stringable>  $errors */
223
        private function doRenderErrors(array $errors, bool $control): string
1✔
224
        {
225
                if (!$errors) {
1✔
226
                        return '';
1✔
227
                }
228

229
                $container = $this->getWrapper($control ? 'control errorcontainer' : 'error container');
1✔
230
                $itemPrototype = $this->getWrapper($control ? 'control erroritem' : 'error item');
1✔
231

232
                foreach ($errors as $error) {
1✔
233
                        $item = clone $itemPrototype;
1✔
234
                        if ($error instanceof HtmlStringable) {
1✔
235
                                $item->addHtml($error);
×
236
                        } else {
237
                                $item->setText($error);
1✔
238
                        }
239

240
                        $container->addHtml($item);
1✔
241
                }
242

243
                return $control
1✔
244
                        ? "\n\t" . $container->render()
1✔
245
                        : "\n" . $container->render(0);
1✔
246
        }
247

248

249
        /**
250
         * Renders form body.
251
         */
252
        public function renderBody(): string
253
        {
254
                $s = $remains = '';
1✔
255

256
                $defaultContainer = $this->getWrapper('group container');
1✔
257
                $translator = $this->form->getTranslator();
1✔
258

259
                foreach ($this->form->getGroups() as $group) {
1✔
260
                        if (!$group->getControls() || !$group->getOption('visual')) {
1✔
261
                                continue;
×
262
                        }
263

264
                        $container = $group->getOption('container') ?? $defaultContainer;
1✔
265
                        $container = $container instanceof Html
1✔
266
                                ? clone $container
1✔
267
                                : Html::el($container);
×
268

269
                        $id = $group->getOption('id');
1✔
270
                        if ($id) {
1✔
271
                                $container->id = $id;
1✔
272
                        }
273

274
                        $s .= "\n" . $container->startTag();
1✔
275

276
                        $text = $group->getOption('label');
1✔
277
                        if ($text instanceof HtmlStringable) {
1✔
278
                                $s .= $this->getWrapper('group label')->addHtml($text);
×
279

280
                        } elseif ($text != null) { // intentionally ==
1✔
281
                                if ($translator !== null) {
1✔
282
                                        $text = $translator->translate($text);
×
283
                                }
284

285
                                $s .= "\n" . $this->getWrapper('group label')->setText($text) . "\n";
1✔
286
                        }
287

288
                        $text = $group->getOption('description');
1✔
289
                        if ($text instanceof HtmlStringable) {
1✔
290
                                $s .= $text;
×
291

292
                        } elseif ($text != null) { // intentionally ==
1✔
293
                                if ($translator !== null) {
1✔
294
                                        $text = $translator->translate($text);
×
295
                                }
296

297
                                $s .= $this->getWrapper('group description')->setText($text) . "\n";
1✔
298
                        }
299

300
                        $s .= $this->renderControls($group);
1✔
301

302
                        $remains = $container->endTag() . "\n" . $remains;
1✔
303
                        if (!$group->getOption('embedNext')) {
1✔
304
                                $s .= $remains;
1✔
305
                                $remains = '';
1✔
306
                        }
307
                }
308

309
                $s .= $remains . $this->renderControls($this->form);
1✔
310

311
                $container = $this->getWrapper('form container');
1✔
312
                $container->setHtml($s);
1✔
313
                return $container->render(0);
1✔
314
        }
315

316

317
        /**
318
         * Renders group of controls.
319
         */
320
        public function renderControls(Nette\Forms\Container|Nette\Forms\ControlGroup $parent): string
1✔
321
        {
322
                $container = $this->getWrapper('controls container');
1✔
323

324
                $buttons = null;
1✔
325
                foreach ($parent->getControls() as $control) {
1✔
326
                        if (
327
                                $control->getOption('rendered')
1✔
328
                                || $control->getOption('type') === 'hidden'
1✔
329
                                || $control->getForm(false) !== $this->form
1✔
330
                        ) {
1✔
331
                                // skip
332

333
                        } elseif ($control->getOption('type') === 'button') {
1✔
334
                                $buttons[] = $control;
1✔
335

336
                        } else {
337
                                if ($buttons) {
1✔
338
                                        $container->addHtml($this->renderPairMulti($buttons));
×
339
                                        $buttons = null;
×
340
                                }
341

342
                                $container->addHtml($this->renderPair($control));
1✔
343
                        }
344
                }
345

346
                if ($buttons) {
1✔
347
                        $container->addHtml($this->renderPairMulti($buttons));
1✔
348
                }
349

350
                $s = '';
1✔
351
                if (count($container)) {
1✔
352
                        $s .= "\n" . $container . "\n";
1✔
353
                }
354

355
                return $s;
1✔
356
        }
357

358

359
        /**
360
         * Renders single visual row.
361
         */
362
        public function renderPair(Nette\Forms\Control $control): string
1✔
363
        {
364
                $pair = $this->getWrapper('pair container');
1✔
365
                $pair->addHtml($this->renderLabel($control));
1✔
366
                $pair->addHtml($this->renderControl($control));
1✔
367
                $pair->class($this->getValue($control->isRequired() ? 'pair .required' : 'pair .optional'), true);
1✔
368
                $pair->class($control->hasErrors() ? $this->getValue('pair .error') : null, true);
1✔
369
                $pair->class($control->getOption('class'), true);
1✔
370
                if (++$this->counter % 2) {
1✔
371
                        $pair->class($this->getValue('pair .odd'), true);
1✔
372
                }
373

374
                $pair->id = $control->getOption('id');
1✔
375
                return $pair->render(0);
1✔
376
        }
377

378

379
        /**
380
         * Renders single visual row of multiple controls.
381
         * @param  Nette\Forms\Control[]  $controls
382
         */
383
        public function renderPairMulti(array $controls): string
1✔
384
        {
385
                $s = [];
1✔
386
                foreach ($controls as $control) {
1✔
387
                        $description = $control->getOption('description');
1✔
388
                        if ($description instanceof HtmlStringable) {
1✔
389
                                $description = ' ' . $description;
×
390

391
                        } elseif ($description != null) { // intentionally ==
1✔
392
                                if ($control instanceof Nette\Forms\Controls\BaseControl) {
×
UNCOV
393
                                        $description = $control->translate($description);
×
394
                                }
395

UNCOV
396
                                $description = ' ' . $this->getWrapper('control description')->setText($description);
×
397

398
                        } else {
399
                                $description = '';
1✔
400
                        }
401

402
                        $control->setOption('rendered', true);
1✔
403
                        $el = $this->renderControlElement($control);
1✔
404
                        if ($el instanceof Html) {
1✔
405
                                if ($el->getName() === 'input') {
1✔
406
                                        $el->class($this->getValue("control .$el->type"), true);
1✔
407
                                }
408

409
                                $el->class($this->getValue('control .error'), $control->hasErrors());
1✔
410
                        }
411

412
                        $s[] = $el . $description;
1✔
413
                }
414

415
                $pair = $this->getWrapper('pair container');
1✔
416
                $pair->addHtml($this->renderLabel($control));
1✔
417
                $pair->addHtml($this->getWrapper('control container')->setHtml(implode(' ', $s)));
1✔
418
                return $pair->render(0);
1✔
419
        }
420

421

422
        /**
423
         * Renders 'label' part of visual row of controls.
424
         */
425
        public function renderLabel(Nette\Forms\Control $control): Html
1✔
426
        {
427
                $suffix = $this->getValue('label suffix') . ($control->isRequired() ? $this->getValue('label requiredsuffix') : '');
1✔
428
                $label = $this->renderLabelElement($control);
1✔
429
                if ($label instanceof Html) {
1✔
430
                        $label->addHtml($suffix);
1✔
431
                        if ($control->isRequired()) {
1✔
432
                                $label->class($this->getValue('control .required'), true);
1✔
433
                        }
434
                } elseif ($label != null) { // @intentionally ==
1✔
435
                        $label .= $suffix;
1✔
436
                }
437

438
                return $this->getWrapper('label container')->setHtml((string) $label);
1✔
439
        }
440

441

442
        /**
443
         * Renders 'control' part of visual row of controls.
444
         */
445
        public function renderControl(Nette\Forms\Control $control): Html
1✔
446
        {
447
                $body = $this->getWrapper('control container');
1✔
448
                if ($this->counter % 2) {
1✔
449
                        $body->class($this->getValue('control .odd'), true);
1✔
450
                }
451

452
                if (!$this->getWrapper('pair container')->getName()) {
1✔
453
                        $body->class($control->getOption('class'), true);
1✔
454
                        $body->id = $control->getOption('id');
1✔
455
                }
456

457
                $description = $control->getOption('description');
1✔
458
                if ($description instanceof HtmlStringable) {
1✔
UNCOV
459
                        $description = ' ' . $description;
×
460

461
                } elseif ($description != null) { // intentionally ==
1✔
462
                        if ($control instanceof Nette\Forms\Controls\BaseControl) {
1✔
463
                                $description = $control->translate($description);
1✔
464
                        }
465

466
                        $description = ' ' . $this->getWrapper('control description')->setText($description);
1✔
467

468
                } else {
469
                        $description = '';
1✔
470
                }
471

472
                if ($control->isRequired()) {
1✔
473
                        $description = $this->getValue('control requiredsuffix') . $description;
1✔
474
                }
475

476
                $els = $errors = [];
1✔
477
                renderControl:
478
                $control->setOption('rendered', true);
1✔
479
                $el = $this->renderControlElement($control);
1✔
480
                if ($el instanceof Html) {
1✔
481
                        if ($el->getName() === 'input') {
1✔
482
                                $el->class($this->getValue("control .$el->type"), true);
1✔
483
                        }
484

485
                        $el->class($this->getValue('control .error'), $control->hasErrors());
1✔
486
                }
487

488
                $els[] = $el;
1✔
489
                $errors = array_merge($errors, $control->getErrors());
1✔
490

491
                if ($nextTo = $control->getOption('nextTo')) {
1✔
492
                        $control = $control->getForm()->getComponent($nextTo);
1✔
493
                        $body->class($this->getValue('control .multi'), true);
1✔
494
                        goto renderControl;
1✔
495
                }
496

497
                return $body->setHtml(implode('', $els) . $description . $this->doRenderErrors($errors, true));
1✔
498
        }
499

500

501
        protected function renderLabelElement(Nette\Forms\Control $control): Html|string|null
1✔
502
        {
503
                return $control->getLabel();
1✔
504
        }
505

506

507
        protected function renderControlElement(Nette\Forms\Control $control): Html|string
1✔
508
        {
509
                return $control->getControl();
1✔
510
        }
511

512

513
        /**
514
         * Returns a clone of the wrapper element specified by 'section key' (e.g. 'control errorcontainer').
515
         */
516
        public function getWrapper(string $name): Html
1✔
517
        {
518
                $data = $this->getValue($name);
1✔
519
                return $data instanceof Html ? clone $data : Html::el($data);
1✔
520
        }
521

522

523
        protected function getValue(string $name): mixed
1✔
524
        {
525
                $name = explode(' ', $name);
1✔
526
                return $this->wrappers[$name[0]][$name[1]] ?? null;
1✔
527
        }
528
}
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