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

mimmi20 / laminasviewrenderer-bootstrap-form / 12571184477

01 Jan 2025 01:36PM UTC coverage: 97.361% (+0.05%) from 97.314%
12571184477

push

github

mimmi20
fix tests

2177 of 2236 relevant lines covered (97.36%)

25.54 hits per line

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

98.68
/src/FormRow.php
1
<?php
2

3
/**
4
 * This file is part of the mimmi20/laminasviewrenderer-bootstrap-form package.
5
 *
6
 * Copyright (c) 2021-2025, Thomas Mueller <mimmi20@live.de>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11

12
declare(strict_types = 1);
13

14
namespace Mimmi20\LaminasView\BootstrapForm;
15

16
use Laminas\Form\Element\Button;
17
use Laminas\Form\Element\Captcha;
18
use Laminas\Form\Element\Checkbox;
19
use Laminas\Form\Element\MonthSelect;
20
use Laminas\Form\Element\MultiCheckbox;
21
use Laminas\Form\Element\Submit;
22
use Laminas\Form\ElementInterface;
23
use Laminas\Form\Exception\DomainException;
24
use Laminas\Form\Exception\InvalidArgumentException;
25
use Laminas\Form\Fieldset;
26
use Laminas\Form\FieldsetInterface;
27
use Laminas\Form\FormInterface;
28
use Laminas\Form\LabelAwareInterface;
29
use Laminas\Form\View\Helper\FormElement;
30
use Laminas\Form\View\Helper\FormRow as BaseFormRow;
31
use Laminas\InputFilter\InputFilterInterface;
32
use Laminas\InputFilter\InputFilterProviderInterface;
33
use Laminas\InputFilter\InputInterface;
34
use Override;
35
use stdClass;
36

37
use function array_key_exists;
38
use function array_merge;
39
use function array_unique;
40
use function assert;
41
use function explode;
42
use function get_debug_type;
43
use function implode;
44
use function in_array;
45
use function is_array;
46
use function is_scalar;
47
use function is_string;
48
use function mb_strlen;
49
use function mb_strpos;
50
use function mb_substr;
51
use function sprintf;
52
use function str_contains;
53
use function str_replace;
54
use function trim;
55

56
use const PHP_EOL;
57

58
final class FormRow extends BaseFormRow implements FormRowInterface
59
{
60
    use FormTrait;
61
    use HiddenHelperTrait;
62
    use HtmlHelperTrait;
63

64
    /**
65
     * The class that is added to element that have errors
66
     *
67
     * @var string
68
     * @phpcsSuppress SlevomatCodingStandard.TypeHints.PropertyTypeHint.MissingNativeTypeHint
69
     */
70
    protected $inputErrorClass = 'is-invalid';
71

72
    /**
73
     * Utility form helper that renders a label (if it exists), an element and errors
74
     *
75
     * @throws DomainException
76
     * @throws InvalidArgumentException
77
     *
78
     * @phpcsSuppress SlevomatCodingStandard.TypeHints.NullableTypeForNullDefaultValue.NullabilityTypeMissing
79
     */
80
    #[Override]
57✔
81
    public function render(ElementInterface $element, string | null $labelPosition = null): string
