1<?php
2
3declare(strict_types=1);
4
5/*
6 * This file is part of the TYPO3 CMS project.
7 *
8 * It is free software; you can redistribute it and/or modify it under
9 * the terms of the GNU General Public License, either version 2
10 * of the License, or any later version.
11 *
12 * For the full copyright and license information, please read the
13 * LICENSE.txt file that was distributed with this source code.
14 *
15 * The TYPO3 project - inspiring people to share!
16 */
17
18/*
19 * Inspired by and partially taken from the Neos.Form package (www.neos.io)
20 */
21
22namespace TYPO3\CMS\Form\Domain\Model;
23
24use TYPO3\CMS\Core\Utility\ArrayUtility;
25use TYPO3\CMS\Extbase\Mvc\Web\Request;
26use TYPO3\CMS\Extbase\Mvc\Web\Response;
27use TYPO3\CMS\Extbase\Object\ObjectManagerInterface;
28use TYPO3\CMS\Extbase\Reflection\ObjectAccess;
29use TYPO3\CMS\Form\Domain\Exception\IdentifierNotValidException;
30use TYPO3\CMS\Form\Domain\Exception\TypeDefinitionNotFoundException;
31use TYPO3\CMS\Form\Domain\Finishers\FinisherInterface;
32use TYPO3\CMS\Form\Domain\Model\Exception\DuplicateFormElementException;
33use TYPO3\CMS\Form\Domain\Model\Exception\FinisherPresetNotFoundException;
34use TYPO3\CMS\Form\Domain\Model\Exception\FormDefinitionConsistencyException;
35use TYPO3\CMS\Form\Domain\Model\FormElements\FormElementInterface;
36use TYPO3\CMS\Form\Domain\Model\FormElements\Page;
37use TYPO3\CMS\Form\Domain\Model\Renderable\AbstractCompositeRenderable;
38use TYPO3\CMS\Form\Domain\Model\Renderable\RenderableInterface;
39use TYPO3\CMS\Form\Domain\Model\Renderable\VariableRenderableInterface;
40use TYPO3\CMS\Form\Domain\Runtime\FormRuntime;
41use TYPO3\CMS\Form\Exception as FormException;
42use TYPO3\CMS\Form\Mvc\ProcessingRule;
43
44/**
45 * This class encapsulates a complete *Form Definition*, with all of its pages,
46 * form elements, validation rules which apply and finishers which should be
47 * executed when the form is completely filled in.
48 *
49 * It is *not modified* when the form executes.
50 *
51 * The Anatomy Of A Form
52 * =====================
53 *
54 * A FormDefinition consists of multiple *Page* ({@link Page}) objects. When a
55 * form is displayed to the user, only one *Page* is visible at any given time,
56 * and there is a navigation to go back and forth between the pages.
57 *
58 * A *Page* consists of multiple *FormElements* ({@link FormElementInterface}, {@link AbstractFormElement}),
59 * which represent the input fields, textareas, checkboxes shown inside the page.
60 *
61 * *FormDefinition*, *Page* and *FormElement* have *identifier* properties, which
62 * must be unique for each given type (i.e. it is allowed that the FormDefinition and
63 * a FormElement have the *same* identifier, but two FormElements are not allowed to
64 * have the same identifier.
65 *
66 * Simple Example
67 * --------------
68 *
69 * Generally, you can create a FormDefinition manually by just calling the API
70 * methods on it, or you use a *Form Definition Factory* to build the form from
71 * another representation format such as YAML.
72 *
73 * /---code php
74 * $formDefinition = $this->objectManager->get(FormDefinition::class, 'myForm');
75 *
76 * $page1 = $this->objectManager->get(Page::class, 'page1');
77 * $formDefinition->addPage($page);
78 *
79 * $element1 = $this->objectManager->get(GenericFormElement::class, 'title', 'Textfield'); # the second argument is the type of the form element
80 * $page1->addElement($element1);
81 * \---
82 *
83 * Creating a Form, Using Abstract Form Element Types
84 * =====================================================
85 *
86 * While you can use the {@link FormDefinition::addPage} or {@link Page::addElement}
87 * methods and create the Page and FormElement objects manually, it is often better
88 * to use the corresponding create* methods ({@link FormDefinition::createPage}
89 * and {@link Page::createElement}), as you pass them an abstract *Form Element Type*
90 * such as *Text* or *Page*, and the system **automatically
91 * resolves the implementation class name and sets default values**.
92 *
93 * So the simple example from above should be rewritten as follows:
94 *
95 * /---code php
96 * $prototypeConfiguration = []; // We'll talk about this later
97 *
98 * $formDefinition = $this->objectManager->get(FormDefinition::class, 'myForm', $prototypeConfiguration);
99 * $page1 = $formDefinition->createPage('page1');
100 * $element1 = $page1->addElement('title', 'Textfield');
101 * \---
102 *
103 * Now, you might wonder how the system knows that the element *Textfield*
104 * is implemented using a GenericFormElement: **This is configured in the $prototypeConfiguration**.
105 *
106 * To make the example from above actually work, we need to add some sensible
107 * values to *$prototypeConfiguration*:
108 *
109 * <pre>
110 * $prototypeConfiguration = [
111 *   'formElementsDefinition' => [
112 *     'Page' => [
113 *       'implementationClassName' => 'TYPO3\CMS\Form\Domain\Model\FormElements\Page'
114 *     ],
115 *     'Textfield' => [
116 *       'implementationClassName' => 'TYPO3\CMS\Form\Domain\Model\FormElements\GenericFormElement'
117 *     ]
118 *   ]
119 * ]
120 * </pre>
121 *
122 * For each abstract *Form Element Type* we add some configuration; in the above
123 * case only the *implementation class name*. Still, it is possible to set defaults
124 * for *all* configuration options of such an element, as the following example
125 * shows:
126 *
127 * <pre>
128 * $prototypeConfiguration = [
129 *   'formElementsDefinition' => [
130 *     'Page' => [
131 *       'implementationClassName' => 'TYPO3\CMS\Form\Domain\Model\FormElements\Page',
132 *       'label' => 'this is the label of the page if nothing is specified'
133 *     ],
134 *     'Textfield' => [
135 *       'implementationClassName' => 'TYPO3\CMS\Form\Domain\Model\FormElements\GenericFormElement',
136 *       'label' = >'Default Label',
137 *       'defaultValue' => 'Default form element value',
138 *       'properties' => [
139 *         'placeholder' => 'Text which is shown if element is empty'
140 *       ]
141 *     ]
142 *   ]
143 * ]
144 * </pre>
145 *
146 * Using Preconfigured $prototypeConfiguration
147 * ---------------------------------
148 *
149 * Often, it is not really useful to manually create the $prototypeConfiguration array.
150 *
151 * Most of it comes pre-configured inside the YAML settings of the extensions,
152 * and the {@link \TYPO3\CMS\Form\Domain\Configuration\ConfigurationService} contains helper methods
153 * which return the ready-to-use *$prototypeConfiguration*.
154 *
155 * Property Mapping and Validation Rules
156 * =====================================
157 *
158 * Besides Pages and FormElements, the FormDefinition can contain information
159 * about the *format of the data* which is inputted into the form. This generally means:
160 *
161 * - expected Data Types
162 * - Property Mapping Configuration to be used
163 * - Validation Rules which should apply
164 *
165 * Background Info
166 * ---------------
167 * You might wonder why Data Types and Validation Rules are *not attached
168 * to each FormElement itself*.
169 *
170 * If the form should create a *hierarchical output structure* such as a multi-
171 * dimensional array or a PHP object, your expected data structure might look as follows:
172 * <pre>
173 * - person
174 * -- firstName
175 * -- lastName
176 * -- address
177 * --- street
178 * --- city
179 * </pre>
180 *
181 * Now, let's imagine you want to edit *person.address.street* and *person.address.city*,
182 * but want to validate that the *combination* of *street* and *city* is valid
183 * according to some address database.
184 *
185 * In this case, the form elements would be configured to fill *street* and *city*,
186 * but the *validator* needs to be attached to the *compound object* *address*,
187 * as both parts need to be validated together.
188 *
189 * Connecting FormElements to the output data structure
190 * ====================================================
191 *
192 * The *identifier* of the *FormElement* is most important, as it determines
193 * where in the output structure the value which is entered by the user is placed,
194 * and thus also determines which validation rules need to apply.
195 *
196 * Using the above example, if you want to create a FormElement for the *street*,
197 * you should use the identifier *person.address.street*.
198 *
199 * Rendering a FormDefinition
200 * ==========================
201 *
202 * In order to trigger *rendering* on a FormDefinition,
203 * the current {@link \TYPO3\CMS\Extbase\Mvc\Web\Request} needs to be bound to the FormDefinition,
204 * resulting in a {@link \TYPO3\CMS\Form\Domain\Runtime\FormRuntime} object which contains the *Runtime State* of the form
205 * (such as the currently inserted values).
206 *
207 * /---code php
208 * # $currentRequest and $currentResponse need to be available, f.e. inside a controller you would
209 * # use $this->request and $this->response; inside a ViewHelper you would use $this->controllerContext->getRequest()
210 * # and $this->controllerContext->getResponse()
211 * $form = $formDefinition->bind($currentRequest, $currentResponse);
212 *
213 * # now, you can use the $form object to get information about the currently
214 * # entered values into the form, etc.
215 * \---
216 *
217 * Refer to the {@link \TYPO3\CMS\Form\Domain\Runtime\FormRuntime} API doc for further information.
218 *
219 * Scope: frontend
220 * **This class is NOT meant to be sub classed by developers.**
221 */
222class FormDefinition extends AbstractCompositeRenderable implements VariableRenderableInterface
223{
224
225    /**
226     * @var \TYPO3\CMS\Extbase\Object\ObjectManagerInterface
227     */
228    protected $objectManager;
229
230    /**
231     * The finishers for this form
232     *
233     * @var \TYPO3\CMS\Form\Domain\Finishers\FinisherInterface[]
234     */
235    protected $finishers = [];
236
237    /**
238     * Property Mapping Rules, indexed by element identifier
239     *
240     * @var \TYPO3\CMS\Form\Mvc\ProcessingRule[]
241     */
242    protected $processingRules = [];
243
244    /**
245     * Contains all elements of the form, indexed by identifier.
246     * Is used as internal cache as we need this really often.
247     *
248     * @var \TYPO3\CMS\Form\Domain\Model\FormElements\FormElementInterface[]
249     */
250    protected $elementsByIdentifier = [];
251
252    /**
253     * Form element default values in the format ['elementIdentifier' => 'default value']
254     *
255     * @var array
256     */
257    protected $elementDefaultValues = [];
258
259    /**
260     * Renderer class name to be used.
261     *
262     * @var string
263     */
264    protected $rendererClassName = '';
265
266    /**
267     * @var array
268     */
269    protected $typeDefinitions;
270
271    /**
272     * @var array
273     */
274    protected $validatorsDefinition;
275
276    /**
277     * @var array
278     */
279    protected $finishersDefinition;
280
281    /**
282     * @var array
283     */
284    protected $conditionContextDefinition;
285
286    /**
287     * The persistence identifier of the form
288     *
289     * @var string
290     */
291    protected $persistenceIdentifier;
292
293    /**
294     * @param \TYPO3\CMS\Extbase\Object\ObjectManagerInterface $objectManager
295     * @internal
296     */
297    public function injectObjectManager(ObjectManagerInterface $objectManager)
298    {
299        $this->objectManager = $objectManager;
300    }
301
302    /**
303     * Constructor. Creates a new FormDefinition with the given identifier.
304     *
305     * @param string $identifier The Form Definition's identifier, must be a non-empty string.
306     * @param array $prototypeConfiguration overrides form defaults of this definition
307     * @param string $type element type of this form
308     * @param string $persistenceIdentifier the persistence identifier of the form
309     * @throws IdentifierNotValidException if the identifier was not valid
310     */
311    public function __construct(
312        string $identifier,
313        array $prototypeConfiguration = [],
314        string $type = 'Form',
315        string $persistenceIdentifier = null
316    ) {
317        $this->typeDefinitions = $prototypeConfiguration['formElementsDefinition'] ?? [];
318        $this->validatorsDefinition = $prototypeConfiguration['validatorsDefinition'] ?? [];
319        $this->finishersDefinition = $prototypeConfiguration['finishersDefinition'] ?? [];
320        $this->conditionContextDefinition = $prototypeConfiguration['conditionContextDefinition'] ?? [];
321
322        if (!is_string($identifier) || strlen($identifier) === 0) {
323            throw new IdentifierNotValidException('The given identifier was not a string or the string was empty.', 1477082503);
324        }
325
326        $this->identifier = $identifier;
327        $this->type = $type;
328        $this->persistenceIdentifier = $persistenceIdentifier;
329
330        if ($prototypeConfiguration !== []) {
331            $this->initializeFromFormDefaults();
332        }
333    }
334
335    /**
336     * Initialize the form defaults of the current type
337     *
338     * @throws TypeDefinitionNotFoundException
339     * @internal
340     */
341    protected function initializeFromFormDefaults()
342    {
343        if (!isset($this->typeDefinitions[$this->type])) {
344            throw new TypeDefinitionNotFoundException(sprintf('Type "%s" not found. Probably some configuration is missing.', $this->type), 1474905835);
345        }
346        $typeDefinition = $this->typeDefinitions[$this->type];
347        $this->setOptions($typeDefinition);
348    }
349
350    /**
351     * Set multiple properties of this object at once.
352     * Every property which has a corresponding set* method can be set using
353     * the passed $options array.
354     *
355     * @param array $options
356     * @param bool $resetFinishers
357     * @internal
358     */
359    public function setOptions(array $options, bool $resetFinishers = false)
360    {
361        if (isset($options['rendererClassName'])) {
362            $this->setRendererClassName($options['rendererClassName']);
363        }
364        if (isset($options['label'])) {
365            $this->setLabel($options['label']);
366        }
367        if (isset($options['renderingOptions'])) {
368            foreach ($options['renderingOptions'] as $key => $value) {
369                $this->setRenderingOption($key, $value);
370            }
371        }
372        if (isset($options['finishers'])) {
373            if ($resetFinishers) {
374                $this->finishers = [];
375            }
376            foreach ($options['finishers'] as $finisherConfiguration) {
377                $this->createFinisher($finisherConfiguration['identifier'], $finisherConfiguration['options'] ?? []);
378            }
379        }
380
381        if (isset($options['variants'])) {
382            foreach ($options['variants'] as $variantConfiguration) {
383                $this->createVariant($variantConfiguration);
384            }
385        }
386
387        ArrayUtility::assertAllArrayKeysAreValid(
388            $options,
389            ['rendererClassName', 'renderingOptions', 'finishers', 'formEditor', 'label', 'variants']
390        );
391    }
392
393    /**
394     * Create a page with the given $identifier and attach this page to the form.
395     *
396     * - Create Page object based on the given $typeName
397     * - set defaults inside the Page object
398     * - attach Page object to this form
399     * - return the newly created Page object
400     *
401     * @param string $identifier Identifier of the new page
402     * @param string $typeName Type of the new page
403     * @return Page the newly created page
404     * @throws TypeDefinitionNotFoundException
405     */
406    public function createPage(string $identifier, string $typeName = 'Page'): Page
407    {
408        if (!isset($this->typeDefinitions[$typeName])) {
409            throw new TypeDefinitionNotFoundException(sprintf('Type "%s" not found. Probably some configuration is missing.', $typeName), 1474905953);
410        }
411
412        $typeDefinition = $this->typeDefinitions[$typeName];
413
414        if (!isset($typeDefinition['implementationClassName'])) {
415            throw new TypeDefinitionNotFoundException(sprintf('The "implementationClassName" was not set in type definition "%s".', $typeName), 1477083126);
416        }
417        $implementationClassName = $typeDefinition['implementationClassName'];
418        /** @var Page $page */
419        $page = $this->objectManager->get($implementationClassName, $identifier, $typeName);
420
421        if (isset($typeDefinition['label'])) {
422            $page->setLabel($typeDefinition['label']);
423        }
424
425        if (isset($typeDefinition['renderingOptions'])) {
426            foreach ($typeDefinition['renderingOptions'] as $key => $value) {
427                $page->setRenderingOption($key, $value);
428            }
429        }
430
431        ArrayUtility::assertAllArrayKeysAreValid(
432            $typeDefinition,
433            ['implementationClassName', 'label', 'renderingOptions', 'formEditor']
434        );
435
436        $this->addPage($page);
437        return $page;
438    }
439
440    /**
441     * Add a new page at the end of the form.
442     *
443     * Instead of this method, you should often use {@link createPage} instead.
444     *
445     * @param Page $page
446     * @throws FormDefinitionConsistencyException if Page is already added to a FormDefinition
447     * @see createPage
448     */
449    public function addPage(Page $page)
450    {
451        $this->addRenderable($page);
452    }
453
454    /**
455     * Get the Form's pages
456     *
457     * @return array|Page[] The Form's pages in the correct order
458     */
459    public function getPages(): array
460    {
461        return $this->renderables;
462    }
463
464    /**
465     * Check whether a page with the given $index exists
466     *
467     * @param int $index
468     * @return bool TRUE if a page with the given $index exists, otherwise FALSE
469     */
470    public function hasPageWithIndex(int $index): bool
471    {
472        return isset($this->renderables[$index]);
473    }
474
475    /**
476     * Get the page with the passed index. The first page has index zero.
477     *
478     * If page at $index does not exist, an exception is thrown. @see hasPageWithIndex()
479     *
480     * @param int $index
481     * @return Page the page, or NULL if none found.
482     * @throws FormException if the specified index does not exist
483     */
484    public function getPageByIndex(int $index)
485    {
486        if (!$this->hasPageWithIndex($index)) {
487            throw new FormException(sprintf('There is no page with an index of %d', $index), 1329233627);
488        }
489        return $this->renderables[$index];
490    }
491
492    /**
493     * Adds the specified finisher to this form
494     *
495     * @param FinisherInterface $finisher
496     */
497    public function addFinisher(FinisherInterface $finisher)
498    {
499        $this->finishers[] = $finisher;
500    }
501
502    /**
503     * @param string $finisherIdentifier identifier of the finisher as registered in the current form (for example: "Redirect")
504     * @param array $options options for this finisher in the format ['option1' => 'value1', 'option2' => 'value2', ...]
505     * @return FinisherInterface
506     * @throws FinisherPresetNotFoundException
507     */
508    public function createFinisher(string $finisherIdentifier, array $options = []): FinisherInterface
509    {
510        if (isset($this->finishersDefinition[$finisherIdentifier]) && is_array($this->finishersDefinition[$finisherIdentifier]) && isset($this->finishersDefinition[$finisherIdentifier]['implementationClassName'])) {
511            $implementationClassName = $this->finishersDefinition[$finisherIdentifier]['implementationClassName'];
512            $defaultOptions = $this->finishersDefinition[$finisherIdentifier]['options'] ?? [];
513            ArrayUtility::mergeRecursiveWithOverrule($defaultOptions, $options);
514
515            /** @var FinisherInterface $finisher */
516            $finisher = $this->objectManager->get($implementationClassName, $finisherIdentifier);
517            $finisher->setOptions($defaultOptions);
518            $this->addFinisher($finisher);
519            return $finisher;
520        }
521        throw new FinisherPresetNotFoundException('The finisher preset identified by "' . $finisherIdentifier . '" could not be found, or the implementationClassName was not specified.', 1328709784);
522    }
523
524    /**
525     * Gets all finishers of this form
526     *
527     * @return \TYPO3\CMS\Form\Domain\Finishers\FinisherInterface[]
528     */
529    public function getFinishers(): array
530    {
531        return $this->finishers;
532    }
533
534    /**
535     * Add an element to the ElementsByIdentifier Cache.
536     *
537     * @param RenderableInterface $renderable
538     * @throws DuplicateFormElementException
539     * @internal
540     */
541    public function registerRenderable(RenderableInterface $renderable)
542    {
543        if ($renderable instanceof FormElementInterface) {
544            if (isset($this->elementsByIdentifier[$renderable->getIdentifier()])) {
545                throw new DuplicateFormElementException(sprintf('A form element with identifier "%s" is already part of the form.', $renderable->getIdentifier()), 1325663761);
546            }
547            $this->elementsByIdentifier[$renderable->getIdentifier()] = $renderable;
548        }
549    }
550
551    /**
552     * Remove an element from the ElementsByIdentifier cache
553     *
554     * @param RenderableInterface $renderable
555     * @internal
556     */
557    public function unregisterRenderable(RenderableInterface $renderable)
558    {
559        if ($renderable instanceof FormElementInterface) {
560            unset($this->elementsByIdentifier[$renderable->getIdentifier()]);
561        }
562    }
563
564    /**
565     * Get all form elements with their identifiers as keys
566     *
567     * @return FormElementInterface[]
568     */
569    public function getElements(): array
570    {
571        return $this->elementsByIdentifier;
572    }
573
574    /**
575     * Get a Form Element by its identifier
576     *
577     * If identifier does not exist, returns NULL.
578     *
579     * @param string $elementIdentifier
580     * @return FormElementInterface The element with the given $elementIdentifier or NULL if none found
581     */
582    public function getElementByIdentifier(string $elementIdentifier)
583    {
584        return $this->elementsByIdentifier[$elementIdentifier] ?? null;
585    }
586
587    /**
588     * Sets the default value of a form element
589     *
590     * @param string $elementIdentifier identifier of the form element. This supports property paths!
591     * @param mixed $defaultValue
592     * @internal
593     */
594    public function addElementDefaultValue(string $elementIdentifier, $defaultValue)
595    {
596        $this->elementDefaultValues = ArrayUtility::setValueByPath(
597            $this->elementDefaultValues,
598            $elementIdentifier,
599            $defaultValue,
600            '.'
601        );
602    }
603
604    /**
605     * returns the default value of the specified form element
606     * or NULL if no default value was set
607     *
608     * @param string $elementIdentifier identifier of the form element. This supports property paths!
609     * @return mixed The elements default value
610     * @internal
611     */
612    public function getElementDefaultValueByIdentifier(string $elementIdentifier)
613    {
614        return ObjectAccess::getPropertyPath($this->elementDefaultValues, $elementIdentifier);
615    }
616
617    /**
618     * Move $pageToMove before $referencePage
619     *
620     * @param Page $pageToMove
621     * @param Page $referencePage
622     */
623    public function movePageBefore(Page $pageToMove, Page $referencePage)
624    {
625        $this->moveRenderableBefore($pageToMove, $referencePage);
626    }
627
628    /**
629     * Move $pageToMove after $referencePage
630     *
631     * @param Page $pageToMove
632     * @param Page $referencePage
633     */
634    public function movePageAfter(Page $pageToMove, Page $referencePage)
635    {
636        $this->moveRenderableAfter($pageToMove, $referencePage);
637    }
638
639    /**
640     * Remove $pageToRemove from form
641     *
642     * @param Page $pageToRemove
643     */
644    public function removePage(Page $pageToRemove)
645    {
646        $this->removeRenderable($pageToRemove);
647    }
648
649    /**
650     * Bind the current request & response to this form instance, effectively creating
651     * a new "instance" of the Form.
652     *
653     * @param Request $request
654     * @param Response $response
655     * @return FormRuntime
656     */
657    public function bind(Request $request, Response $response): FormRuntime
658    {
659        return $this->objectManager->get(FormRuntime::class, $this, $request, $response);
660    }
661
662    /**
663     * @param string $propertyPath
664     * @return ProcessingRule
665     */
666    public function getProcessingRule(string $propertyPath): ProcessingRule
667    {
668        if (!isset($this->processingRules[$propertyPath])) {
669            $this->processingRules[$propertyPath] = $this->objectManager->get(ProcessingRule::class);
670        }
671        return $this->processingRules[$propertyPath];
672    }
673
674    /**
675     * Get all mapping rules
676     *
677     * @return \TYPO3\CMS\Form\Mvc\ProcessingRule[]
678     * @internal
679     */
680    public function getProcessingRules(): array
681    {
682        return $this->processingRules;
683    }
684
685    /**
686     * @return array
687     * @internal
688     */
689    public function getTypeDefinitions(): array
690    {
691        return $this->typeDefinitions;
692    }
693
694    /**
695     * @return array
696     * @internal
697     */
698    public function getValidatorsDefinition(): array
699    {
700        return $this->validatorsDefinition;
701    }
702
703    /**
704     * @return array
705     * @internal
706     */
707    public function getConditionContextDefinition(): array
708    {
709        return $this->conditionContextDefinition;
710    }
711
712    /**
713     * Get the persistence identifier of the form
714     *
715     * @return string
716     * @internal
717     */
718    public function getPersistenceIdentifier(): string
719    {
720        return $this->persistenceIdentifier;
721    }
722
723    /**
724     * Set the renderer class name
725     *
726     * @param string $rendererClassName
727     */
728    public function setRendererClassName(string $rendererClassName)
729    {
730        $this->rendererClassName = $rendererClassName;
731    }
732
733    /**
734     * Get the classname of the renderer
735     *
736     * @return string
737     */
738    public function getRendererClassName(): string
739    {
740        return $this->rendererClassName;
741    }
742}
743