2 declare(strict_types
=1);
3 namespace TYPO3\CMS\Form\Domain\Model
;
6 * This file is part of the TYPO3 CMS project.
8 * It originated from the Neos.Form package (www.neos.io)
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.
14 * For the full copyright and license information, please read the
15 * LICENSE.txt file that was distributed with this source code.
17 * The TYPO3 project - inspiring people to share!
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\Runtime\FormRuntime
;
35 use TYPO3\CMS\Form\Exception
as FormException
;
36 use TYPO3\CMS\Form\Mvc\ProcessingRule
;
39 * This class encapsulates a complete *Form Definition*, with all of its pages,
40 * form elements, validation rules which apply and finishers which should be
41 * executed when the form is completely filled in.
43 * It is *not modified* when the form executes.
45 * The Anatomy Of A Form
46 * =====================
48 * A FormDefinition consists of multiple *Page* ({@link Page}) objects. When a
49 * form is displayed to the user, only one *Page* is visible at any given time,
50 * and there is a navigation to go back and forth between the pages.
52 * A *Page* consists of multiple *FormElements* ({@link FormElementInterface}, {@link AbstractFormElement}),
53 * which represent the input fields, textareas, checkboxes shown inside the page.
55 * *FormDefinition*, *Page* and *FormElement* have *identifier* properties, which
56 * must be unique for each given type (i.e. it is allowed that the FormDefinition and
57 * a FormElement have the *same* identifier, but two FormElements are not allowed to
58 * have the same identifier.
63 * Generally, you can create a FormDefinition manually by just calling the API
64 * methods on it, or you use a *Form Definition Factory* to build the form from
65 * another representation format such as YAML.
68 * $formDefinition = $this->objectManager->get(FormDefinition::class, 'myForm');
70 * $page1 = $this->objectManager->get(Page::class, 'page1');
71 * $formDefinition->addPage($page);
73 * $element1 = $this->objectManager->get(GenericFormElement::class, 'title', 'Textfield'); # the second argument is the type of the form element
74 * $page1->addElement($element1);
77 * Creating a Form, Using Abstract Form Element Types
78 * =====================================================
80 * While you can use the {@link FormDefinition::addPage} or {@link Page::addElement}
81 * methods and create the Page and FormElement objects manually, it is often better
82 * to use the corresponding create* methods ({@link FormDefinition::createPage}
83 * and {@link Page::createElement}), as you pass them an abstract *Form Element Type*
84 * such as *Text* or *Page*, and the system **automatically
85 * resolves the implementation class name and sets default values**.
87 * So the simple example from above should be rewritten as follows:
90 * $prototypeConfiguration = []; // We'll talk about this later
92 * $formDefinition = $this->objectManager->get(FormDefinition::class, 'myForm', $prototypeConfiguration);
93 * $page1 = $formDefinition->createPage('page1');
94 * $element1 = $page1->addElement('title', 'Textfield');
97 * Now, you might wonder how the system knows that the element *Textfield*
98 * is implemented using a GenericFormElement: **This is configured in the $prototypeConfiguration**.
100 * To make the example from above actually work, we need to add some sensible
101 * values to *$prototypeConfiguration*:
104 * $prototypeConfiguration = [
105 * 'formElementsDefinition' => [
107 * 'implementationClassName' => 'TYPO3\CMS\Form\Domain\Model\FormElements\Page'
110 * 'implementationClassName' => 'TYPO3\CMS\Form\Domain\Model\FormElements\GenericFormElement'
116 * For each abstract *Form Element Type* we add some configuration; in the above
117 * case only the *implementation class name*. Still, it is possible to set defaults
118 * for *all* configuration options of such an element, as the following example
122 * $prototypeConfiguration = [
123 * 'formElementsDefinition' => [
125 * 'implementationClassName' => 'TYPO3\CMS\Form\Domain\Model\FormElements\Page',
126 * 'label' => 'this is the label of the page if nothing is specified'
129 * 'implementationClassName' => 'TYPO3\CMS\Form\Domain\Model\FormElements\GenericFormElement',
130 * 'label' = >'Default Label',
131 * 'defaultValue' => 'Default form element value',
133 * 'placeholder' => 'Text which is shown if element is empty'
140 * Using Preconfigured $prototypeConfiguration
141 * ---------------------------------
143 * Often, it is not really useful to manually create the $prototypeConfiguration array.
145 * Most of it comes pre-configured inside the extensions's yaml settings,
146 * and the {@link \TYPO3\CMS\Form\Domain\Configuration\ConfigurationService} contains helper methods
147 * which return the ready-to-use *$prototypeConfiguration*.
149 * Property Mapping and Validation Rules
150 * =====================================
152 * Besides Pages and FormElements, the FormDefinition can contain information
153 * about the *format of the data* which is inputted into the form. This generally means:
155 * - expected Data Types
156 * - Property Mapping Configuration to be used
157 * - Validation Rules which should apply
161 * You might wonder why Data Types and Validation Rules are *not attached
162 * to each FormElement itself*.
164 * If the form should create a *hierarchical output structure* such as a multi-
165 * dimensional array or a PHP object, your expected data structure might look as follows:
175 * Now, let's imagine you want to edit *person.address.street* and *person.address.city*,
176 * but want to validate that the *combination* of *street* and *city* is valid
177 * according to some address database.
179 * In this case, the form elements would be configured to fill *street* and *city*,
180 * but the *validator* needs to be attached to the *compound object* *address*,
181 * as both parts need to be validated together.
183 * Connecting FormElements to the output data structure
184 * ====================================================
186 * The *identifier* of the *FormElement* is most important, as it determines
187 * where in the output structure the value which is entered by the user is placed,
188 * and thus also determines which validation rules need to apply.
190 * Using the above example, if you want to create a FormElement for the *street*,
191 * you should use the identifier *person.address.street*.
193 * Rendering a FormDefinition
194 * ==========================
196 * In order to trigger *rendering* on a FormDefinition,
197 * the current {@link \TYPO3\CMS\Extbase\Mvc\Web\Request} needs to be bound to the FormDefinition,
198 * resulting in a {@link \TYPO3\CMS\Form\Domain\Runtime\FormRuntime} object which contains the *Runtime State* of the form
199 * (such as the currently inserted values).
202 * # $currentRequest and $currentResponse need to be available, f.e. inside a controller you would
203 * # use $this->request and $this->response; inside a ViewHelper you would use $this->controllerContext->getRequest()
204 * # and $this->controllerContext->getResponse()
205 * $form = $formDefinition->bind($currentRequest, $currentResponse);
207 * # now, you can use the $form object to get information about the currently
208 * # entered values into the form, etc.
211 * Refer to the {@link \TYPO3\CMS\Form\Domain\Runtime\FormRuntime} API doc for further information.
214 * **This class is NOT meant to be sub classed by developers.**
216 class FormDefinition
extends AbstractCompositeRenderable
220 * @var \TYPO3\CMS\Extbase\Object\ObjectManagerInterface
222 protected $objectManager;
225 * The finishers for this form
227 * @var \TYPO3\CMS\Form\Domain\Finishers\FinisherInterface[]
229 protected $finishers = [];
232 * Property Mapping Rules, indexed by element identifier
234 * @var \TYPO3\CMS\Form\Mvc\ProcessingRule[]
236 protected $processingRules = [];
239 * Contains all elements of the form, indexed by identifier.
240 * Is used as internal cache as we need this really often.
242 * @var \TYPO3\CMS\Form\Domain\Model\FormElements\FormElementInterface[]
244 protected $elementsByIdentifier = [];
247 * Form element default values in the format ['elementIdentifier' => 'default value']
251 protected $elementDefaultValues = [];
254 * Renderer class name to be used.
258 protected $rendererClassName = '';
263 protected $typeDefinitions;
268 protected $validatorsDefinition;
273 protected $finishersDefinition;
276 * The persistence identifier of the form
280 protected $persistenceIdentifier = null;
283 * @param \TYPO3\CMS\Extbase\Object\ObjectManagerInterface $objectManager
286 public function injectObjectManager(\TYPO3\CMS\Extbase\
Object\ObjectManagerInterface
$objectManager)
288 $this->objectManager
= $objectManager;
292 * Constructor. Creates a new FormDefinition with the given identifier.
294 * @param string $identifier The Form Definition's identifier, must be a non-empty string.
295 * @param array $prototypeConfiguration overrides form defaults of this definition
296 * @param string $type element type of this form
297 * @param string $persistenceIdentifier the persistence identifier of the form
298 * @throws IdentifierNotValidException if the identifier was not valid
301 public function __construct(
303 array $prototypeConfiguration = [],
304 string $type = 'Form',
305 string $persistenceIdentifier = null
307 $this->typeDefinitions
= isset($prototypeConfiguration['formElementsDefinition']) ?
$prototypeConfiguration['formElementsDefinition'] : [];
308 $this->validatorsDefinition
= isset($prototypeConfiguration['validatorsDefinition']) ?
$prototypeConfiguration['validatorsDefinition'] : [];
309 $this->finishersDefinition
= isset($prototypeConfiguration['finishersDefinition']) ?
$prototypeConfiguration['finishersDefinition'] : [];
311 if (!is_string($identifier) ||
strlen($identifier) === 0) {
312 throw new IdentifierNotValidException('The given identifier was not a string or the string was empty.', 1477082503);
315 $this->identifier
= $identifier;
317 $this->persistenceIdentifier
= $persistenceIdentifier;
319 if ($prototypeConfiguration !== []) {
320 $this->initializeFromFormDefaults();
325 * Initialize the form defaults of the current type
327 * @throws TypeDefinitionNotFoundException
330 protected function initializeFromFormDefaults()
332 if (!isset($this->typeDefinitions
[$this->type
])) {
333 throw new TypeDefinitionNotFoundException(sprintf('Type "%s" not found. Probably some configuration is missing.', $this->type
), 1474905835);
335 $typeDefinition = $this->typeDefinitions
[$this->type
];
336 $this->setOptions($typeDefinition);
340 * Set multiple properties of this object at once.
341 * Every property which has a corresponding set* method can be set using
342 * the passed $options array.
344 * @param array $options
347 public function setOptions(array $options)
349 if (isset($options['rendererClassName'])) {
350 $this->setRendererClassName($options['rendererClassName']);
352 if (isset($options['label'])) {
353 $this->setLabel($options['label']);
355 if (isset($options['renderingOptions'])) {
356 foreach ($options['renderingOptions'] as $key => $value) {
357 if (is_array($value)) {
358 $currentValue = isset($this->getRenderingOptions()[$key]) ?
$this->getRenderingOptions()[$key] : [];
359 ArrayUtility
::mergeRecursiveWithOverrule($currentValue, $value);
360 $this->setRenderingOption($key, $currentValue);
362 $this->setRenderingOption($key, $value);
366 if (isset($options['finishers'])) {
367 foreach ($options['finishers'] as $finisherConfiguration) {
368 $this->createFinisher($finisherConfiguration['identifier'], isset($finisherConfiguration['options']) ?
$finisherConfiguration['options'] : []);
372 ArrayUtility
::assertAllArrayKeysAreValid(
374 ['rendererClassName', 'renderingOptions', 'finishers', 'formEditor', 'label']
379 * Create a page with the given $identifier and attach this page to the form.
381 * - Create Page object based on the given $typeName
382 * - set defaults inside the Page object
383 * - attach Page object to this form
384 * - return the newly created Page object
386 * @param string $identifier Identifier of the new page
387 * @param string $typeName Type of the new page
388 * @return Page the newly created page
389 * @throws TypeDefinitionNotFoundException
392 public function createPage(string $identifier, string $typeName = 'Page'): Page
394 if (!isset($this->typeDefinitions
[$typeName])) {
395 throw new TypeDefinitionNotFoundException(sprintf('Type "%s" not found. Probably some configuration is missing.', $typeName), 1474905953);
398 $typeDefinition = $this->typeDefinitions
[$typeName];
400 if (!isset($typeDefinition['implementationClassName'])) {
401 throw new TypeDefinitionNotFoundException(sprintf('The "implementationClassName" was not set in type definition "%s".', $typeName), 1477083126);
403 $implementationClassName = $typeDefinition['implementationClassName'];
404 $page = $this->objectManager
->get($implementationClassName, $identifier, $typeName);
406 if (isset($typeDefinition['label'])) {
407 $page->setLabel($typeDefinition['label']);
410 if (isset($typeDefinition['renderingOptions'])) {
411 foreach ($typeDefinition['renderingOptions'] as $key => $value) {
412 $page->setRenderingOption($key, $value);
416 ArrayUtility
::assertAllArrayKeysAreValid(
418 ['implementationClassName', 'label', 'renderingOptions', 'formEditor']
421 $this->addPage($page);
426 * Add a new page at the end of the form.
428 * Instead of this method, you should often use {@link createPage} instead.
431 * @throws FormDefinitionConsistencyException if Page is already added to a FormDefinition
435 public function addPage(Page
$page)
437 $this->addRenderable($page);
441 * Get the Form's pages
443 * @return array<Page> The Form's pages in the correct order
446 public function getPages(): array
448 return $this->renderables
;
452 * Check whether a page with the given $index exists
455 * @return bool TRUE if a page with the given $index exists, otherwise FALSE
458 public function hasPageWithIndex(int $index): bool
460 return isset($this->renderables
[$index]);
464 * Get the page with the passed index. The first page has index zero.
466 * If page at $index does not exist, an exception is thrown. @see hasPageWithIndex()
469 * @return Page the page, or NULL if none found.
470 * @throws FormException if the specified index does not exist
473 public function getPageByIndex(int $index)
475 if (!$this->hasPageWithIndex($index)) {
476 throw new FormException(sprintf('There is no page with an index of %d', $index), 1329233627);
478 return $this->renderables
[$index];
482 * Adds the specified finisher to this form
484 * @param FinisherInterface $finisher
487 public function addFinisher(FinisherInterface
$finisher)
489 $this->finishers
[] = $finisher;
493 * @param string $finisherIdentifier identifier of the finisher as registered in the current form (for example: "Redirect")
494 * @param array $options options for this finisher in the format ['option1' => 'value1', 'option2' => 'value2', ...]
495 * @return FinisherInterface
496 * @throws FinisherPresetNotFoundException
499 public function createFinisher(string $finisherIdentifier, array $options = []): FinisherInterface
501 if (isset($this->finishersDefinition
[$finisherIdentifier]) && is_array($this->finishersDefinition
[$finisherIdentifier]) && isset($this->finishersDefinition
[$finisherIdentifier]['implementationClassName'])) {
502 $implementationClassName = $this->finishersDefinition
[$finisherIdentifier]['implementationClassName'];
503 $defaultOptions = isset($this->finishersDefinition
[$finisherIdentifier]['options']) ?
$this->finishersDefinition
[$finisherIdentifier]['options'] : [];
504 ArrayUtility
::mergeRecursiveWithOverrule($defaultOptions, $options);
506 $finisher = $this->objectManager
->get($implementationClassName, $finisherIdentifier);
507 $finisher->setOptions($defaultOptions);
508 $this->addFinisher($finisher);
511 throw new FinisherPresetNotFoundException('The finisher preset identified by "' . $finisherIdentifier . '" could not be found, or the implementationClassName was not specified.', 1328709784);
515 * Gets all finishers of this form
517 * @return \TYPO3\CMS\Form\Domain\Finishers\FinisherInterface[]
520 public function getFinishers(): array
522 return $this->finishers
;
526 * Add an element to the ElementsByIdentifier Cache.
528 * @param RenderableInterface $renderable
529 * @throws DuplicateFormElementException
532 public function registerRenderable(RenderableInterface
$renderable)
534 if ($renderable instanceof FormElementInterface
) {
535 if (isset($this->elementsByIdentifier
[$renderable->getIdentifier()])) {
536 throw new DuplicateFormElementException(sprintf('A form element with identifier "%s" is already part of the form.', $renderable->getIdentifier()), 1325663761);
538 $this->elementsByIdentifier
[$renderable->getIdentifier()] = $renderable;
543 * Remove an element from the ElementsByIdentifier cache
545 * @param RenderableInterface $renderable
548 public function unregisterRenderable(RenderableInterface
$renderable)
550 if ($renderable instanceof FormElementInterface
) {
551 unset($this->elementsByIdentifier
[$renderable->getIdentifier()]);
556 * Get a Form Element by its identifier
558 * If identifier does not exist, returns NULL.
560 * @param string $elementIdentifier
561 * @return FormElementInterface The element with the given $elementIdentifier or NULL if none found
564 public function getElementByIdentifier(string $elementIdentifier)
566 return isset($this->elementsByIdentifier
[$elementIdentifier]) ?
$this->elementsByIdentifier
[$elementIdentifier] : null;
570 * Sets the default value of a form element
572 * @param string $elementIdentifier identifier of the form element. This supports property paths!
573 * @param mixed $defaultValue
576 public function addElementDefaultValue(string $elementIdentifier, $defaultValue)
578 $this->elementDefaultValues
= ArrayUtility
::setValueByPath(
579 $this->elementDefaultValues
,
587 * returns the default value of the specified form element
588 * or NULL if no default value was set
590 * @param string $elementIdentifier identifier of the form element. This supports property paths!
591 * @return mixed The elements default value
594 public function getElementDefaultValueByIdentifier(string $elementIdentifier)
596 return ObjectAccess
::getPropertyPath($this->elementDefaultValues
, $elementIdentifier);
600 * Move $pageToMove before $referencePage
602 * @param Page $pageToMove
603 * @param Page $referencePage
606 public function movePageBefore(Page
$pageToMove, Page
$referencePage)
608 $this->moveRenderableBefore($pageToMove, $referencePage);
612 * Move $pageToMove after $referencePage
614 * @param Page $pageToMove
615 * @param Page $referencePage
618 public function movePageAfter(Page
$pageToMove, Page
$referencePage)
620 $this->moveRenderableAfter($pageToMove, $referencePage);
624 * Remove $pageToRemove from form
626 * @param Page $pageToRemove
629 public function removePage(Page
$pageToRemove)
631 $this->removeRenderable($pageToRemove);
635 * Bind the current request & response to this form instance, effectively creating
636 * a new "instance" of the Form.
638 * @param Request $request
639 * @param Response $response
640 * @return FormRuntime
643 public function bind(Request
$request, Response
$response): FormRuntime
645 return $this->objectManager
->get(FormRuntime
::class, $this, $request, $response);
649 * @param string $propertyPath
650 * @return ProcessingRule
653 public function getProcessingRule(string $propertyPath): ProcessingRule
655 if (!isset($this->processingRules
[$propertyPath])) {
656 $this->processingRules
[$propertyPath] = $this->objectManager
->get(ProcessingRule
::class);
658 return $this->processingRules
[$propertyPath];
662 * Get all mapping rules
664 * @return \TYPO3\CMS\Form\Mvc\ProcessingRule[]
667 public function getProcessingRules(): array
669 return $this->processingRules
;
676 public function getTypeDefinitions(): array
678 return $this->typeDefinitions
;
685 public function getValidatorsDefinition(): array
687 return $this->validatorsDefinition
;
691 * Get the persistence identifier of the form
696 public function getPersistenceIdentifier(): string
698 return $this->persistenceIdentifier
;
702 * Set the renderer class name
704 * @param string $rendererClassName
707 public function setRendererClassName(string $rendererClassName)
709 $this->rendererClassName
= $rendererClassName;
713 * Get the classname of the renderer
718 public function getRendererClassName(): string
720 return $this->rendererClassName
;