82
    {
83
        $form = $element->getOption('form');
57✔
84
        assert(
57✔
85
            $form instanceof FormInterface || $form === null,
57✔
86
            sprintf(
57✔
87
                '$form should be an Instance of %s or null, but was %s',
57✔
88
                FormInterface::class,
57✔
89
                get_debug_type($form),
57✔
90
            ),
57✔
91
        );
57✔
92

93
        if (!$element->hasAttribute('required')) {
56✔
94
            $elementName = $element->getName();
56✔
95

96
            if ($elementName !== null) {
56✔
97
                if ($form !== null) {
56✔
98
                    $filter = $this->getInputFilter(
43✔
99
                        elementName: $elementName,
43✔
100
                        inputFilter: $form->getInputFilter(),
43✔
101
                        element: $element,
43✔
102
                    );
43✔
103

104
                    if ($filter instanceof InputInterface && $filter->isRequired()) {
43✔
105
                        $element->setAttribute('required', true);
39✔
106
                    }
107
                }
108
            }
109
        }
110

111
        $label = $element->getLabel() ?? '';
56✔
112

113
        if ($labelPosition === null) {
56✔
114
            $labelPosition = $this->getLabelPosition();
56✔
115
        }
116

117
        // hidden elements do not need a <label> -https://github.com/zendframework/zf2/issues/5607
118
        $type = $element->getAttribute('type');
56✔
119

120
        // Translate the label
121
        if ($label !== '' && $type !== 'hidden') {
56✔
122
            $label = $this->translateLabel($label);
42✔
123
        }
124

125
        // Does this element have errors ?
126
        if ($element->getMessages() !== []) {
56✔
127
            $inputErrorClass = $this->getInputErrorClass();
13✔
128
            $classAttributes = [];
13✔
129

130
            if ($element->hasAttribute('class')) {
13✔
131
                $classAttributes = array_merge(
6✔
132
                    $classAttributes,
6✔
133
                    explode(' ', (string) $element->getAttribute('class')),
6✔
134
                );
6✔
135
            }
136

137
            if ($inputErrorClass) {
13✔
138
                $classAttributes[] = $inputErrorClass;
13✔
139
            }
140

141
            $errorClass = $element->getOption('error-class');
13✔
142

143
            if ($errorClass) {
13✔
144
                $classAttributes[] = $errorClass;
1✔
145
            }
146

147
            $element->setAttribute('class', implode(' ', array_unique($classAttributes)));
13✔
148
        } else {
149
            $wasValidated = $element->getOption('was-validated');
50✔
150

151
            if ($wasValidated === null && $form !== null) {
50✔
152
                $wasValidated = $form->getOption('was-validated');
1✔
153
            }
154

155
            if ($wasValidated) {
50✔
156
                $classAttributes = [];
5✔
157

158
                if ($element->hasAttribute('class')) {
5✔
159
                    $classAttributes = array_merge(
5✔
160
                        $classAttributes,
5✔
161
                        explode(' ', (string) $element->getAttribute('class')),
5✔
162
                    );
5✔
163
                }
164

165
                $validClass = $element->getOption('valid-class');
5✔
166

167
                if ($validClass) {
5✔
168
                    $classAttributes[] = $validClass;
4✔
169
                }
170

171
                $element->setAttribute('class', implode(' ', array_unique($classAttributes)));
5✔
172
            }
173
        }
174

175
        $indent = $this->getIndent();
56✔
176

177
        if ($this->view !== null && $this->partial) {
56✔
178
            $vars = [
10✔
179
                'element' => $element,
10✔
180
                'label' => $label,
10✔
181
                'labelAttributes' => $this->labelAttributes,
10✔
182
                'labelPosition' => $labelPosition,
10✔
183
                'renderErrors' => $this->renderErrors,
10✔
184
                'indent' => $indent,
10✔
185
            ];
10✔
186

187
            return $this->view->render($this->partial, $vars);
10✔
188
        }
189

190
        if ($type === 'hidden') {
46✔
191
            $hiddenHelper = $this->getHiddenHelper();
31✔
192
            $hiddenHelper->setIndent($indent);
31✔
193

194
            $errorContent = '';
31✔
195

196
            if ($this->renderErrors) {
31✔
197
                $errorContent = $this->renderFormErrors($element, $indent . $this->getWhitespace(4));
28✔
198
            }
199

200
            $markup = $hiddenHelper->render($element);
31✔
201

202
            return $markup . $errorContent;
31✔
203
        }
204

205
        $label = $this->escapeLabel($element, $label);
43✔
206

207
        assert(is_string($label));
43✔
208

209
        $layout           = $element->getOption('layout');
43✔
210
        $floating         = $element->getOption('floating');
43✔
211
        $showRequiredMark = $element->getOption('show-required-mark');
43✔
212
        $requiredMark     = $element->getOption('field-required-mark');
43✔
213

214
        if ($form !== null) {
43✔
215
            if ($layout === null) {
42✔
216
                $layout = $form->getOption('layout');
1✔
217
            }
218

219
            if (
220
                $floating === null
42✔
221
                && ($layout === Form::LAYOUT_VERTICAL || $layout === Form::LAYOUT_INLINE)
42✔
222
                && $form->getOption('floating-labels')
42✔
223
            ) {
224
                $element->setOption('floating', true);
1✔
225
            }
226

227
            if ($showRequiredMark === null) {
42✔
228
                $showRequiredMark = $form->getOption('form-required-mark') !== null
22✔
229
                    && $form->getOption('field-required-mark') !== null;
22✔
230
            }
231

232
            if ($showRequiredMark && $requiredMark === null) {
42✔
233
                $requiredMark = $form->getOption('field-required-mark');
2✔
234
            }
235
        }
236

237
        if ($showRequiredMark && is_string($requiredMark) && $element->getAttribute('required')) {
43✔
238
            $label .= $requiredMark;
20✔
239
        }
240

241
        if ($layout === Form::LAYOUT_HORIZONTAL) {
43✔
242
            return $this->renderHorizontalRow($element, $label);
20✔
243
        }
244

245
        return $this->renderVerticalRow($element, $label, $labelPosition);
25✔
246
    }
247

248
    /**
249
     * @throws DomainException
250
     * @throws InvalidArgumentException
251
     */
252
    private function renderHorizontalRow(ElementInterface $element, string $label): string
