[TASK] EXT:form - add onSubmit callback for pages
[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\GeneralUtility;
22 use TYPO3\CMS\Extbase\Error\Result;
23 use TYPO3\CMS\Extbase\Mvc\Controller\Arguments;
24 use TYPO3\CMS\Extbase\Mvc\Controller\ControllerContext;
25 use TYPO3\CMS\Extbase\Mvc\Web\Request;
26 use TYPO3\CMS\Extbase\Mvc\Web\Response;
27 use TYPO3\CMS\Extbase\Mvc\Web\Routing\UriBuilder;
28 use TYPO3\CMS\Extbase\Property\Exception as PropertyException;
29 use TYPO3\CMS\Extbase\Reflection\PropertyReflection;
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 element.
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 * @return void
137 * @internal
138 */
139 public function injectHashService(\TYPO3\CMS\Extbase\Security\Cryptography\HashService $hashService)
140 {
141 $this->hashService = $hashService;
142 }
143
144 /**
145 * @param \TYPO3\CMS\Extbase\Object\ObjectManagerInterface $objectManager
146 * @internal
147 */
148 public function injectObjectManager(\TYPO3\CMS\Extbase\Object\ObjectManagerInterface $objectManager)
149 {
150 $this->objectManager = $objectManager;
151 }
152
153 /**
154 * @param FormDefinition $formDefinition
155 * @param Request $request
156 * @param Response $response
157 * @api
158 */
159 public function __construct(FormDefinition $formDefinition, Request $request, Response $response)
160 {
161 $this->formDefinition = $formDefinition;
162 $arguments = $request->getArguments();
163 $this->request = clone $request;
164 $formIdentifier = $this->formDefinition->getIdentifier();
165 if (isset($arguments[$formIdentifier])) {
166 $this->request->setArguments($arguments[$formIdentifier]);
167 }
168
169 $this->response = $response;
170 }
171
172 /**
173 * @return void
174 * @internal
175 */
176 public function initializeObject()
177 {
178 $this->initializeFormStateFromRequest();
179 $this->initializeCurrentPageFromRequest();
180 $this->initializeHoneypotFromRequest();
181
182 if (!$this->isFirstRequest() && $this->getRequest()->getMethod() === 'POST') {
183 $this->processSubmittedFormValues();
184 }
185
186 $this->renderHoneypot();
187 }
188
189 /**
190 * @return void
191 */
192 protected function initializeFormStateFromRequest()
193 {
194 $serializedFormStateWithHmac = $this->request->getInternalArgument('__state');
195 if ($serializedFormStateWithHmac === null) {
196 $this->formState = GeneralUtility::makeInstance(FormState::class);
197 } else {
198 $serializedFormState = $this->hashService->validateAndStripHmac($serializedFormStateWithHmac);
199 $this->formState = unserialize(base64_decode($serializedFormState));
200 }
201 }
202
203 /**
204 * @return void
205 */
206 protected function initializeCurrentPageFromRequest()
207 {
208 if (!$this->formState->isFormSubmitted()) {
209 $this->currentPage = $this->formDefinition->getPageByIndex(0);
210 return;
211 }
212 $this->lastDisplayedPage = $this->formDefinition->getPageByIndex($this->formState->getLastDisplayedPageIndex());
213
214 // We know now that lastDisplayedPage is filled
215 $currentPageIndex = (int)$this->request->getInternalArgument('__currentPage');
216 if ($currentPageIndex > $this->lastDisplayedPage->getIndex() + 1) {
217 // We only allow jumps to following pages
218 $currentPageIndex = $this->lastDisplayedPage->getIndex() + 1;
219 }
220
221 // We now know that the user did not try to skip a page
222 if ($currentPageIndex === count($this->formDefinition->getPages())) {
223 // Last Page
224 $this->currentPage = null;
225 } else {
226 $this->currentPage = $this->formDefinition->getPageByIndex($currentPageIndex);
227 }
228 }
229
230 /**
231 * @return void
232 */
233 protected function initializeHoneypotFromRequest()
234 {
235 $renderingOptions = $this->formDefinition->getRenderingOptions();
236 if (!isset($renderingOptions['honeypot']['enable']) || $renderingOptions['honeypot']['enable'] === false || TYPO3_MODE === 'BE') {
237 return;
238 }
239
240 ArrayUtility::assertAllArrayKeysAreValid($renderingOptions['honeypot'], ['enable', 'formElementToUse']);
241
242 if (!$this->isFirstRequest()) {
243 $elementsCount = count($this->lastDisplayedPage->getElements());
244 if ($elementsCount === 0) {
245 return;
246 }
247
248 $honeypotNameFromSession = $this->getHoneypotNameFromSession($this->lastDisplayedPage);
249 if ($honeypotNameFromSession) {
250 $honeypotElement = $this->lastDisplayedPage->createElement($honeypotNameFromSession, $renderingOptions['honeypot']['formElementToUse']);
251 $validator = $this->objectManager->get(EmptyValidator::class);
252 $honeypotElement->addValidator($validator);
253 }
254 }
255 }
256
257 /**
258 * @return void
259 */
260 protected function renderHoneypot()
261 {
262 $renderingOptions = $this->formDefinition->getRenderingOptions();
263 if (!isset($renderingOptions['honeypot']['enable']) || $renderingOptions['honeypot']['enable'] === false || TYPO3_MODE === 'BE') {
264 return;
265 }
266
267 ArrayUtility::assertAllArrayKeysAreValid($renderingOptions['honeypot'], ['enable', 'formElementToUse']);
268
269 if (!$this->isAfterLastPage()) {
270 $elementsCount = count($this->currentPage->getElements());
271 if ($elementsCount === 0) {
272 return;
273 }
274
275 if (!$this->isFirstRequest()) {
276 $honeypotNameFromSession = $this->getHoneypotNameFromSession($this->lastDisplayedPage);
277 if ($honeypotNameFromSession) {
278 $honeypotElement = $this->formDefinition->getElementByIdentifier($honeypotNameFromSession);
279 if ($honeypotElement instanceof FormElementInterface) {
280 $this->lastDisplayedPage->removeElement($honeypotElement);
281 }
282 }
283 }
284
285 $elementsCount = count($this->currentPage->getElements());
286 $randomElementNumber = mt_rand(0, ($elementsCount - 1));
287 $honeypotName = substr(str_shuffle('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'), 0, mt_rand(5, 26));
288
289 $referenceElement = $this->currentPage->getElements()[$randomElementNumber];
290 $honeypotElement = $this->currentPage->createElement($honeypotName, $renderingOptions['honeypot']['formElementToUse']);
291 $validator = $this->objectManager->get(EmptyValidator::class);
292
293 $honeypotElement->addValidator($validator);
294 if (mt_rand(0, 1) === 1) {
295 $this->currentPage->moveElementAfter($honeypotElement, $referenceElement);
296 } else {
297 $this->currentPage->moveElementBefore($honeypotElement, $referenceElement);
298 }
299 $this->setHoneypotNameInSession($this->currentPage, $honeypotName);
300 }
301 }
302
303 /**
304 * @param Page $page
305 * return null|string
306 */
307 protected function getHoneypotNameFromSession(Page $page)
308 {
309 if ($this->getTypoScriptFrontendController()->loginUser) {
310 $honeypotNameFromSession = $this->getTypoScriptFrontendController()->fe_user->getKey(
311 'user',
312 self::HONEYPOT_NAME_SESSION_IDENTIFIER . $this->getIdentifier() . $page->getIdentifier()
313 );
314 } else {
315 $honeypotNameFromSession = $this->getTypoScriptFrontendController()->fe_user->getKey(
316 'ses',
317 self::HONEYPOT_NAME_SESSION_IDENTIFIER . $this->getIdentifier() . $page->getIdentifier()
318 );
319 }
320 return $honeypotNameFromSession;
321 }
322
323 /**
324 * @param Page $page
325 * @param string $honeypotName
326 * @return void
327 */
328 protected function setHoneypotNameInSession(Page $page, string $honeypotName)
329 {
330 if ($this->getTypoScriptFrontendController()->loginUser) {
331 $this->getTypoScriptFrontendController()->fe_user->setKey(
332 'user',
333 self::HONEYPOT_NAME_SESSION_IDENTIFIER . $this->getIdentifier() . $page->getIdentifier(),
334 $honeypotName
335 );
336 } else {
337 $this->getTypoScriptFrontendController()->fe_user->setKey(
338 'ses',
339 self::HONEYPOT_NAME_SESSION_IDENTIFIER . $this->getIdentifier() . $page->getIdentifier(),
340 $honeypotName
341 );
342 }
343 }
344
345 /**
346 * Returns TRUE if the last page of the form has been submitted, otherwise FALSE
347 *
348 * @return bool
349 */
350 protected function isAfterLastPage(): bool
351 {
352 return $this->currentPage === null;
353 }
354
355 /**
356 * Returns TRUE if no previous page is stored in the FormState, otherwise FALSE
357 *
358 * @return bool
359 */
360 protected function isFirstRequest(): bool
361 {
362 return $this->lastDisplayedPage === null;
363 }
364
365 /**
366 * @return void
367 */
368 protected function processSubmittedFormValues()
369 {
370 $result = $this->mapAndValidatePage($this->lastDisplayedPage);
371 if ($result->hasErrors() && !$this->userWentBackToPreviousStep()) {
372 $this->currentPage = $this->lastDisplayedPage;
373 $this->request->setOriginalRequestMappingResults($result);
374 }
375 }
376
377 /**
378 * returns TRUE if the user went back to any previous step in the form.
379 *
380 * @return bool
381 */
382 protected function userWentBackToPreviousStep(): bool
383 {
384 return !$this->isAfterLastPage() && !$this->isFirstRequest() && $this->currentPage->getIndex() < $this->lastDisplayedPage->getIndex();
385 }
386
387 /**
388 * @param Page $page
389 * @return Result
390 * @throws PropertyMappingException
391 */
392 protected function mapAndValidatePage(Page $page): Result
393 {
394 $result = $this->objectManager->get(Result::class);
395 $requestArguments = $this->request->getArguments();
396
397 $propertyPathsForWhichPropertyMappingShouldHappen = [];
398 $registerPropertyPaths = function ($propertyPath) use (&$propertyPathsForWhichPropertyMappingShouldHappen) {
399 $propertyPathParts = explode('.', $propertyPath);
400 $accumulatedPropertyPathParts = [];
401 foreach ($propertyPathParts as $propertyPathPart) {
402 $accumulatedPropertyPathParts[] = $propertyPathPart;
403 $temporaryPropertyPath = implode('.', $accumulatedPropertyPathParts);
404 $propertyPathsForWhichPropertyMappingShouldHappen[$temporaryPropertyPath] = $temporaryPropertyPath;
405 }
406 };
407
408 $value = null;
409 $page->onSubmit($this, $value, $requestArguments);
410 foreach ($page->getElementsRecursively() as $element) {
411 try {
412 $value = ArrayUtility::getValueByPath($requestArguments, $element->getIdentifier(), '.');
413 } catch (\RuntimeException $exception) {
414 $value = null;
415 }
416 $element->onSubmit($this, $value, $requestArguments);
417
418 $this->formState->setFormValue($element->getIdentifier(), $value);
419 $registerPropertyPaths($element->getIdentifier());
420 }
421
422 // The more parts the path has, the more early it is processed
423 usort($propertyPathsForWhichPropertyMappingShouldHappen, function ($a, $b) {
424 return substr_count($b, '.') - substr_count($a, '.');
425 });
426
427 $processingRules = $this->formDefinition->getProcessingRules();
428
429 foreach ($propertyPathsForWhichPropertyMappingShouldHappen as $propertyPath) {
430 if (isset($processingRules[$propertyPath])) {
431 $processingRule = $processingRules[$propertyPath];
432 $value = $this->formState->getFormValue($propertyPath);
433 try {
434 $value = $processingRule->process($value);
435 } catch (PropertyException $exception) {
436 throw new PropertyMappingException(
437 'Failed to process FormValue at "' . $propertyPath . '" from "' . gettype($value) . '" to "' . $processingRule->getDataType() . '"',
438 1480024933,
439 $exception
440 );
441 }
442 $result->forProperty($propertyPath)->merge($processingRule->getProcessingMessages());
443 $this->formState->setFormValue($propertyPath, $value);
444 }
445 }
446
447 return $result;
448 }
449
450 /**
451 * Override the current page taken from the request, rendering the page with index $pageIndex instead.
452 *
453 * This is typically not needed in production code, but it is very helpful when displaying
454 * some kind of "preview" of the form.
455 *
456 * @param int $pageIndex
457 * @return void
458 * @api
459 */
460 public function overrideCurrentPage(int $pageIndex)
461 {
462 $this->currentPage = $this->formDefinition->getPageByIndex($pageIndex);
463 }
464
465 /**
466 * Render this form.
467 *
468 * @return null|string rendered form
469 * @throws RenderingException
470 * @api
471 */
472 public function render()
473 {
474 if ($this->isAfterLastPage()) {
475 $this->invokeFinishers();
476 return $this->response->getContent();
477 }
478
479 $this->formState->setLastDisplayedPageIndex($this->currentPage->getIndex());
480
481 if ($this->formDefinition->getRendererClassName() === null) {
482 throw new RenderingException(sprintf('The form definition "%s" does not have a rendererClassName set.', $this->formDefinition->getIdentifier()), 1326095912);
483 }
484 $rendererClassName = $this->formDefinition->getRendererClassName();
485 $renderer = $this->objectManager->get($rendererClassName);
486 if (!($renderer instanceof RendererInterface)) {
487 throw new RenderingException(sprintf('The renderer "%s" des not implement RendererInterface', $rendererClassName), 1326096024);
488 }
489
490 $controllerContext = $this->getControllerContext();
491
492 $renderer->setControllerContext($controllerContext);
493 $renderer->setFormRuntime($this);
494 return $renderer->render($this);
495 }
496
497 /**
498 * Executes all finishers of this form
499 *
500 * @return void
501 */
502 protected function invokeFinishers()
503 {
504 $finisherContext = $this->objectManager->get(FinisherContext::class,
505 $this,
506 $this->getControllerContext()
507 );
508 foreach ($this->formDefinition->getFinishers() as $finisher) {
509 $finisher->execute($finisherContext);
510 if ($finisherContext->isCancelled()) {
511 break;
512 }
513 }
514 }
515
516 /**
517 * @return string The identifier of underlying form
518 * @api
519 */
520 public function getIdentifier(): string
521 {
522 return $this->formDefinition->getIdentifier();
523 }
524
525 /**
526 * Get the request this object is bound to.
527 *
528 * This is mostly relevant inside Finishers, where you f.e. want to redirect
529 * the user to another page.
530 *
531 * @return Request the request this object is bound to
532 * @api
533 */
534 public function getRequest(): Request
535 {
536 return $this->request;
537 }
538
539 /**
540 * Get the response this object is bound to.
541 *
542 * This is mostly relevant inside Finishers, where you f.e. want to set response
543 * headers or output content.
544 *
545 * @return Response the response this object is bound to
546 * @api
547 */
548 public function getResponse(): Response
549 {
550 return $this->response;
551 }
552
553 /**
554 * Returns the currently selected page
555 *
556 * @return Page
557 * @api
558 */
559 public function getCurrentPage(): Page
560 {
561 return $this->currentPage;
562 }
563
564 /**
565 * Returns the previous page of the currently selected one or NULL if there is no previous page
566 *
567 * @return null|Page
568 * @api
569 */
570 public function getPreviousPage()
571 {
572 $previousPageIndex = $this->currentPage->getIndex() - 1;
573 if ($this->formDefinition->hasPageWithIndex($previousPageIndex)) {
574 return $this->formDefinition->getPageByIndex($previousPageIndex);
575 }
576 return null;
577 }
578
579 /**
580 * Returns the next page of the currently selected one or NULL if there is no next page
581 *
582 * @return null|Page
583 * @api
584 */
585 public function getNextPage()
586 {
587 $nextPageIndex = $this->currentPage->getIndex() + 1;
588 if ($this->formDefinition->hasPageWithIndex($nextPageIndex)) {
589 return $this->formDefinition->getPageByIndex($nextPageIndex);
590 }
591 return null;
592 }
593
594 /**
595 * @return ControllerContext
596 */
597 protected function getControllerContext(): ControllerContext
598 {
599 $uriBuilder = $this->objectManager->get(UriBuilder::class);
600 $uriBuilder->setRequest($this->request);
601 $controllerContext = $this->objectManager->get(ControllerContext::class);
602 $controllerContext->setRequest($this->request);
603 $controllerContext->setResponse($this->response);
604 $controllerContext->setArguments($this->objectManager->get(Arguments::class, []));
605 $controllerContext->setUriBuilder($uriBuilder);
606 return $controllerContext;
607 }
608
609 /**
610 * Abstract "type" of this Renderable. Is used during the rendering process
611 * to determine the template file or the View PHP class being used to render
612 * the particular element.
613 *
614 * @return string
615 * @api
616 */
617 public function getType(): string
618 {
619 return $this->formDefinition->getType();
620 }
621
622 /**
623 * @param string $identifier
624 * @return bool
625 * @internal
626 */
627 public function offsetExists($identifier)
628 {
629 if ($this->getElementValue($identifier) !== null) {
630 return true;
631 }
632
633 if (is_callable([$this, 'get' . ucfirst($identifier)])) {
634 return true;
635 }
636 if (is_callable([$this, 'has' . ucfirst($identifier)])) {
637 return true;
638 }
639 if (is_callable([$this, 'is' . ucfirst($identifier)])) {
640 return true;
641 }
642 if (property_exists($this, $identifier)) {
643 $propertyReflection = new PropertyReflection($this, $identifier);
644 return $propertyReflection->isPublic();
645 }
646
647 return false;
648 }
649
650 /**
651 * @param string $identifier
652 * @return mixed
653 * @internal
654 */
655 public function offsetGet($identifier)
656 {
657 if ($this->getElementValue($identifier) !== null) {
658 return $this->getElementValue($identifier);
659 }
660 $getterMethodName = 'get' . ucfirst($identifier);
661 if (is_callable([$this, $getterMethodName])) {
662 return $this->{$getterMethodName}();
663 }
664 return null;
665 }
666
667 /**
668 * @param string $identifier
669 * @param mixed $value
670 * @return void
671 * @internal
672 */
673 public function offsetSet($identifier, $value)
674 {
675 $this->formState->setFormValue($identifier, $value);
676 }
677
678 /**
679 * @param string $identifier
680 * @return void
681 * @internal
682 */
683 public function offsetUnset($identifier)
684 {
685 $this->formState->setFormValue($identifier, null);
686 }
687
688 /**
689 * Returns the value of the specified element
690 *
691 * @param string $identifier
692 * @return mixed
693 * @api
694 */
695 public function getElementValue(string $identifier)
696 {
697 $formValue = $this->formState->getFormValue($identifier);
698 if ($formValue !== null) {
699 return $formValue;
700 }
701 return $this->formDefinition->getElementDefaultValueByIdentifier($identifier);
702 }
703
704 /**
705 * @return array<Page> The Form's pages in the correct order
706 * @api
707 */
708 public function getPages(): array
709 {
710 return $this->formDefinition->getPages();
711 }
712
713 /**
714 * @return FormState
715 * @internal
716 */
717 public function getFormState(): FormState
718 {
719 return $this->formState;
720 }
721
722 /**
723 * Get all rendering options
724 *
725 * @return array associative array of rendering options
726 * @api
727 */
728 public function getRenderingOptions(): array
729 {
730 return $this->formDefinition->getRenderingOptions();
731 }
732
733 /**
734 * Get the renderer class name to be used to display this renderable;
735 * must implement RendererInterface
736 *
737 * @return string the renderer class name
738 * @api
739 */
740 public function getRendererClassName(): string
741 {
742 return $this->formDefinition->getRendererClassName();
743 }
744
745 /**
746 * Get the label which shall be displayed next to the form element
747 *
748 * @return string
749 * @api
750 */
751 public function getLabel(): string
752 {
753 return $this->formDefinition->getLabel();
754 }
755
756 /**
757 * Get the underlying form definition from the runtime
758 *
759 * @return FormDefinition
760 * @api
761 */
762 public function getFormDefinition(): FormDefinition
763 {
764 return $this->formDefinition;
765 }
766
767 /**
768 * This is a callback that is invoked by the Renderer before the corresponding element is rendered.
769 * Use this to access previously submitted values and/or modify the $formRuntime before an element
770 * is outputted to the browser.
771 *
772 * @param FormRuntime $formRuntime
773 * @return void
774 * @api
775 */
776 public function beforeRendering(FormRuntime $formRuntime)
777 {
778 }
779
780 /**
781 * @return TypoScriptFrontendController
782 */
783 protected function getTypoScriptFrontendController()
784 {
785 return $GLOBALS['TSFE'];
786 }
787 }