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

mimmi20 / laminasviewrenderer-bootstrap-form / 12160906017

04 Dec 2024 01:37PM UTC coverage: 97.314%. Remained the same
12160906017

push

github

web-flow
Merge pull request #310 from mimmi20/updates

upgrade to PHP 8.3

60 of 61 new or added lines in 40 files covered. (98.36%)

38 existing lines in 7 files now uncovered.

2174 of 2234 relevant lines covered (97.31%)

25.52 hits per line

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

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

3
/**
4
 * This file is part of the mimmi20/laminasviewrenderer-bootstrap-form package.
5
 *
6
 * Copyright (c) 2021-2024, 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
/** @SuppressWarnings(PHPMD.ExcessiveClassComplexity) */
59
final class FormRow extends BaseFormRow implements FormRowInterface
60
{
61
    use FormTrait;
62
    use HiddenHelperTrait;
63
    use HtmlHelperTrait;
64

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

195
            $errorContent = '';
31✔
196

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

545
            $baseIndent = $indent;
14✔
546

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

790
        $rendered .= $helpContent;
23✔
791

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

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

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

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

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

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

818
        return $elementErrors;
41✔
819
    }
820

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

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

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

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

849
        $classes = [];
21✔
850

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

945
        return $messageContent;
10✔
946
    }
947

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1021
        return $attributes;
43✔
1022
    }
1023

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

UNCOV
1122
        return null;
×
1123
    }
1124

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

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

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

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

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

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

1161
        return $elementString;
37✔
1162
    }
1163

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

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

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

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

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

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

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

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

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

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

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

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

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

© 2025 Coveralls, Inc