20✔
253
    {
254
        $rowAttributes      = $this->mergeAttributes($element, 'row_attributes', ['row']);
20✔
255
        $colAttributes      = $this->mergeAttributes($element, 'col_attributes', []);
20✔
256
        $labelColAttributes = $this->mergeAttributes(
20✔
257
            $element,
20✔
258
            'label_col_attributes',
20✔
259
            ['col-form-label'],
20✔
260
        );
20✔
261

262
        $indent     = $this->getIndent();
20✔
263
        $type       = $element->getAttribute('type');
20✔
264
        $htmlHelper = $this->getHtmlHelper();
20✔
265

266
        $elementHelper = $this->getElementHelper();
20✔
267
        assert($elementHelper instanceof FormElement);
20✔
268

269
        // Multicheckbox elements have to be handled differently as the HTML standard does not allow nested
270
        // labels. The semantic way is to group them inside a fieldset
271
        if (
272
            $element instanceof MultiCheckbox
20✔
273
            || $element instanceof MonthSelect
20✔
274
            || $element instanceof Captcha
20✔
275
            || in_array($type, ['multi_checkbox', 'radio'], true)
20✔
276
        ) {
277
            $baseIndent = $indent;
17✔
278
            $lf1Indent  = $indent . $this->getWhitespace(4);
17✔
279
            $lf2Indent  = $lf1Indent . $this->getWhitespace(4);
17✔
280
            $lf3Indent  = $lf2Indent . $this->getWhitespace(4);
17✔
281
            $lf4Indent  = $lf3Indent . $this->getWhitespace(4);
17✔
282

283
            $asCard        = $element->getOption('as-card');
17✔
284
            $asFormControl = $element->getOption('as-form-control');
17✔
285

286
            $legend = $lf1Indent . $htmlHelper->render('legend', $labelColAttributes, $label) . PHP_EOL;
17✔
287

288
            $errorContent   = '';
17✔
289
            $helpContent    = '';
17✔
290
            $messageContent = '';
17✔
291

292
            if ($this->renderErrors) {
17✔
293
                $errorContent = $this->renderFormErrors(
17✔
294
                    $element,
17✔
295
                    $asCard || $asFormControl ? $lf4Indent : $lf2Indent,
17✔
296
                );
17✔
297
            }
298

299
            if ($element->getOption('messages')) {
17✔
300
                $messageContent = $this->renderMessages(
2✔
301
                    $element,
2✔
302
                    $asCard || $asFormControl ? $lf4Indent : $lf2Indent,
2✔
303
                );
2✔
304
            }
305

306
            if ($element->getOption('help_content') !== null) {
17✔
307
                $helpContent = $this->renderFormHelp($element, $lf1Indent, $rowAttributes);
2✔
308
            }
309

310
            if ($elementHelper instanceof FormIndentInterface) {
17✔
311
                $elementHelper->setIndent($element->getOption('as-card') ? $lf4Indent : $lf3Indent);
17✔
312
            }
313

314
            $elementString  = $elementHelper->render($element);
17✔
315
            $elementString .= $errorContent;
17✔
316
            $elementString .= $messageContent;
17✔
317

318
            $elementString = $this->wrapInContainer(
17✔
319
                element: $element,
17✔
320
                elementString: $elementString,
17✔
321
                htmlHelper: $htmlHelper,
17✔
322
                indent: $lf2Indent,
17✔
323
            );
17✔
324

325
            $outerDiv = $lf1Indent . $htmlHelper->render(
17✔
326
                'div',
17✔
327
                $colAttributes,
17✔
328
                PHP_EOL . $lf2Indent . $elementString . PHP_EOL . $lf1Indent,
17✔
329
            );
17✔
330

331
            return $baseIndent . $htmlHelper->render(
17✔
332
                'fieldset',
17✔
333
                $rowAttributes,
17✔
334
                PHP_EOL . $legend . $outerDiv . $helpContent . PHP_EOL . $baseIndent,
17✔
335
            );
17✔
336
        }
337

338
        if ($element instanceof Checkbox || $type === 'checkbox') {
20✔
339
            // this is a special case, because label is always rendered inside it
340
            $errorContent   = '';
17✔
341
            $helpContent    = '';
17✔
342
            $messageContent = '';
17✔
343
            $baseIndent     = $indent;
17✔
344
            $lf1Indent      = $indent . $this->getWhitespace(4);
17✔
345
            $lf2Indent      = $lf1Indent . $this->getWhitespace(4);
17✔
346
            $lf3Indent      = $lf2Indent . $this->getWhitespace(4);
17✔
347
            $lf4Indent      = $lf3Indent . $this->getWhitespace(4);
17✔
348

349
            $asCard        = $element->getOption('as-card');
17✔
350
            $asFormControl = $element->getOption('as-form-control');
17✔
351

352
            if ($this->renderErrors) {
17✔
353
                $errorContent = $this->renderFormErrors(
17✔
354
                    $element,
17✔
355
                    $asCard || $asFormControl ? $lf4Indent : $lf2Indent,
17✔
356
                );
17✔
357
            }
358

359
            if ($element->getOption('messages')) {
17✔
360
                $messageContent = $this->renderMessages(
2✔
361
                    $element,
2✔
362
                    $asCard || $asFormControl ? $lf4Indent : $lf2Indent,
2✔
363
                );
2✔
364
            }
365

366
            if ($element->getOption('help_content') !== null) {
17✔
367
                $helpContent = $this->renderFormHelp($element, $lf1Indent, $rowAttributes);
10✔
368
            }
369

370
            if ($elementHelper instanceof FormIndentInterface) {
17✔
371
                $elementHelper->setIndent($element->getOption('as-card') ? $lf4Indent : $lf3Indent);
17✔
372
            }
373

374
            $elementString  = $elementHelper->render($element);
17✔
375
            $elementString .= $errorContent . $messageContent;
17✔
376

377
            $elementString = $this->wrapInContainer(
17✔
378
                element: $element,
17✔
379
                elementString: $elementString,
17✔
380
                htmlHelper: $htmlHelper,
17✔
381
                indent: $lf2Indent,
17✔
382
            );
17✔
383

384
            $outerDiv = $lf1Indent . $htmlHelper->render(
17✔
385
                'div',
17✔
386
                $colAttributes,
17✔
387
                PHP_EOL . $lf2Indent . $elementString . PHP_EOL . $lf1Indent,
17✔
388
            );
17✔
389

390
            return $baseIndent . $htmlHelper->render(
17✔
391
                'div',
17✔
392
                $rowAttributes,
17✔
393
                PHP_EOL . $outerDiv . $helpContent . PHP_EOL . $baseIndent,
17✔
394
            );
17✔
395
        }
396

397
        if (
398
            $element instanceof Button
20✔
399
            || $element instanceof Submit
17✔
400
            || $element instanceof Fieldset
17✔
401
            || in_array($type, ['button', 'submit', 'reset'], true)
20✔
402
        ) {
403
            // this is a special case, because label is always rendered inside it
404
            $baseIndent = $indent;
19✔
405
            $lf1Indent  = $indent . $this->getWhitespace(4);
19✔
406
            $lf2Indent  = $lf1Indent . $this->getWhitespace(4);
19✔
407

408
            if ($elementHelper instanceof FormIndentInterface) {
19✔
409
                $elementHelper->setIndent($lf2Indent);
19✔
410
            }
411

412
            $elementString = $elementHelper->render($element);
19✔
413

414
            $outerDiv = $lf1Indent . $htmlHelper->render(
19✔
415
                'div',
19✔
416
                $colAttributes,
19✔
417
                PHP_EOL . $elementString . PHP_EOL . $lf1Indent,
19✔
418
            );
19✔
419

420
            return $baseIndent . $htmlHelper->render(
19✔
421
                'div',
19✔
422
                $rowAttributes,
19✔
423
                PHP_EOL . $outerDiv . PHP_EOL . $baseIndent,
19✔
424
            );
19✔
425
        }
426

427
        if ($element->hasAttribute('id')) {
17✔
428
            $id = $element->getAttribute('id');
17✔
429

430
            assert(is_string($id));
17✔
431

432
            $labelColAttributes['for'] = $id;
17✔
433
        }
434

435
        $errorContent   = '';
17✔
436
        $helpContent    = '';
17✔
437
        $messageContent = '';
17✔
438
        $baseIndent     = $indent;
17✔
439
        $lf1Indent      = $indent . $this->getWhitespace(4);
17✔
440
        $lf2Indent      = $lf1Indent . $this->getWhitespace(4);
17✔
441
        $lf3Indent      = $lf2Indent . $this->getWhitespace(4);
17✔
442

443
        $labelHelper = $this->getLabelHelper();
17✔
444

445
        $legend = $lf1Indent . $labelHelper->openTag(
17✔
446
            $labelColAttributes,
17✔
447
        ) . $label . $labelHelper->closeTag();
17✔
448

449
        if ($this->renderErrors) {
17✔
450
            $errorContent = $this->renderFormErrors($element, $lf2Indent);
17✔
451
        }
452

453
        if ($element->getOption('messages')) {
17✔
454
            $messageContent = $this->renderMessages($element, $lf2Indent);
3✔
455
        }
456

457
        if ($element->getOption('help_content') !== null) {
17✔
458
            $helpContent = $this->renderFormHelp($element, $lf1Indent, $rowAttributes);
10✔
459
        }
460

461
        if ($elementHelper instanceof FormIndentInterface) {
17✔
462
            $elementHelper->setIndent($element->getOption('in-group') ? $lf3Indent : $lf2Indent);
17✔
463
        }
464

465
        $elementString = $elementHelper->render($element);
17✔
466

467
        $elementString .= $errorContent . $messageContent;
17✔
468

469
        $elementString = $this->wrapInGroup(
17✔
470
            element: $element,
17✔
471
            elementString: $elementString,
17✔
472
            htmlHelper: $htmlHelper,
17✔
473
            indent: $lf2Indent,
17✔
474
        );
17✔
475

476
        $outerDiv = $lf1Indent . $htmlHelper->render(
17✔
477
            'div',
17✔
478
            $colAttributes,
17✔
479
            PHP_EOL . $elementString . PHP_EOL . $lf1Indent,
17✔
480
        );
17✔
481

482
        return $baseIndent . $htmlHelper->render(
17✔
483
            'div',
17✔
484
            $rowAttributes,
17✔
485
            PHP_EOL . $legend . PHP_EOL . $outerDiv . $helpContent . PHP_EOL . $baseIndent,
17✔
486
        );
17✔
487
    }
