275dcec936598e25414945bdd8e81f126bdc788e
[Packages/TYPO3.CMS.git] / typo3 / sysext / form / Classes / Domain / Runtime / FormRuntime.php
1 <?php
2 declare(strict_types = 1);
3 namespace TYPO3\CMS\Form\Domain\Runtime;
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\Core\Utility\Exception\MissingArrayPathException;
22 use TYPO3\CMS\Core\Utility\GeneralUtility;
23 use TYPO3\CMS\Extbase\Error\Result;
24 use TYPO3\CMS\Extbase\Mvc\Controller\Arguments;
25 use TYPO3\CMS\Extbase\Mvc\Controller\ControllerContext;
26 use TYPO3\CMS\Extbase\Mvc\Web\Request;
27 use TYPO3\CMS\Extbase\Mvc\Web\Response;
28 use TYPO3\CMS\Extbase\Mvc\Web\Routing\UriBuilder;
29 use TYPO3\CMS\Extbase\Property\Exception as PropertyException;
30 use TYPO3\CMS\Form\Domain\Exception\RenderingException;
31 use TYPO3\CMS\Form\Domain\Finishers\FinisherContext;
32 use TYPO3\CMS\Form\Domain\Model\FormDefinition;
33 use TYPO3\CMS\Form\Domain\Model\FormElements\FormElementInterface;
34 use TYPO3\CMS\Form\Domain\Model\FormElements\Page;
35 use TYPO3\CMS\Form\Domain\Model\Renderable\RootRenderableInterface;
36 use TYPO3\CMS\Form\Domain\Renderer\RendererInterface;
37 use TYPO3\CMS\Form\Domain\Runtime\Exception\PropertyMappingException;
38 use TYPO3\CMS\Form\Mvc\Validation\EmptyValidator;
39 use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
40
41 /**
42 * This class implements the *runtime logic* of a form, i.e. deciding which
43 * page is shown currently, what the current values of the form are, trigger
44 * validation and property mapping.
45 *
46 * You generally receive an instance of this class by calling {@link \TYPO3\CMS\Form\Domain\Model\FormDefinition::bind}.
47 *
48 * Rendering a Form
49 * ================
50 *
51 * That's easy, just call render() on the FormRuntime:
52 *
53 * /---code php
54 * $form = $formDefinition->bind($request, $response);
55 * $renderedForm = $form->render();
56 * \---
57 *
58 * Accessing Form Values
59 * =====================
60 *
61 * In order to get the values the user has entered into the form, you can access
62 * this object like an array: If a form field with the identifier *firstName*
63 * exists, you can do **$form['firstName']** to retrieve its current value.
64 *
65 * You can also set values in the same way.
66 *
67 * Rendering Internals
68 * ===================
69 *
70 * The FormRuntime asks the FormDefinition about the configured Renderer
71 * which should be used ({@link \TYPO3\CMS\Form\Domain\Model\FormDefinition::getRendererClassName}),
72 * and then trigger render() on this Renderer.
73 *
74 * This makes it possible to declaratively define how a form should be rendered.
75 *
76 * Scope: frontend
77 * **This class is NOT meant to be sub classed by developers.**
78 * @api
79 */
80 class FormRuntime implements RootRenderableInterface, \ArrayAccess
81 {
82 const HONEYPOT_NAME_SESSION_IDENTIFIER = 'tx_form_honeypot_name_';
83
84 /**
85 * @var \TYPO3\CMS\Extbase\Object\ObjectManagerInterface
86 */
87 protected $objectManager;
88
89 /**
90 * @var \TYPO3\CMS\Form\Domain\Model\FormDefinition
91 */
92 protected $formDefinition;
93
94 /**
95 * @var \TYPO3\CMS\Extbase\Mvc\Web\Request
96 */
97 protected $request;
98
99 /**
100 * @var \TYPO3\CMS\Extbase\Mvc\Web\Response
101 */
102 protected $response;
103
104 /**
105 * @var \TYPO3\CMS\Form\Domain\Runtime\FormState
106 */
107 protected $formState;
108
109 /**
110 * The current page is the page which will be displayed to the user
111 * during rendering.
112 *
113 * If $currentPage is NULL, the *last* page has been submitted and
114 * finishing actions need to take place. You should use $this->isAfterLastPage()
115 * instead of explicitely checking for NULL.
116 *
117 * @var \TYPO3\CMS\Form\Domain\Model\FormElements\Page
118 */
119 protected $currentPage = null;
120
121 /**
122 * Reference to the page which has been shown on the last request (i.e.
123 * we have to handle the submitted data from lastDisplayedPage)
124 *
125 * @var \TYPO3\CMS\Form\Domain\Model\FormElements\Page
126 */
127 protected $lastDisplayedPage = null;
128
129 /**
130 * @var \TYPO3\CMS\Extbase\Security\Cryptography\HashService
131 */
132 protected $hashService;
133
134 /**
135 * @param \TYPO3\CMS\Extbase\Security\Cryptography\HashService $hashService
136 * @internal
137 */
138 public function injectHashService(\TYPO3\CMS\Extbase\Security\Cryptography\HashService $hashService)
139 {
140 $this->hashService = $hashService;
141 }
142
143 /**
144 * @param \TYPO3\CMS\Extbase\Object\ObjectManagerInterface $objectManager
145 * @internal
146 */
147 public function injectObjectManager(\TYPO3\CMS\Extbase\Object\ObjectManagerInterface $objectManager)
148 {
149 $this->objectManager = $objectManager;
150 }
151
152 /**
153 * @param FormDefinition $formDefinition
154 * @param Request $request
155 * @param Response $response
156 * @api
157 */
158 public function __construct(FormDefinition $formDefinition, Request $request, Response $response)
159 {
160 $this->formDefinition = $formDefinition;
161 $arguments = $request->getArguments();
162 $this->request = clone $request;
163 $formIdentifier = $this->formDefinition->getIdentifier();
164 if (isset($arguments[$formIdentifier])) {
165 $this->request->setArguments($arguments[$formIdentifier]);
166 }
167
168 $this->response = $response;
169 }
170
171 /**
172 * @internal
173 */
174 public function initializeObject()
175 {
176 $this->initializeFormStateFromRequest();
177 $this->initializeCurrentPageFromRequest();
178 $this->initializeHoneypotFromRequest();
179
180 if (!$this->isFirstRequest() && $this->getRequest()->getMethod() === 'POST') {
181 $this->processSubmittedFormValues();
182 }
183
184 $this->renderHoneypot();
185 }
186
187 /**
188 * Initializes the current state of the form, based on the request
189 */
190 protected function initializeFormStateFromRequest()
191 {
192 $serializedFormStateWithHmac = $this->request->getInternalArgument('__state');
193 if ($serializedFormStateWithHmac === null) {
194 $this->formState = GeneralUtility::makeInstance(FormState::class);
195 } else {
196 $serializedFormState = $this->hashService->validateAndStripHmac($serializedFormStateWithHmac);
197 $this->formState = unserialize(base64_decode($serializedFormState));
198 }
199 }
200
201 /**
202 * Initializes the current page data based on the current request, also modifiable by a hook
203 */
204 protected function initializeCurrentPageFromRequest()
205 {
206 if (!$this->formState->isFormSubmitted()) {
207 $this->currentPage = $this->formDefinition->getPageByIndex(0);
208 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['afterInitializeCurrentPage'] ?? [] as $className) {
209 $hookObj = GeneralUtility::makeInstance($className);
210 if (method_exists($hookObj, 'afterInitializeCurrentPage')) {
211 $this->currentPage = $hookObj->afterInitializeCurrentPage(
212 $this,
213 $this->currentPage,
214 null,
215 $this->request->getArguments()
216 );
217 }
218 }
219 return;
220 }
221 $this->lastDisplayedPage = $this->formDefinition->getPageByIndex($this->formState->getLastDisplayedPageIndex());
222
223 // We know now that lastDisplayedPage is filled
224 $currentPageIndex = (int)$this->request->getInternalArgument('__currentPage');
225 if ($currentPageIndex > $this->lastDisplayedPage->getIndex() + 1) {
226 // We only allow jumps to following pages
227 $currentPageIndex = $this->lastDisplayedPage->getIndex() + 1;
228 }
229
230 // We now know that the user did not try to skip a page
231 if ($currentPageIndex === count($this->formDefinition->getPages())) {
232 // Last Page
233 $this->currentPage = null;
234 } else {
235 $this->currentPage = $this->formDefinition->getPageByIndex($currentPageIndex);
236 }
237
238 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['afterInitializeCurrentPage'] ?? [] as $className) {
239 $hookObj = GeneralUtility::makeInstance($className);
240 if (method_exists($hookObj, 'afterInitializeCurrentPage')) {
241 $this->currentPage = $hookObj->afterInitializeCurrentPage(
242 $this,
243 $this->currentPage,
244 $this->lastDisplayedPage,
245 $this->request->getArguments()
246 );
247 }
248 }
249 }
250
251 /**
252 * Checks if the honey pot is active, and adds a validator if so.
253 */
254 protected function initializeHoneypotFromRequest()
255 {
256 $renderingOptions = $this->formDefinition->getRenderingOptions();
257 if (!isset($renderingOptions['honeypot']['enable']) || $renderingOptions['honeypot']['enable'] === false || TYPO3_MODE === 'BE') {
258 return;
259 }
260
261 ArrayUtility::assertAllArrayKeysAreValid($renderingOptions['honeypot'], ['enable', 'formElementToUse']);
262
263 if (!$this->isFirstRequest()) {
264 $elementsCount = count($this->lastDisplayedPage->getElements());
265 if ($elementsCount === 0) {
266 return;
267 }
268
269 $honeypotNameFromSession = $this->getHoneypotNameFromSession($this->lastDisplayedPage);
270 if ($honeypotNameFromSession) {
271 $honeypotElement = $this->lastDisplayedPage->createElement($honeypotNameFromSession, $renderingOptions['honeypot']['formElementToUse']);
272 $validator = $this->objectManager->get(EmptyValidator::class);
273 $honeypotElement->addValidator($validator);
274 }
275 }
276 }
277
278 /**
279 * Renders a hidden field if the honey pot is active.
280 */
281 protected function renderHoneypot()
282 {
283 $renderingOptions = $this->formDefinition->getRenderingOptions();
284 if (!isset($renderingOptions['honeypot']['enable']) || $renderingOptions['honeypot']['enable'] === false || TYPO3_MODE === 'BE') {
285 return;
286 }
287
288 ArrayUtility::assertAllArrayKeysAreValid($renderingOptions['honeypot'], ['enable', 'formElementToUse']);
289
290 if (!$this->isAfterLastPage()) {
291 $elementsCount = count($this->currentPage->getElements());
292 if ($elementsCount === 0) {
293 return;
294 }
295
296 if (!$this->isFirstRequest()) {
297 $honeypotNameFromSession = $this->getHoneypotNameFromSession($this->lastDisplayedPage);
298 if ($honeypotNameFromSession) {
299 $honeypotElement = $this->formDefinition->getElementByIdentifier($honeypotNameFromSession);
300 if ($honeypotElement instanceof FormElementInterface) {
301 $this->lastDisplayedPage->removeElement($honeypotElement);
302 }
303 }
304 }
305
306 $elementsCount = count($this->currentPage->getElements());
307 $randomElementNumber = mt_rand(0, $elementsCount - 1);
308 $honeypotName = substr(str_shuffle('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'), 0, mt_rand(5, 26));
309
310 $referenceElement = $this->currentPage->getElements()[$randomElementNumber];
311 $honeypotElement = $this->currentPage->createElement($honeypotName, $renderingOptions['honeypot']['formElementToUse']);
312 $validator = $this->objectManager->get(EmptyValidator::class);
313
314 $honeypotElement->addValidator($validator);
315 if (mt_rand(0, 1) === 1) {
316 $this->currentPage->moveElementAfter($honeypotElement, $referenceElement);
317 } else {
318 $this->currentPage->moveElementBefore($honeypotElement, $referenceElement);
319 }
320 $this->setHoneypotNameInSession($this->currentPage, $honeypotName);
321 }
322 }
323
324 /**
325 * @param Page $page
326 * @return string|null
327 */
328 protected function getHoneypotNameFromSession(Page $page)
329 {
330 if ($this->getTypoScriptFrontendController()->loginUser) {
331 $honeypotNameFromSession = $this->getTypoScriptFrontendController()->fe_user->getKey(
332 'user',
333 self::HONEYPOT_NAME_SESSION_IDENTIFIER . $this->getIdentifier() . $page->getIdentifier()
334 );
335 } else {
336 $honeypotNameFromSession = $this->getTypoScriptFrontendController()->fe_user->getKey(
337 'ses',
338 self::HONEYPOT_NAME_SESSION_IDENTIFIER . $this->getIdentifier() . $page->getIdentifier()
339 );
340 }
341 return $honeypotNameFromSession;
342 }
343
344 /**
345 * @param Page $page
346 * @param string $honeypotName
347 */
348 protected function setHoneypotNameInSession(Page $page, string $honeypotName)
349 {
350 if ($this->getTypoScriptFrontendController()->loginUser) {
351 $this->getTypoScriptFrontendController()->fe_user->setKey(
352 'user',
353 self::HONEYPOT_NAME_SESSION_IDENTIFIER . $this->getIdentifier() . $page->getIdentifier(),
354 $honeypotName
355 );
356 } else {
357 $this->getTypoScriptFrontendController()->fe_user->setKey(
358 'ses',
359 self::HONEYPOT_NAME_SESSION_IDENTIFIER . $this->getIdentifier() . $page->getIdentifier(),
360 $honeypotName
361 );
362 }
363 }
364
365 /**
366 * Returns TRUE if the last page of the form has been submitted, otherwise FALSE
367 *
368 * @return bool
369 */
370 protected function isAfterLastPage(): bool
371 {
372 return $this->currentPage === null;
373 }
374
375 /**
376 * Returns TRUE if no previous page is stored in the FormState, otherwise FALSE
377 *
378 * @return bool
379 */
380 protected function isFirstRequest(): bool
381 {
382 return $this->lastDisplayedPage === null;
383 }
384
385 /**
386 * Runs throuh all validations
387 */
388 protected function processSubmittedFormValues()
389 {
390 $result = $this->mapAndValidatePage($this->lastDisplayedPage);
391 if ($result->hasErrors() && !$this->userWentBackToPreviousStep()) {
392 $this->currentPage = $this->lastDisplayedPage;
393 $this->request->setOriginalRequestMappingResults($result);
394 }
395 }
396
397 /**
398 * returns TRUE if the user went back to any previous step in the form.
399 *
400 * @return bool
401 */
402 protected function userWentBackToPreviousStep(): bool
403 {
404 return !$this->isAfterLastPage() && !$this->isFirstRequest() && $this->currentPage->getIndex() < $this->lastDisplayedPage->getIndex();
405 }
406
407 /**
408 * @param Page $page
409 * @return Result
410 * @throws PropertyMappingException
411 */
412 protected function mapAndValidatePage(Page $page): Result
413 {
414 $result = $this->objectManager->get(Result::class);
415 $requestArguments = $this->request->getArguments();
416
417 $propertyPathsForWhichPropertyMappingShouldHappen = [];
418 $registerPropertyPaths = function ($propertyPath) use (&$propertyPathsForWhichPropertyMappingShouldHappen) {
419 $propertyPathParts = explode('.', $propertyPath);
420 $accumulatedPropertyPathParts = [];
421 foreach ($propertyPathParts as $propertyPathPart) {
422 $accumulatedPropertyPathParts[] = $propertyPathPart;
423 $temporaryPropertyPath = implode('.', $accumulatedPropertyPathParts);
424 $propertyPathsForWhichPropertyMappingShouldHappen[$temporaryPropertyPath] = $temporaryPropertyPath;
425 }
426 };
427
428 $value = null;
429
430 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['afterSubmit'] ?? [] as $className) {
431 $hookObj = GeneralUtility::makeInstance($className);
432 if (method_exists($hookObj, 'afterSubmit')) {
433 $value = $hookObj->afterSubmit(
434 $this,
435 $page,
436 $value,
437 $requestArguments
438 );
439 }
440 }
441
442 foreach ($page->getElementsRecursively() as $element) {
443 try {
444 $value = ArrayUtility::getValueByPath($requestArguments, $element->getIdentifier(), '.');
445 } catch (MissingArrayPathException $exception) {
446 $value = null;
447 }
448
449 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['afterSubmit'] ?? [] as $className) {
450 $hookObj = GeneralUtility::makeInstance($className);
451 if (method_exists($hookObj, 'afterSubmit')) {
452 $value = $hookObj->afterSubmit(
453 $this,
454 $element,
455 $value,
456 $requestArguments
457 );
458 }
459 }
460
461 $this->formState->setFormValue($element->getIdentifier(), $value);
462 $registerPropertyPaths($element->getIdentifier());
463 }
464
465 // The more parts the path has, the more early it is processed
466 usort($propertyPathsForWhichPropertyMappingShouldHappen, function ($a, $b) {
467 return substr_count($b, '.') - substr_count($a, '.');
468 });
469
470 $processingRules = $this->formDefinition->getProcessingRules();
471
472 foreach ($propertyPathsForWhichPropertyMappingShouldHappen as $propertyPath) {
473 if (isset($processingRules[$propertyPath])) {
474 $processingRule = $processingRules[$propertyPath];
475 $value = $this->formState->getFormValue($propertyPath);
476 try {
477 $value = $processingRule->process($value);
478 } catch (PropertyException $exception) {
479 throw new PropertyMappingException(
480 'Failed to process FormValue at "' . $propertyPath . '" from "' . gettype($value) . '" to "' . $processingRule->getDataType() . '"',
481 1480024933,
482 $exception
483 );
484 }
485 $result->forProperty($propertyPath)->merge($processingRule->getProcessingMessages());
486 $this->formState->setFormValue($propertyPath, $value);
487 }
488 }
489
490 return $result;
491 }
492
493 /**
494 * Override the current page taken from the request, rendering the page with index $pageIndex instead.
495 *
496 * This is typically not needed in production code, but it is very helpful when displaying
497 * some kind of "preview" of the form.
498 *
499 * @param int $pageIndex
500 * @api
501 */
502 public function overrideCurrentPage(int $pageIndex)
503 {
504 $this->currentPage = $this->formDefinition->getPageByIndex($pageIndex);
505 }
506
507 /**
508 * Render this form.
509 *
510 * @return string|null rendered form
511 * @throws RenderingException
512 * @api
513 */
514 public function render()
515 {
516 if ($this->isAfterLastPage()) {
517 $this->invokeFinishers();
518 return $this->response->getContent();
519 }
520
521 $this->formState->setLastDisplayedPageIndex($this->currentPage->getIndex());
522
523 if ($this->formDefinition->getRendererClassName() === '') {
524 throw new RenderingException(sprintf('The form definition "%s" does not have a rendererClassName set.', $this->formDefinition->getIdentifier()), 1326095912);
525 }
526 $rendererClassName = $this->formDefinition->getRendererClassName();
527 $renderer = $this->objectManager->get($rendererClassName);
528 if (!($renderer instanceof RendererInterface)) {
529 throw new RenderingException(sprintf('The renderer "%s" des not implement RendererInterface', $rendererClassName), 1326096024);
530 }
531
532 $controllerContext = $this->getControllerContext();
533
534 $renderer->setControllerContext($controllerContext);
535 $renderer->setFormRuntime($this);
536 return $renderer->render($this);
537 }
538
539 /**
540 * Executes all finishers of this form
541 */
542 protected function invokeFinishers()
543 {
544 $finisherContext = $this->objectManager->get(
545 FinisherContext::class,
546 $this,
547 $this->getControllerContext()
548 );
549 foreach ($this->formDefinition->getFinishers() as $finisher) {
550 $finisher->execute($finisherContext);
551 if ($finisherContext->isCancelled()) {
552 break;
553 }
554 }
555 }
556
557 /**
558 * @return string The identifier of underlying form
559 * @api
560 */
561 public function getIdentifier(): string
562 {
563 return $this->formDefinition->getIdentifier();
564 }
565
566 /**
567 * Get the request this object is bound to.
568 *
569 * This is mostly relevant inside Finishers, where you f.e. want to redirect
570 * the user to another page.
571 *
572 * @return Request the request this object is bound to
573 * @api
574 */
575 public function getRequest(): Request
576 {
577 return $this->request;
578 }
579
580 /**
581 * Get the response this object is bound to.
582 *
583 * This is mostly relevant inside Finishers, where you f.e. want to set response
584 * headers or output content.
585 *
586 * @return Response the response this object is bound to
587 * @api
588 */
589 public function getResponse(): Response
590 {
591 return $this->response;
592 }
593
594 /**
595 * Returns the currently selected page
596 *
597 * @return Page
598 * @api
599 */
600 public function getCurrentPage(): Page
601 {
602 return $this->currentPage;
603 }
604
605 /**
606 * Returns the previous page of the currently selected one or NULL if there is no previous page
607 *
608 * @return Page|null
609 * @api
610 */
611 public function getPreviousPage()
612 {
613 $previousPageIndex = $this->currentPage->getIndex() - 1;
614 if ($this->formDefinition->hasPageWithIndex($previousPageIndex)) {
615 return $this->formDefinition->getPageByIndex($previousPageIndex);
616 }
617 return null;
618 }
619
620 /**
621 * Returns the next page of the currently selected one or NULL if there is no next page
622 *
623 * @return Page|null
624 * @api
625 */
626 public function getNextPage()
627 {
628 $nextPageIndex = $this->currentPage->getIndex() + 1;
629 if ($this->formDefinition->hasPageWithIndex($nextPageIndex)) {
630 return $this->formDefinition->getPageByIndex($nextPageIndex);
631 }
632 return null;
633 }
634
635 /**
636 * @return ControllerContext
637 */
638 protected function getControllerContext(): ControllerContext
639 {
640 $uriBuilder = $this->objectManager->get(UriBuilder::class);
641 $uriBuilder->setRequest($this->request);
642 $controllerContext = $this->objectManager->get(ControllerContext::class);
643 $controllerContext->setRequest($this->request);
644 $controllerContext->setResponse($this->response);
645 $controllerContext->setArguments($this->objectManager->get(Arguments::class, []));
646 $controllerContext->setUriBuilder($uriBuilder);
647 return $controllerContext;
648 }
649
650 /**
651 * Abstract "type" of this Renderable. Is used during the rendering process
652 * to determine the template file or the View PHP class being used to render
653 * the particular element.
654 *
655 * @return string
656 * @api
657 */
658 public function getType(): string
659 {
660 return $this->formDefinition->getType();
661 }
662
663 /**
664 * @param string $identifier
665 * @return bool
666 * @internal
667 */
668 public function offsetExists($identifier)
669 {
670 if ($this->getElementValue($identifier) !== null) {
671 return true;
672 }
673
674 if (is_callable([$this, 'get' . ucfirst($identifier)])) {
675 return true;
676 }
677 if (is_callable([$this, 'has' . ucfirst($identifier)])) {
678 return true;
679 }
680 if (is_callable([$this, 'is' . ucfirst($identifier)])) {
681 return true;
682 }
683 if (property_exists($this, $identifier)) {
684 $propertyReflection = new \ReflectionProperty($this, $identifier);
685 return $propertyReflection->isPublic();
686 }
687
688 return false;
689 }
690
691 /**
692 * @param string $identifier
693 * @return mixed
694 * @internal
695 */
696 public function offsetGet($identifier)
697 {
698 if ($this->getElementValue($identifier) !== null) {
699 return $this->getElementValue($identifier);
700 }
701 $getterMethodName = 'get' . ucfirst($identifier);
702 if (is_callable([$this, $getterMethodName])) {
703 return $this->{$getterMethodName}();
704 }
705 return null;
706 }
707
708 /**
709 * @param string $identifier
710 * @param mixed $value
711 * @internal
712 */
713 public function offsetSet($identifier, $value)
714 {
715 $this->formState->setFormValue($identifier, $value);
716 }
717
718 /**
719 * @param string $identifier
720 * @internal
721 */
722 public function offsetUnset($identifier)
723 {
724 $this->formState->setFormValue($identifier, null);
725 }
726
727 /**
728 * Returns the value of the specified element
729 *
730 * @param string $identifier
731 * @return mixed
732 * @api
733 */
734 public function getElementValue(string $identifier)
735 {
736 $formValue = $this->formState->getFormValue($identifier);
737 if ($formValue !== null) {
738 return $formValue;
739 }
740 return $this->formDefinition->getElementDefaultValueByIdentifier($identifier);
741 }
742
743 /**
744 * @return array<Page> The Form's pages in the correct order
745 * @api
746 */
747 public function getPages(): array
748 {
749 return $this->formDefinition->getPages();
750 }
751
752 /**
753 * @return FormState
754 * @internal
755 */
756 public function getFormState(): FormState
757 {
758 return $this->formState;
759 }
760
761 /**
762 * Get all rendering options
763 *
764 * @return array associative array of rendering options
765 * @api
766 */
767 public function getRenderingOptions(): array
768 {
769 return $this->formDefinition->getRenderingOptions();
770 }
771
772 /**
773 * Get the renderer class name to be used to display this renderable;
774 * must implement RendererInterface
775 *
776 * @return string the renderer class name
777 * @api
778 */
779 public function getRendererClassName(): string
780 {
781 return $this->formDefinition->getRendererClassName();
782 }
783
784 /**
785 * Get the label which shall be displayed next to the form element
786 *
787 * @return string
788 * @api
789 */
790 public function getLabel(): string
791 {
792 return $this->formDefinition->getLabel();
793 }
794
795 /**
796 * Get the template name of the renderable
797 *
798 * @return string
799 * @api
800 */
801 public function getTemplateName(): string
802 {
803 return $this->formDefinition->getTemplateName();
804 }
805
806 /**
807 * Get the underlying form definition from the runtime
808 *
809 * @return FormDefinition
810 * @api
811 */
812 public function getFormDefinition(): FormDefinition
813 {
814 return $this->formDefinition;
815 }
816
817 /**
818 * @return TypoScriptFrontendController
819 */
820 protected function getTypoScriptFrontendController()
821 {
822 return $GLOBALS['TSFE'];
823 }
824 }