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