488

489
    /**
490
     * @throws DomainException
491
     * @throws InvalidArgumentException
492
     */
493
    private function renderVerticalRow(
25✔
494
        ElementInterface $element,
495
        string $label,
496
        string | null $labelPosition = null,
497
    ): string {
498
        $colAttributes   = $this->mergeAttributes($element, 'col_attributes', []);
25✔
499
        $labelAttributes = $this->mergeAttributes($element, 'label_attributes', ['form-label']);
25✔
500

501
        if ($element->hasAttribute('id')) {
25✔
502
            $id = $element->getAttribute('id');
23✔
503

504
            assert(is_string($id));
23✔
505

506
            $labelAttributes['for'] = $id;
23✔
507
        }
508

509
        $indent     = $this->getIndent();
25✔
510
        $htmlHelper = $this->getHtmlHelper();
25✔
511

512
        $elementHelper = $this->getElementHelper();
25✔
513
        assert($elementHelper instanceof FormElement);
25✔
514

515
        // Multicheckbox elements have to be handled differently as the HTML standard does not allow nested
516
        // labels. The semantic way is to group them inside a fieldset
517
        if (
518
            $element instanceof MultiCheckbox
25✔
519
            || $element instanceof MonthSelect
25✔
520
            || $element instanceof Captcha
25✔
521
        ) {
522
            $legendClasses    = [];
14✔
523
            $legendAttributes = $this->mergeAttributes($element, 'legend_attributes', ['form-label']);
14✔
524

525
            if (array_key_exists('class', $legendAttributes)) {
14✔
526
                $legendClasses = array_merge($legendClasses, explode(' ', $legendAttributes['class']));
14✔
527

528
                unset($legendAttributes['class']);
14✔
529
            }
530

531
            $legendAttributes['class'] = trim(implode(' ', array_unique($legendClasses)));
14✔
532

533
            $legend = $indent . $this->getWhitespace(4) . $htmlHelper->render(
14✔
534
                'legend',
14✔
535
                $legendAttributes,
14✔
536
                $label,
14✔
537
            );
14✔
538

539
            $errorContent   = '';
14✔
540
            $helpContent    = '';
14✔
541
            $messageContent = '';
14✔
542
            $floating       = $element->getOption('floating');
14✔
543

544
            $baseIndent = $indent;
14✔
545

546
            if ($floating) {
14✔
547
                $indent .= $this->getWhitespace(4);
7✔
548
            }
549

550
            $lf1Indent = $indent . $this->getWhitespace(4);
14✔
551
            $lf2Indent = $lf1Indent . $this->getWhitespace(4);
14✔
552
            $lf3Indent = $lf2Indent . $this->getWhitespace(4);
14✔
553

554
            $asCard        = $element->getOption('as-card');
14✔
555
            $asFormControl = $element->getOption('as-form-control');
14✔
556

557
            if ($this->renderErrors) {
14✔
558
                $errorContent = $this->renderFormErrors(
14✔
559
                    $element,
14✔
560
                    $asCard || $asFormControl ? $lf3Indent : $lf1Indent,
14✔
561
                );
14✔
562
            }
563

564
            if ($element->getOption('messages')) {
14✔
565
                $messageContent = $this->renderMessages(
2✔
566
                    $element,
2✔
567
                    $asCard || $asFormControl ? $lf3Indent : $lf1Indent,
2✔
568
                );
2✔
569
            }
570

571
            if ($element->getOption('help_content') !== null) {
14✔
572
                $helpContent = $this->renderFormHelp(
5✔
573
                    $element,
5✔
574
                    $floating ? $indent : $lf1Indent,
5✔
575
                    $colAttributes,
5✔
576
                );
5✔
577
            }
578

579
            if ($elementHelper instanceof FormIndentInterface) {
14✔
580
                $elementHelper->setIndent($element->getOption('as-card') ? $lf3Indent : $lf2Indent);
14✔
581
            }
582

583
            $elementString  = $elementHelper->render($element);
14✔
584
            $elementString .= $errorContent . $messageContent;
14✔
585

586
            $elementString = $this->wrapInContainer(
14✔
587
                element: $element,
14✔
588
                elementString: $elementString,
14✔
589
                htmlHelper: $htmlHelper,
14✔
590
                indent: $lf1Indent,
14✔
591
            );
14✔
592

593
            if ($floating) {
14✔
594
                $elementString = PHP_EOL . $lf1Indent . $elementString . PHP_EOL . '    ' . $legend . PHP_EOL . $indent;
7✔
595

596
                $elementString  = $indent . $htmlHelper->render(
7✔
597
                    'div',
7✔
598
                    ['class' => 'form-floating flex-fill'],
7✔
599
                    $elementString,
7✔
600
                );
7✔
601
                $elementString .= $helpContent;
7✔
602
            } else {
603
                $elementString = $legend . PHP_EOL . $lf1Indent . $elementString . $helpContent;
7✔
604
            }
605

606
            return $baseIndent . $htmlHelper->render(
14✔
607
                'fieldset',
14✔
608
                $colAttributes,
14✔
609
                PHP_EOL . $elementString . PHP_EOL . $baseIndent,
14✔
610
            );
14✔
611
        }
612

613
        if ($element instanceof Checkbox) {
25✔
614
            // this is a special case, because label is always rendered inside it
615
            $errorContent   = '';
20✔
616
            $helpContent    = '';
20✔
617
            $messageContent = '';
20✔
618
            $baseIndent     = $indent;
20✔
619
            $lf1Indent      = $indent . $this->getWhitespace(4);
20✔
620
            $lf2Indent      = $lf1Indent . $this->getWhitespace(4);
20✔
621
            $lf3Indent      = $lf2Indent . $this->getWhitespace(4);
20✔
622

623
            $asCard        = $element->getOption('as-card');
20✔
624
            $asFormControl = $element->getOption('as-form-control');
20✔
625

626
            if ($this->renderErrors) {
20✔
627
                $errorContent = $this->renderFormErrors(
20✔
628
                    $element,
20✔
629
                    $asCard || $asFormControl ? $lf3Indent : $lf1Indent,
20✔
630
                );
20✔
631
            }
632

633
            if ($element->getOption('messages')) {
20✔
634
                $messageContent = $this->renderMessages(
2✔
635
                    $element,
2✔
636
                    $asCard || $asFormControl ? $lf3Indent : $lf1Indent,
2✔
637
                );
2✔
638
            }
639

640
            if ($element->getOption('help_content') !== null) {
20✔
641
                $helpContent = $this->renderFormHelp($element, $lf1Indent, $colAttributes);
11✔
642
            }
643

644
            if ($elementHelper instanceof FormIndentInterface) {
20✔
645
                $elementHelper->setIndent($element->getOption('as-card') ? $lf3Indent : $lf2Indent);
20✔
646
            }
647

648
            $elementString  = $elementHelper->render($element);
20✔
649
            $elementString .= $errorContent . $messageContent;
20✔
650

651
            $elementString = $this->wrapInContainer(
20✔
652
                element: $element,
20✔
653
                elementString: $elementString,
20✔
654
                htmlHelper: $htmlHelper,
20✔
655
                indent: $lf1Indent,
20✔
656
            );
20✔
657

658
            return $baseIndent . $htmlHelper->render(
20✔
659
                'div',
20✔
660
                $colAttributes,
20✔
661
                PHP_EOL . $lf1Indent . $elementString . $helpContent . PHP_EOL . $baseIndent,
20✔
662
            );
20✔
663
        }
664

665
        $type = $element->getAttribute('type');
25✔
666

667
        if (
668
            $element instanceof Button
25✔
669
            || $element instanceof Submit
24✔
670
            || $element instanceof Fieldset
24✔
671
            || in_array($type, ['button', 'submit', 'reset'], true)
25✔
672
        ) {
673
            // this is a special case, because label is always rendered inside it
674
            $baseIndent = $indent;
24✔
675
            $lf1Indent  = $indent . $this->getWhitespace(4);
24✔
676

677
            if ($elementHelper instanceof FormIndentInterface) {
24✔
678
                $elementHelper->setIndent($lf1Indent);
22✔
679
            }
680

681
            $elementString = $elementHelper->render($element);
24✔
682

683
            return $baseIndent . $htmlHelper->render(
24✔
684
                'div',
24✔
685
                $colAttributes,
24✔
686
                PHP_EOL . $elementString . PHP_EOL . $baseIndent,
24✔
687
            );
24✔
688
        }
689

690
        $floating   = $element->getOption('floating');
23✔
691
        $baseIndent = $indent;
23✔
692

693
        if ($floating) {
23✔
694
            $indent .= $this->getWhitespace(4);
11✔
695
        }
696

697
        $lf1Indent = $indent . $this->getWhitespace(4);
23✔
698
        $lf2Indent = $lf1Indent . $this->getWhitespace(4);
23✔
699

700
        $errorContent   = '';
23✔
701
        $helpContent    = '';
23✔
702
        $messageContent = '';
23✔
703

704
        if ($this->renderErrors) {
23✔
705
            $errorContent = $this->renderFormErrors($element, $floating ? $indent : $lf1Indent);
23✔
706
        }
707

708
        if ($element->getOption('messages')) {
23✔
709
            $messageContent = $this->renderMessages($element, $floating ? $indent : $lf1Indent);
7✔
710
        }
711

712
        if ($element->getOption('help_content') !== null) {
23✔
713
            $helpContent = $this->renderFormHelp(
11✔
714
                $element,
11✔
715
                $floating ? $indent : $lf1Indent,
11✔
716
                $colAttributes,
11✔
717
            );
11✔
718
        }
719

720
        if ($elementHelper instanceof FormIndentInterface) {
23✔
721
            $elementHelper->setIndent($element->getOption('in-group') ? $lf2Indent : $lf1Indent);
23✔
722
        }
723

724
        $elementString = $elementHelper->render($element);
23✔
725

726
        if ($label === '') {
23✔
727
            $rendered = $elementString . $errorContent . $messageContent;
1✔
728
        } else {
729
            if ($element instanceof LabelAwareInterface) {
23✔
730
                if ($floating) {
23✔
731
                    $labelPosition = BaseFormRow::LABEL_APPEND;
11✔
732
                } elseif ($element->hasLabelOption('label_position')) {
12✔
733
                    $labelPosition = $element->getLabelOption('label_position');
1✔
734
                } else {
735
                    $labelPosition = BaseFormRow::LABEL_PREPEND;
12✔
736
                }
737
            }
738

739
            $labelHelper = $this->getLabelHelper();
23✔
740

741
            $legend = $labelHelper->openTag($labelAttributes) . $label . $labelHelper->closeTag();
23✔
742

743
            if ($labelPosition === BaseFormRow::LABEL_PREPEND) {
23✔
744
                $elementString .= $errorContent . $messageContent;
12✔
745
                $elementString  = $this->wrapInGroup(
12✔
746
                    element: $element,
12✔
747
                    elementString: $elementString,
12✔
748
                    htmlHelper: $htmlHelper,
12✔
749
                    indent: $lf1Indent,
12✔
750
                );
12✔
751

752
                $rendered = $lf1Indent . $legend . PHP_EOL . $elementString;
12✔
753
            } else {
754
                if (!$floating) {
12✔
755
                    $elementString .= $errorContent . $messageContent;
1✔
756
                    $elementString  = $this->wrapInGroup(
1✔
757
                        element: $element,
1✔
758
                        elementString: $elementString,
1✔
759
                        htmlHelper: $htmlHelper,
1✔
760
                        indent: $indent,
1✔
761
                    );
1✔
762
                }
763

764
                $rendered  = $elementString . PHP_EOL;
12✔
765
                $rendered .= $element->getOption('in-group') ? $lf2Indent : $lf1Indent;
12✔
766
                $rendered .= $legend;
12✔
767
            }
768
        }
769

770
        if ($floating) {
23✔
771
            $rendered  = PHP_EOL . $rendered . PHP_EOL;
11✔
772
            $rendered .= $element->getOption('in-group') ? $lf1Indent : $indent;
11✔
773
            $rendered  = $htmlHelper->render(
11✔
774
                'div',
11✔
775
                ['class' => 'form-floating flex-fill'],
11✔
776
                $rendered,
11✔
777
            );
11✔
778

779
            $rendered .= $errorContent . $messageContent;
11✔
780
            $rendered  = ($element->getOption('in-group') ? $lf1Indent : $indent) . $rendered;
11✔
781
            $rendered  = $this->wrapInGroup(
11✔
782
                element: $element,
11✔
783
                elementString: $rendered,
11✔
784
                htmlHelper: $htmlHelper,
11✔
785
                indent: $indent,
11✔
786
            );
11✔
787
        }
788

789
        $rendered .= $helpContent;
23✔
790

791
        return $baseIndent . $htmlHelper->render(
23✔
792
            'div',
23✔
793
            $colAttributes,
23✔
794
            PHP_EOL . $rendered . PHP_EOL . $baseIndent,
23✔
795
        );
23✔
796
    }
