Building and rendering forms 

This chapter explains how EXT:form renders forms in the frontend and how developers can build forms programmatically or customize the rendering pipeline.

For the complete PHP API of every class mentioned here, see EXT:form API on api.typo3.org.

Template resolution (FluidFormRenderer) 

The FluidFormRenderer resolves Fluid templates, layouts and partials through rendering options defined in the prototype configuration. All options are read from \TYPO3\CMS\Form\Domain\Model\FormDefinition::getRenderingOptions().

templateRootPaths 

Defines one or more paths to Fluid templates. Paths are searched in reverse order (bottom to top); the first match wins.

Only the root form element (type Form) must be a template file. All child elements are resolved as partials.

EXT:my_sitepackage/Configuration/Form/CustomPrototype.yaml
prototypes:
  standard:
    formElementsDefinition:
      Form:
        renderingOptions:
          templateRootPaths:
            10: 'EXT:form/Resources/Private/Frontend/Templates/'
            20: 'EXT:my_sitepackage/Resources/Private/Forms/Frontend/Templates/'
Copied!

With the default type Form the renderer expects a file named Form.html inside the first matching path.

layoutRootPaths 

Defines one or more paths to Fluid layouts, searched in reverse order.

EXT:my_sitepackage/Configuration/Form/CustomPrototype.yaml
prototypes:
  standard:
    formElementsDefinition:
      Form:
        renderingOptions:
          layoutRootPaths:
            10: 'EXT:form/Resources/Private/Frontend/Layouts/'
            20: 'EXT:my_sitepackage/Resources/Private/Forms/Frontend/Layouts/'
Copied!

partialRootPaths 

Defines one or more paths to Fluid partials, searched in reverse order.

Within these paths the renderer looks for a file named after the form element type (e.g. Text.html for a Text element). Use templateName to override this convention.

EXT:my_sitepackage/Configuration/Form/CustomPrototype.yaml
prototypes:
  standard:
    formElementsDefinition:
      Form:
        renderingOptions:
          partialRootPaths:
            10: 'EXT:form/Resources/Private/Frontend/Partials/'
            20: 'EXT:my_sitepackage/Resources/Private/Forms/Frontend/Partials/'
Copied!

templateName 

By default the element type is used as the partial file name (e.g. type TextText.html). Set templateName to use a different file instead:

EXT:my_sitepackage/Configuration/Form/CustomPrototype.yaml
prototypes:
  standard:
    formElementsDefinition:
      Foo:
        renderingOptions:
          templateName: 'Text'
Copied!

The element of type Foo now renders using Text.html.

The render ViewHelper 

Use <formvh:render> in a Fluid template to render a form. The ViewHelper accepts the following arguments:

persistenceIdentifier 

Path to a YAML form definition. This is the most common way to render a form:

EXT:my_sitepackage/Resources/Private/Templates/ContactPage.html
<formvh:render
    persistenceIdentifier="EXT:my_sitepackage/Resources/Private/Forms/Contact.yaml"
/>
Copied!

overrideConfiguration 

A configuration array that is merged on top of the loaded form definition (or passed directly to the factory when no persistenceIdentifier is given). This allows adjusting a form per usage without duplicating the YAML file.

factoryClass 

A fully qualified class name implementing FormFactoryInterface . Defaults to ArrayFormFactory . Set a custom factory to build forms programmatically.

EXT:my_sitepackage/Resources/Private/Templates/ContactPage.html
<formvh:render
    factoryClass="Vendor\MySitePackage\Domain\Factory\CustomFormFactory"
/>
Copied!

prototypeName 

Name of the prototype the factory should use (e.g. standard). If omitted the framework looks for the prototype name inside the form definition; if none is found, standard is used.

Building forms programmatically 

