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