797

798
    /** @throws DomainException */
799
    private function renderFormErrors(ElementInterface $element, string $indent): string
41✔
800
    {
801
        $elementErrorsHelper = $this->getElementErrorsHelper();
41✔
802
        assert($elementErrorsHelper instanceof FormElementErrors);
41✔
803

804
        $elementErrorsHelper->setIndent($indent);
41✔
805
        $elementErrors = $elementErrorsHelper->render($element);
41✔
806

807
        if ($elementErrors !== '' && $element->hasAttribute('id')) {
41✔
808
            $ariaDesc = $element->hasAttribute('aria-describedby')
7✔
809
                ? $element->getAttribute('aria-describedby') . ' '
1✔
810
                : '';
7✔
811

812
            $ariaDesc .= $element->getAttribute('id') . 'Feedback';
7✔
813

814
            $element->setAttribute('aria-describedby', $ariaDesc);
7✔
815
        }
816

817
        return $elementErrors;
41✔
818
    }
819

820
    /**
821
     * @param array<string, array<mixed>|bool|float|int|string> $containerAttributes
822
     *
823
     * @throws void
824
     */
825
    private function renderFormHelp(ElementInterface $element, string $indent, array &$containerAttributes): string
21✔
826
    {
827
        $helpContent = $element->getOption('help_content');
21✔
828

829
        if (!is_string($helpContent) && !is_array($helpContent)) {
21✔
830
            return '';
3✔
831
        }
832

833
        if (is_string($helpContent) && $helpContent === '') {
21✔
834
            return '';
3✔
835
        }
836

837
        if (
838
            is_array($helpContent)
21✔
839
            && (
840
                !array_key_exists('content', $helpContent)
21✔
841
                || !is_string($helpContent['content'])
21✔
842
                || $helpContent['content'] === ''
21✔
843
            )
844
        ) {
845
            return '';
2✔
846
        }
847

848
        $classes = [];
21✔
849

850
        if (
851
            array_key_exists('class', $containerAttributes)
21✔
852
            && is_scalar($containerAttributes['class'])
21✔
853
        ) {
854
            $classes = explode(' ', (string) $containerAttributes['class']);
21✔
855

856
            unset($containerAttributes['class']);
21✔
857
        }
858

859
        $classes[] = 'has-help';
21✔
860

861
        $containerAttributes['class'] = implode(' ', array_unique($classes));
21✔
862

863
        $attributes = $this->mergeAttributes($element, 'help_attributes', ['toast']);
21✔
864

865
        assert(is_string($helpContent) || is_array($helpContent));
21✔
866

867
        if ($element->hasAttribute('id')) {
21✔
868
            $attributes['id'] = $element->getAttribute('id') . 'Help';
21✔
869

870
            $ariaDesc = $element->hasAttribute('aria-describedby')
21✔
871
                ? $element->getAttribute('aria-describedby') . ' '
5✔
872
                : '';
21✔
873

874
            $ariaDesc .= $element->getAttribute('id') . 'Help';
21✔
875

876
            $element->setAttribute('aria-describedby', $ariaDesc);
21✔
877
        }
878

879
        $htmlHelper = $this->getHtmlHelper();
21✔
880

881
        if (is_string($helpContent)) {
21✔
882
            return PHP_EOL . $indent . $htmlHelper->render('div', $attributes, $helpContent);
21✔
883
        }
884

885
        $lf1Indent = $indent . $this->getWhitespace(4);
2✔
886

887
        $content = $htmlHelper->render('div', ['class' => 'toast-body'], $helpContent['content']);
2✔
888
        $header  = '';
2✔
889

890
        if (
891
            array_key_exists('header', $helpContent)
2✔
892
            && is_string($helpContent['header'])
2✔
893
            && $helpContent['header'] !== ''
2✔
894
        ) {
895
            $header = $htmlHelper->render('div', ['class' => 'toast-header'], $helpContent['header']);
2✔
896
            $header = $lf1Indent . $header . PHP_EOL;
2✔
897
        }
898

899
        $content = $htmlHelper->render(
2✔
900
            'div',
2✔
901
            $attributes,
2✔
902
            PHP_EOL . $header . $lf1Indent . $content . PHP_EOL . $indent,
2✔
903
        );
2✔
904

905
        return PHP_EOL . $indent . $content;
2✔
906
    }