Instead of writing YAML, you can create a form entirely in PHP by implementing a custom FormFactory.

  1. Create a FormFactory

    Extend AbstractFormFactory and implement build(). Use FormDefinition::createPage() to add pages, Page::createElement() to add elements, and FormDefinition::createFinisher() to attach finishers.

    EXT:my_sitepackage/Classes/Domain/Factory/CustomFormFactory.php
    <?php
    
    declare(strict_types=1);
    
    namespace Vendor\MySitePackage\Domain\Factory;
    
    use Psr\Http\Message\ServerRequestInterface;
    use TYPO3\CMS\Core\Utility\GeneralUtility;
    use TYPO3\CMS\Form\Domain\Configuration\ConfigurationService;
    use TYPO3\CMS\Form\Domain\Factory\AbstractFormFactory;
    use TYPO3\CMS\Form\Domain\Model\FormDefinition;
    use TYPO3\CMS\Form\Domain\Model\FormElements\AbstractFormElement;
    
    final class CustomFormFactory extends AbstractFormFactory
    {
        public function build(
            array $configuration,
            ?string $prototypeName = null,
            ?ServerRequestInterface $request = null,
        ): FormDefinition {
            $prototypeName ??= 'standard';
            $configurationService = GeneralUtility::makeInstance(
                ConfigurationService::class,
            );
            $prototypeConfiguration = $configurationService->getPrototypeConfiguration(
                $prototypeName,
            );
    
            $form = GeneralUtility::makeInstance(
                FormDefinition::class,
                'ContactForm',
                $prototypeConfiguration,
            );
            $form->setRenderingOption('controllerAction', 'index');
    
            // Page 1 – personal data
            $page1 = $form->createPage('page1');
    
            /** @var AbstractFormElement $name */
            $name = $page1->createElement('name', 'Text');
            $name->setLabel('Name');
            $name->createValidator('NotEmpty');
    
            /** @var AbstractFormElement $email */
            $email = $page1->createElement('email', 'Text');
            $email->setLabel('Email');
    
            // Page 2 – message
            $page2 = $form->createPage('page2');
    
            /** @var AbstractFormElement $message */
            $message = $page2->createElement('message', 'Textarea');
            $message->setLabel('Message');
            $message->createValidator('StringLength', ['minimum' => 5, 'maximum' => 500]);
    
            // Radio buttons
            /** @var AbstractFormElement $subject */
            $subject = $page2->createElement('subject', 'RadioButton');
            $subject->setProperty('options', [
                'general' => 'General inquiry',
                'support' => 'Support request',
            ]);
            $subject->setLabel('Subject');
    
            // Finisher – send email
            $form->createFinisher('EmailToSender', [
                'subject' => 'Contact form submission',
                'recipients' => [
                    'info@example.com' => 'My Company',
                ],
                'senderAddress' => 'noreply@example.com',
            ]);
    
            $this->triggerFormBuildingFinished($form);
    
            return $form;
        }
    }
    
    Copied!
  2. Render the form

    Reference your factory in a Fluid template:

    EXT:my_sitepackage/Resources/Private/Templates/ContactPage.html
    <formvh:render
        factoryClass="Vendor\MySitePackage\Domain\Factory\CustomFormFactory"
    />
    
    Copied!

Key classes and their responsibilities 

The following table lists the most important classes you work with when building or manipulating forms programmatically. Use your IDE's autocompletion or the API documentation for the full method reference.

Class Purpose
FormDefinition The complete form model. Create pages ( createPage()), attach finishers ( createFinisher()), look up elements ( getElementByIdentifier()), and bind to a request ( bind()).
FormRuntime A bound form instance (created by FormDefinition::bind()). Provides access to the current page, submitted values ( getElementValue()), and the request/response objects. This is the object available inside finishers and event listeners.
Page One page of a multi-step form. Add elements with createElement(), reorder them with moveElementBefore() / moveElementAfter().
Section A grouping element inside a page. Same API as Page for managing child elements.
AbstractFormElement Base class of all concrete elements. Most element types use GenericFormElement ; specialized subclasses include DatePicker and FileUpload . Set properties ( setProperty()), add validators ( createValidator()), define default values ( setDefaultValue()).
ConfigurationService Reads the merged prototype configuration. Call getPrototypeConfiguration('standard') to obtain the full array for a prototype.

Initializing elements at runtime 

Override initializeFormElement() in a custom form element class to populate data (e.g. from a database) when the element is added to the form. At that point the prototype defaults have already been applied; properties from the YAML definition are applied afterwards.

Working with finishers 

Custom finishers extend AbstractFinisher and place their logic in executeInternal(). The base class provides:

parseOption(string $optionName)
Resolves a finisher option, applying form-element variable replacements and TypoScript-style option overrides. Always prefer this over direct array access.

The FinisherContext passed to execute() gives access to:

getFormRuntime()
The FormRuntime for the current submission.
getFormValues()
All submitted values (after validation and property mapping).
getFinisherVariableProvider()

A key/value store to share data between finishers within the same request. The returned FinisherVariableProvider offers:

add(string $finisherIdentifier, string $key, mixed $value)
Store a value under a finisher-specific namespace.
get(string $finisherIdentifier, string $key, mixed $default = null)
Retrieve a previously stored value (returns $default if not set).
exists(string $finisherIdentifier, string $key)
Check whether a value has been stored.
remove(string $finisherIdentifier, string $key)
Remove a stored value.
cancel()
Stops execution of any remaining finishers.

Runtime manipulation 

EXT:form dispatches PSR-14 events at every important step of the rendering lifecycle. Use them to modify the form, redirect page flow, or adjust submitted values – without subclassing framework internals.