[TASK] Use null coalescing operator where possible
[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\Runtime\FormRuntime;
35 use TYPO3\CMS\Form\Exception as FormException;
36 use TYPO3\CMS\Form\Mvc\ProcessingRule;
37
38 /**
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.
42 *
43 * It is *not modified* when the form executes.
44 *
45 * The Anatomy Of A Form
46 * =====================
47 *
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.
51 *
52 * A *Page* consists of multiple *FormElements* ({@link FormElementInterface}, {@link AbstractFormElement}),
53 * which represent the input fields, textareas, checkboxes shown inside the page.
54 *
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.
59 *
60 * Simple Example
61 * --------------
62 *
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.
66 *
67 * /---code php
68 * $formDefinition = $this->objectManager->get(FormDefinition::class, 'myForm');
69 *
70 * $page1 = $this->objectManager->get(Page::class, 'page1');
71 * $formDefinition->addPage($page);
72 *
73 * $element1 = $this->objectManager->get(GenericFormElement::class, 'title', 'Textfield'); # the second argument is the type of the form element
74 * $page1->addElement($element1);
75 * \---
76 *
77 * Creating a Form, Using Abstract Form Element Types
78 * =====================================================
79 *
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**.
86 *
87 * So the simple example from above should be rewritten as follows:
88 *
89 * /---code php
90 * $prototypeConfiguration = []; // We'll talk about this later
91 *
92 * $formDefinition = $this->objectManager->get(FormDefinition::class, 'myForm', $prototypeConfiguration);
93 * $page1 = $formDefinition->createPage('page1');
94 * $element1 = $page1->addElement('title', 'Textfield');
95 * \---
96 *
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**.
99 *
100 * To make the example from above actually work, we need to add some sensible
101 * values to *$prototypeConfiguration*:
102 *
103 * <pre>
104 * $prototypeConfiguration = [
105 * 'formElementsDefinition' => [
106 * 'Page' => [
107 * 'implementationClassName' => 'TYPO3\CMS\Form\Domain\Model\FormElements\Page'
108 * ],
109 * 'Textfield' => [
110 * 'implementationClassName' => 'TYPO3\CMS\Form\Domain\Model\FormElements\GenericFormElement'
111 * ]
112 * ]
113 * ]
114 * </pre>
115 *
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
119 * shows:
120 *
121 * <pre>
122 * $prototypeConfiguration = [
123 * 'formElementsDefinition' => [
124 * 'Page' => [
125 * 'implementationClassName' => 'TYPO3\CMS\Form\Domain\Model\FormElements\Page',
126 * 'label' => 'this is the label of the page if nothing is specified'
127 * ],
128 * 'Textfield' => [
129 * 'implementationClassName' => 'TYPO3\CMS\Form\Domain\Model\FormElements\GenericFormElement',
130 * 'label' = >'Default Label',
131 * 'defaultValue' => 'Default form element value',
132 * 'properties' => [
133 * 'placeholder' => 'Text which is shown if element is empty'
134 * ]
135 * ]
136 * ]
137 * ]
138 * </pre>
139 *
140 * Using Preconfigured $prototypeConfiguration
141 * ---------------------------------
142 *
143 * Often, it is not really useful to manually create the $prototypeConfiguration array.
144 *
145 * Most of it comes pre-configured inside the YAML settings of the extensions,
146 * and the {@link \TYPO3\CMS\Form\Domain\Configuration\ConfigurationService} contains helper methods
147 * which return the ready-to-use *$prototypeConfiguration*.
148 *
149 * Property Mapping and Validation Rules
150 * =====================================
151 *
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:
154 *
155 * - expected Data Types
156 * - Property Mapping Configuration to be used
157 * - Validation Rules which should apply
158 *
159 * Background Info
160 * ---------------
161 * You might wonder why Data Types and Validation Rules are *not attached
162 * to each FormElement itself*.
163 *
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:
166 * <pre>
167 * - person
168 * -- firstName
169 * -- lastName
170 * -- address
171 * --- street
172 * --- city
173 * </pre>
174 *
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.
178 *
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.
182 *
183 * Connecting FormElements to the output data structure
184 * ====================================================
185 *
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.
189 *
190 * Using the above example, if you want to create a FormElement for the *street*,
191 * you should use the identifier *person.address.street*.
192 *
193 * Rendering a FormDefinition
194 * ==========================
195 *
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).
200 *
201 * /---code php
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);
206 *
207 * # now, you can use the $form object to get information about the currently
208 * # entered values into the form, etc.
209 * \---
210 *
211 * Refer to the {@link \TYPO3\CMS\Form\Domain\Runtime\FormRuntime} API doc for further information.
212 *
213 * Scope: frontend
214 * **This class is NOT meant to be sub classed by developers.**
215 */
216 class FormDefinition extends AbstractCompositeRenderable
217 {
218
219 /**
220 * @var \TYPO3\CMS\Extbase\Object\ObjectManagerInterface
221 */
222 protected $objectManager;
223
224 /**
225 * The finishers for this form
226 *
227 * @var \TYPO3\CMS\Form\Domain\Finishers\FinisherInterface[]
228 */
229 protected $finishers = [];
230
231 /**
232 * Property Mapping Rules, indexed by element identifier
233 *
234 * @var \TYPO3\CMS\Form\Mvc\ProcessingRule[]
235 */
236 protected $processingRules = [];
237
238 /**
239 * Contains all elements of the form, indexed by identifier.
240 * Is used as internal cache as we need this really often.
241 *
242 * @var \TYPO3\CMS\Form\Domain\Model\FormElements\FormElementInterface[]
243 */
244 protected $elementsByIdentifier = [];
245
246 /**
247 * Form element default values in the format ['elementIdentifier' => 'default value']
248 *
249 * @var array
250 */
251 protected $elementDefaultValues = [];
252
253 /**
254 * Renderer class name to be used.
255 *
256 * @var string
257 */
258 protected $rendererClassName = '';
259
260 /**
261 * @var array
262 */
263 protected $typeDefinitions;
264
265 /**
266 * @var array
267 */
268 protected $validatorsDefinition;
269
270 /**
271 * @var array
272 */
273 protected $finishersDefinition;
274
275 /**
276 * The persistence identifier of the form
277 *
278 * @var string
279 */
280 protected $persistenceIdentifier = null;
281
282 /**
283 * @param \TYPO3\CMS\Extbase\Object\ObjectManagerInterface $objectManager
284 * @internal
285 */
286 public function injectObjectManager(\TYPO3\CMS\Extbase\Object\ObjectManagerInterface $objectManager)
287 {
288 $this->objectManager = $objectManager;
289 }
290
291 /**
292 * Constructor. Creates a new FormDefinition with the given identifier.
293 *
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
299 * @api
300 */
301 public function __construct(
302 string $identifier,
303 array $prototypeConfiguration = [],
304 string $type = 'Form',
305 string $persistenceIdentifier = null
306 ) {
307 $this->typeDefinitions = $prototypeConfiguration['formElementsDefinition'] ?? [];
308 $this->validatorsDefinition = $prototypeConfiguration['validatorsDefinition'] ?? [];
309 $this->finishersDefinition = $prototypeConfiguration['finishersDefinition'] ?? [];
310
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);
313 }
314
315 $this->identifier = $identifier;
316 $this->type = $type;
317 $this->persistenceIdentifier = $persistenceIdentifier;
318
319 if ($prototypeConfiguration !== []) {
320 $this->initializeFromFormDefaults();
321 }
322 }
323
324 /**
325 * Initialize the form defaults of the current type
326 *
327 * @throws TypeDefinitionNotFoundException
328 * @internal
329 */
330 protected function initializeFromFormDefaults()
331 {
332 if (!isset($this->typeDefinitions[$this->type])) {
333 throw new TypeDefinitionNotFoundException(sprintf('Type "%s" not found. Probably some configuration is missing.', $this->type), 1474905835);
334 }
335 $typeDefinition = $this->typeDefinitions[$this->type];
336 $this->setOptions($typeDefinition);
337 }
338
339 /**
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.
343 *
344 * @param array $options
345 * @internal
346 */
347 public function setOptions(array $options)
348 {
349 if (isset($options['rendererClassName'])) {
350 $this->setRendererClassName($options['rendererClassName']);
351 }
352 if (isset($options['label'])) {
353 $this->setLabel($options['label']);
354 }
355 if (isset($options['renderingOptions'])) {
356 foreach ($options['renderingOptions'] as $key => $value) {
357 if (is_array($value)) {
358 $currentValue = $this->getRenderingOptions()[$key] ?? [];
359 ArrayUtility::mergeRecursiveWithOverrule($currentValue, $value);
360 $this->setRenderingOption($key, $currentValue);
361 } else {
362 $this->setRenderingOption($key, $value);
363 }
364 }
365 }
366 if (isset($options['finishers'])) {
367 foreach ($options['finishers'] as $finisherConfiguration) {
368 $this->createFinisher($finisherConfiguration['identifier'], $finisherConfiguration['options'] ?? []);
369 }
370 }
371
372 ArrayUtility::assertAllArrayKeysAreValid(
373 $options,
374 ['rendererClassName', 'renderingOptions', 'finishers', 'formEditor', 'label']
375 );
376 }
377
378 /**
379 * Create a page with the given $identifier and attach this page to the form.
380 *
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
385 *
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
390 * @api
391 */
392 public function createPage(string $identifier, string $typeName = 'Page'): Page
393 {
394 if (!isset($this->typeDefinitions[$typeName])) {
395 throw new TypeDefinitionNotFoundException(sprintf('Type "%s" not found. Probably some configuration is missing.', $typeName), 1474905953);
396 }
397
398 $typeDefinition = $this->typeDefinitions[$typeName];
399
400 if (!isset($typeDefinition['implementationClassName'])) {
401 throw new TypeDefinitionNotFoundException(sprintf('The "implementationClassName" was not set in type definition "%s".', $typeName), 1477083126);
402 }
403 $implementationClassName = $typeDefinition['implementationClassName'];
404 $page = $this->objectManager->get($implementationClassName, $identifier, $typeName);
405
406 if (isset($typeDefinition['label'])) {
407 $page->setLabel($typeDefinition['label']);
408 }
409
410 if (isset($typeDefinition['renderingOptions'])) {
411 foreach ($typeDefinition['renderingOptions'] as $key => $value) {
412 $page->setRenderingOption($key, $value);
413 }
414 }
415
416 ArrayUtility::assertAllArrayKeysAreValid(
417 $typeDefinition,
418 ['implementationClassName', 'label', 'renderingOptions', 'formEditor']
419 );
420
421 $this->addPage($page);
422 return $page;
423 }
424
425 /**
426 * Add a new page at the end of the form.
427 *
428 * Instead of this method, you should often use {@link createPage} instead.
429 *
430 * @param Page $page
431 * @throws FormDefinitionConsistencyException if Page is already added to a FormDefinition
432 * @see createPage
433 * @api
434 */
435 public function addPage(Page $page)
436 {
437 $this->addRenderable($page);
438 }
439
440 /**
441 * Get the Form's pages
442 *
443 * @return array<Page> The Form's pages in the correct order
444 * @api
445 */
446 public function getPages(): array
447 {
448 return $this->renderables;
449 }
450
451 /**
452 * Check whether a page with the given $index exists
453 *
454 * @param int $index
455 * @return bool TRUE if a page with the given $index exists, otherwise FALSE
456 * @api
457 */
458 public function hasPageWithIndex(int $index): bool
459 {
460 return isset($this->renderables[$index]);
461 }
462
463 /**
464 * Get the page with the passed index. The first page has index zero.
465 *
466 * If page at $index does not exist, an exception is thrown. @see hasPageWithIndex()
467 *
468 * @param int $index
469 * @return Page the page, or NULL if none found.
470 * @throws FormException if the specified index does not exist
471 * @api
472 */
473 public function getPageByIndex(int $index)
474 {
475 if (!$this->hasPageWithIndex($index)) {
476 throw new FormException(sprintf('There is no page with an index of %d', $index), 1329233627);
477 }
478 return $this->renderables[$index];
479 }
480
481 /**
482 * Adds the specified finisher to this form
483 *
484 * @param FinisherInterface $finisher
485 * @api
486 */
487 public function addFinisher(FinisherInterface $finisher)
488 {
489 $this->finishers[] = $finisher;
490 }
491
492 /**
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
497 * @api
498 */
499 public function createFinisher(string $finisherIdentifier, array $options = []): FinisherInterface
500 {
501 if (isset($this->finishersDefinition[$finisherIdentifier]) && is_array($this->finishersDefinition[$finisherIdentifier]) && isset($this->finishersDefinition[$finisherIdentifier]['implementationClassName'])) {
502 $implementationClassName = $this->finishersDefinition[$finisherIdentifier]['implementationClassName'];
503 $defaultOptions = $this->finishersDefinition[$finisherIdentifier]['options'] ?? [];
504 ArrayUtility::mergeRecursiveWithOverrule($defaultOptions, $options);
505
506 $finisher = $this->objectManager->get($implementationClassName, $finisherIdentifier);
507 $finisher->setOptions($defaultOptions);
508 $this->addFinisher($finisher);
509 return $finisher;
510 }
511 throw new FinisherPresetNotFoundException('The finisher preset identified by "' . $finisherIdentifier . '" could not be found, or the implementationClassName was not specified.', 1328709784);
512 }
513
514 /**
515 * Gets all finishers of this form
516 *
517 * @return \TYPO3\CMS\Form\Domain\Finishers\FinisherInterface[]
518 * @api
519 */
520 public function getFinishers(): array
521 {
522 return $this->finishers;
523 }
524
525 /**
526 * Add an element to the ElementsByIdentifier Cache.
527 *
528 * @param RenderableInterface $renderable
529 * @throws DuplicateFormElementException
530 * @internal
531 */
532 public function registerRenderable(RenderableInterface $renderable)
533 {
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);
537 }
538 $this->elementsByIdentifier[$renderable->getIdentifier()] = $renderable;
539 }
540 }
541
542 /**
543 * Remove an element from the ElementsByIdentifier cache
544 *
545 * @param RenderableInterface $renderable
546 * @internal
547 */
548 public function unregisterRenderable(RenderableInterface $renderable)
549 {
550 if ($renderable instanceof FormElementInterface) {
551 unset($this->elementsByIdentifier[$renderable->getIdentifier()]);
552 }
553 }
554
555 /**
556 * Get a Form Element by its identifier
557 *
558 * If identifier does not exist, returns NULL.
559 *
560 * @param string $elementIdentifier
561 * @return FormElementInterface The element with the given $elementIdentifier or NULL if none found
562 * @api
563 */
564 public function getElementByIdentifier(string $elementIdentifier)
565 {
566 return $this->elementsByIdentifier[$elementIdentifier] ?? null;
567 }
568
569 /**
570 * Sets the default value of a form element
571 *
572 * @param string $elementIdentifier identifier of the form element. This supports property paths!
573 * @param mixed $defaultValue
574 * @internal
575 */
576 public function addElementDefaultValue(string $elementIdentifier, $defaultValue)
577 {
578 $this->elementDefaultValues = ArrayUtility::setValueByPath(
579 $this->elementDefaultValues,
580 $elementIdentifier,
581 $defaultValue,
582 '.'
583 );
584 }
585
586 /**
587 * returns the default value of the specified form element
588 * or NULL if no default value was set
589 *
590 * @param string $elementIdentifier identifier of the form element. This supports property paths!
591 * @return mixed The elements default value
592 * @internal
593 */
594 public function getElementDefaultValueByIdentifier(string $elementIdentifier)
595 {
596 return ObjectAccess::getPropertyPath($this->elementDefaultValues, $elementIdentifier);
597 }
598
599 /**
600 * Move $pageToMove before $referencePage
601 *
602 * @param Page $pageToMove
603 * @param Page $referencePage
604 * @api
605 */
606 public function movePageBefore(Page $pageToMove, Page $referencePage)
607 {
608 $this->moveRenderableBefore($pageToMove, $referencePage);
609 }
610
611 /**
612 * Move $pageToMove after $referencePage
613 *
614 * @param Page $pageToMove
615 * @param Page $referencePage
616 * @api
617 */
618 public function movePageAfter(Page $pageToMove, Page $referencePage)
619 {
620 $this->moveRenderableAfter($pageToMove, $referencePage);
621 }
622
623 /**
624 * Remove $pageToRemove from form
625 *
626 * @param Page $pageToRemove
627 * @api
628 */
629 public function removePage(Page $pageToRemove)
630 {
631 $this->removeRenderable($pageToRemove);
632 }
633
634 /**
635 * Bind the current request & response to this form instance, effectively creating
636 * a new "instance" of the Form.
637 *
638 * @param Request $request
639 * @param Response $response
640 * @return FormRuntime
641 * @api
642 */
643 public function bind(Request $request, Response $response): FormRuntime
644 {
645 return $this->objectManager->get(FormRuntime::class, $this, $request, $response);
646 }
647
648 /**
649 * @param string $propertyPath
650 * @return ProcessingRule
651 * @api
652 */
653 public function getProcessingRule(string $propertyPath): ProcessingRule
654 {
655 if (!isset($this->processingRules[$propertyPath])) {
656 $this->processingRules[$propertyPath] = $this->objectManager->get(ProcessingRule::class);
657 }
658 return $this->processingRules[$propertyPath];
659 }
660
661 /**
662 * Get all mapping rules
663 *
664 * @return \TYPO3\CMS\Form\Mvc\ProcessingRule[]
665 * @internal
666 */
667 public function getProcessingRules(): array
668 {
669 return $this->processingRules;
670 }
671
672 /**
673 * @return array
674 * @internal
675 */
676 public function getTypeDefinitions(): array
677 {
678 return $this->typeDefinitions;
679 }
680
681 /**
682 * @return array
683 * @internal
684 */
685 public function getValidatorsDefinition(): array
686 {
687 return $this->validatorsDefinition;
688 }
689
690 /**
691 * Get the persistence identifier of the form
692 *
693 * @return string
694 * @internal
695 */
696 public function getPersistenceIdentifier(): string
697 {
698 return $this->persistenceIdentifier;
699 }
700
701 /**
702 * Set the renderer class name
703 *
704 * @param string $rendererClassName
705 * @api
706 */
707 public function setRendererClassName(string $rendererClassName)
708 {
709 $this->rendererClassName = $rendererClassName;
710 }
711
712 /**
713 * Get the classname of the renderer
714 *
715 * @return string
716 * @api
717 */
718 public function getRendererClassName(): string
719 {
720 return $this->rendererClassName;
721 }
722 }