907

908
    /** @throws void */
909
    private function renderMessages(ElementInterface $element, string $indent): string
10✔
910
    {
911
        $messages = $element->getOption('messages');
10✔
912

913
        if (!is_array($messages)) {
10✔
914
            return '';
4✔
915
        }
916

917
        $messageContent = '';
10✔
918
        $htmlHelper     = $this->getHtmlHelper();
10✔
919

920
        foreach ($messages as $message) {
10✔
921
            assert(is_array($message));
10✔
922

923
            $content = $message['content'] ?? '';
10✔
924

925
            assert(is_string($content));
10✔
926

927
            if ($content === '') {
10✔
928
                continue;
4✔
929
            }
930

931
            $attributes = $message['attributes'] ?? [];
10✔
932

933
            if (array_key_exists('id', $attributes)) {
10✔
934
                $ariaDesc = $element->hasAttribute('aria-describedby')
4✔
935
                    ? $element->getAttribute('aria-describedby') . ' '
2✔
936
                    : '';
2✔
937

938
                $ariaDesc .= $attributes['id'];
4✔
939

940
                $element->setAttribute('aria-describedby', $ariaDesc);
4✔
941
            }
942

943
            $messageContent .= PHP_EOL . $indent . $htmlHelper->render('div', $attributes, $content);
10✔
944
        }
945

946
        return $messageContent;
10✔
947
    }
