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