948

949
    /**
950
     * @param array<int|string, array{content?: string, attributes?: array<int|string, bool|float|int|iterable<int, string>|stdClass|string|null>}> $messages
951
     *
952
     * @throws void
953
     */
954
    private function renderGroupContent(array $messages, string $indent): string
6✔
955
    {
956
        $messageContents = [];
6✔
957
        $htmlHelper      = $this->getHtmlHelper();
6✔
958

959
        foreach ($messages as $message) {
6✔
960
            assert(is_array($message));
6✔
961

962
            $content = $message['content'] ?? '';
6✔
963

964
            if ($content === '') {
6✔
965
                continue;
3✔
966
            }
967

968
            $attributes = $message['attributes'] ?? [];
6✔
969

970
            $messageContents[] = $htmlHelper->render('div', $attributes, $content);
6✔
971
        }
972

973
        return PHP_EOL . $indent . implode(PHP_EOL . $indent, $messageContents);
6✔
974
    }
975

976
    /**
977
     * @param array<int, string> $classes
978
     *
979
     * @return array<string, string>
980
     *
981
     * @throws void
982
     */
983
    private function mergeAttributes(ElementInterface $element, string $optionName, array $classes = []): array
43✔
984
    {
985
        $attributes = $element->getOption($optionName) ?? [];
43✔
986
        assert(is_array($attributes));
43✔
987

988
        if (array_key_exists('class', $attributes)) {
43✔
989
            $classes = array_merge($classes, explode(' ', (string) $attributes['class']));
34✔
990

991
            unset($attributes['class']);
34✔
992
        }
993

994
        $form = $element->getOption('form');
43✔
995
        assert(
43✔
996
            $form instanceof FormInterface || $form === null,
43✔
997
            sprintf(
43✔
998
                '$form should be an Instance of %s or null, but was %s',
43✔
999
                FormInterface::class,
43✔
1000
                get_debug_type($form),
43✔
1001
            ),
43✔
1002
        );
43✔
1003

1004
        if ($form !== null) {
43✔
1005
            $formAttributes = $form->getOption($optionName) ?? [];
42✔
1006

1007
            assert(is_array($formAttributes));
42✔
1008

1009
            if (array_key_exists('class', $formAttributes)) {
42✔
1010
                $classes = array_merge($classes, explode(' ', (string) $formAttributes['class']));
28✔
1011

1012
                unset($formAttributes['class']);
28✔
1013
            }
1014

1015
            $attributes = [...$formAttributes, ...$attributes];
42✔
1016
        }
1017

1018
        if ($classes) {
43✔
1019
            $attributes['class'] = implode(' ', array_unique($classes));
43✔
1020
        }
1021

1022
        return $attributes;
43✔
1023
    }
1024

1025
    /**
1026
     * @param InputFilterInterface<TFilteredValues> $inputFilter
1027
     *
1028
     * @return InputFilterInterface<mixed>|InputInterface|null
1029
     *
1030
     * @throws void
1031
     *
1032
     * @template TFilteredValues
1033
     */
1034
    private function getInputFilter(
43✔
1035
        string $elementName,
1036
        InputFilterInterface $inputFilter,
1037
        ElementInterface $element,
1038
        int $level = 0,
1039
    ): InputInterface | InputFilterInterface | null {
1040
        if ($inputFilter->has($elementName)) {
43✔
1041
            $filter = $inputFilter->get($elementName);
41✔
1042

1043
            if ($filter instanceof InputInterface) {
41✔
1044
                return $filter;
41✔
1045
            }
1046
        }
1047

1048
        $fieldset = $element->getOption('fieldset');
19✔
1049
        assert(
19✔
1050
            $fieldset instanceof FieldsetInterface || $fieldset === null,
19✔
1051
            sprintf(
19✔
1052
                '$fieldset should be an Instance of %s or null, but was %s',
19✔
1053
                FieldsetInterface::class,
19✔
1054
                get_debug_type($fieldset),
19✔
1055
            ),
19✔
1056
        );
19✔
1057

1058
        if (!$fieldset instanceof InputFilterProviderInterface || $fieldset->getName() === null) {
19✔
1059
            return null;
17✔
1060
        }
1061

1062
        $fieldsetName         = $fieldset->getName();
2✔
1063
        $fieldsetNameOriginal = $fieldsetName;
2✔
1064

1065
        if (!$inputFilter->has($fieldsetNameOriginal) && str_contains($fieldsetNameOriginal, '[')) {
2✔
1066
            $startPos = mb_strpos($fieldsetNameOriginal, '[');
1✔
1067
            $endPos   = mb_strpos($fieldsetNameOriginal, ']', $startPos + 1);
1✔
1068

1069
            if ($startPos !== false && $endPos !== false) {
1✔
1070
                $baseFieldsetName = mb_substr($fieldsetNameOriginal, 0, $startPos);
1✔
1071
                $fieldsetName     = mb_substr(
1✔
1072
                    $fieldsetNameOriginal,
1✔
1073
                    $startPos + 1,
1✔
1074
                    $endPos - $startPos - 1,
1✔
1075
                );
1✔
1076

1077
                if ($inputFilter->has($baseFieldsetName)) {
1✔
1078
                    $baseFilter = $inputFilter->get($baseFieldsetName);
1✔
1079

1080
                    if ($baseFilter instanceof InputFilterInterface) {
1✔
1081
                        return $this->getInputFilter(
1✔
1082
                            elementName: str_replace(
1✔
1083
                                $fieldsetNameOriginal,
1✔
1084
                                $fieldsetName,
1✔
1085
                                $elementName,
1✔
1086
                            ),
1✔
1087
                            inputFilter: $baseFilter,
1✔
1088
                            element: $element,
1✔
1089
                            level: $level + 1,
1✔
1090
                        );
1✔
1091
                    }
1092
                }
1093
            }
1094
        }
1095

1096
        if (!$inputFilter->has($fieldsetName)) {
2✔
1097
            return null;
×
1098
        }
1099

1100
        $filter = $inputFilter->get($fieldsetName);
2✔
1101

1102
        if ($filter instanceof InputInterface) {
2✔
1103
            return $filter;
×
1104
        }
1105

1106
        $originalElementName = mb_substr($elementName, mb_strlen($fieldsetName) + 1, -1);
2✔
1107

1108
        if ($filter->has($originalElementName)) {
2✔
1109
            $subFilter = $filter->get($originalElementName);
2✔
1110

1111
            if ($subFilter instanceof InputInterface) {
2✔
1112
                return $subFilter;
2✔
1113
            }
1114

1115
            return $this->getInputFilter(
×
1116
                elementName: $originalElementName,
×
1117
                inputFilter: $subFilter,
×
1118
                element: $element,
×
1119
                level: $level + 1,
×
1120
            );
×
1121
        }
1122

1123
        return null;
×
1124
    }
1125

1126
    /** @throws void */
1127
    private function wrapInContainer(
37✔
1128
        ElementInterface $element,
1129
        string $elementString,
1130
        FormHtmlInterface $htmlHelper,
1131
        string $indent,
1132
    ): string {
1133
        $asCard        = $element->getOption('as-card');
37✔
1134
        $asFormControl = $element->getOption('as-form-control');
37✔
1135

1136
        if ($asCard || $asFormControl) {
37✔
1137
            if ($asCard) {
37✔
1138
                $controlClasses = ['card', 'has-validation'];
34✔
1139

1140
                $lf1Indent = $indent . $this->getWhitespace(4);
34✔
1141

1142
                $elementString = $lf1Indent . $htmlHelper->render(
34✔
1143
                    'div',
34✔
1144
                    ['class' => 'card-body'],
34✔
1145
                    PHP_EOL . $elementString . PHP_EOL . $lf1Indent,
34✔
1146
                );
34✔
1147
            } else {
1148
                $controlClasses = ['form-control', 'has-validation'];
3✔
1149
            }
1150

1151
            if ($element->getAttribute('required')) {
37✔
1152
                $controlClasses[] = 'required';
37✔
1153
            }
1154

1155
            $elementString = $htmlHelper->render(
37✔
1156
                'div',
37✔
1157
                ['class' => implode(' ', $controlClasses)],
37✔
1158
                PHP_EOL . $elementString . PHP_EOL . $indent,
37✔
1159
            );
37✔
1160
        }
1161

1162
        return $elementString;
37✔
1163
    }
1164

1165
    /** @throws void */
1166
    private function wrapInGroup(
38✔
1167
        ElementInterface $element,
1168
        string $elementString,
1169
        FormHtmlInterface $htmlHelper,
1170
        string $indent,
1171
    ): string {
1172
        $inGroup = $element->getOption('in-group');
38✔
1173

1174
        if ($inGroup) {
38✔
1175
            $prefixes = $element->getOption('group-prefixes');
6✔
1176
            $suffixes = $element->getOption('group-suffixes');
6✔
1177

1178
            $lf1Indent = $indent . $this->getWhitespace(4);
6✔
1179

1180
            $elementString = PHP_EOL . $elementString;
6✔
1181

1182
            if (is_array($prefixes)) {
6✔
1183
                $prefixContent = $this->renderGroupContent($prefixes, $lf1Indent);
3✔
1184

1185
                $elementString = $prefixContent . $elementString;
3✔
1186
            }
1187

1188
            if (is_array($suffixes)) {
6✔
1189
                $suffixContent = $this->renderGroupContent($suffixes, $lf1Indent);
6✔
1190

1191
                $elementString .= $suffixContent;
6✔
1192
            }
1193

1194
            $controlClasses = ['input-group', 'has-validation'];
6✔
1195

1196
            if ($element->getAttribute('required')) {
6✔
1197
                $controlClasses[] = 'required';
6✔
1198
            }
1199

1200
            $elementString = $htmlHelper->render(
6✔
1201
                'div',
6✔
1202
                ['class' => implode(' ', $controlClasses)],
6✔
1203
                $elementString . PHP_EOL . $indent,
6✔
1204
            );
6✔
1205

1206
            $elementString = $indent . $elementString;
6✔
1207
        }
1208

1209
        return $elementString;
38✔
1210
    }
1211
}
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