Custom finisher 

Write a custom finisher 

To make your finisher configurable by users in the backend form editor, see here.

Add a new finisher to the form configuration prototype by defining a finishersDefinition. Set the implementationClassName property to your new implementation class.

EXT:my_site_package/Configuration/Form/CustomFormSetup.yaml
prototypes:
  standard:
    finishersDefinition:
      CustomFinisher:
        implementationClassName: 'MyVendor\MySitePackage\Domain\Finishers\CustomFinisher'

Copied!

Register your custom form definition.

Add options to your finisher with the options property. Options are default values which can be overridden in the form definition.

Define default values 

EXT:my_site_package/Configuration/Form/CustomFormSetup.yaml
prototypes:
  standard:
    finishersDefinition:
      CustomFinisher:
        implementationClassName: 'MyVendor\MySitePackage\Domain\Finishers\CustomFinisher'
        options:
          yourCustomOption: 'Ralf'
Copied!

Override options using the form definition 

public/fileadmin/forms/my_form.yaml
identifier: sample-form
label: 'Simple Contact Form'
prototype: standard
type: Form

finishers:
  -
    identifier: CustomFinisher
    options:
      yourCustomOption: 'Björn'

renderables:
  # ...
Copied!

A finisher must implement FinisherInterface and should extend AbstractFinisher . In doing so, in the logic of the finisher the method executeInternal() will be called first.

Accessing finisher options 

If your finisher class extends AbstractFinisher , you can access the option values in the finisher using method parseOption():

$yourCustomOption = $this->parseOption('yourCustomOption');
Copied!

parseOption() looks for 'yourCustomOption' in your form definition.

EXT:my_site_package/Classes/Domain/Finishers/CustomFinisher.yaml
prototypes:
  standard:
    finishersDefinition:
      CustomFinisher:
        implementationClassName: 'MyVendor\MySitePackage\Domain\Finishers\CustomFinisher'
        options:
          yourCustomOption: 'Ralf'
Copied!

If it can't find it, parseOption() checks

  1. for a default value in the prototype configuration,
  2. for $defaultOptions inside your finisher class:

If it doesn't find anything, parseOption() returns null.

If it finds the option, the process checks whether the option value will access FormRuntime values. If the FormRuntime returns a positive result, it is checked whether the option value can access values of preceding finishers. At the end, it translates the finisher options.

Accessing form runtime values 

You can populate finisher options with submitted form values using the parseOption() method. You can access values of the FormRuntime and therefore values in every form element by encapsulating option values with {}. Below, if there is a form element with the identifier 'subject', you can access the value in the finisher configuration:

public/fileadmin/forms/my_form.yaml
identifier: simple-contact-form
label: 'Simple Contact Form'
prototype: standard
type: Form

finishers:
  -
    identifier: Custom
    options:
      yourCustomOption: '{subject}'

renderables:
  -
    identifier: subject
    label: 'Subject'
    type: Text
Copied!
// $yourCustomOption contains the value of the form element with the
// identifier 'subject'
$yourCustomOption = $this->parseOption('yourCustomOption');
Copied!

You can use {__currentTimestamp} as an option value to return the current UNIX timestamp.

Display values and submitted values 

Changed in version 14.3.7 Important: #106903 - Form element value resolution in finishers

Before, the built-in finishers used the submitted value, for example the option key, in all options.

parseOption() replaces {<elementIdentifier>} with the submitted value of the form element. For a select, radio button, multi checkbox, or country select element, this value is the option key, for example mr.

parseOptionAsDisplayValue() replaces the placeholder with the display value instead. The form element provides this value, for example the translated option label Mister. Use it for options that are read by a human. Never use it for options that end up in a query, a stored record, or a URL.

// "Request from Mister" instead of "Request from mr"
$subject = $this->parseOptionAsDisplayValue('subject');
Copied!

The built-in finishers use display values for the following options:

  • EmailFinisher: subject, title, message, and senderName
  • ConfirmationFinisher: message
  • FlashMessageFinisher: messageBody and messageTitle

All other options of the built-in finishers keep the submitted value. This includes the email addresses, the redirect targets, and all options of the SaveToDatabaseFinisher.

Both methods join an array value inside a longer string with , , for example the values of a multi select element in Request for {interests}.

Accessing uploaded files 

New in version 14.2 Feature: #105708 - Multiple file upload for EXT:form elements

Before, FileUpload and ImageUpload elements accepted only one file.

The value of a FileUpload or ImageUpload element depends on its multiple property:

  • Single upload: a FileReference
  • Multiple upload: an ObjectStorage of FileReference objects

Handle both cases in a custom finisher that processes uploaded files:

EXT:my_site_package/Classes/Domain/Finishers/CustomFinisher.php
use TYPO3\CMS\Core\Resource\FileInterface;
use TYPO3\CMS\Extbase\Domain\Model\FileReference;
use TYPO3\CMS\Extbase\Persistence\ObjectStorage;
use TYPO3\CMS\Form\Domain\Finishers\AbstractFinisher;
use TYPO3\CMS\Form\Domain\Model\FormElements\FileUpload;

class CustomFinisher extends AbstractFinisher
{
  protected function executeInternal(): void
  {
    $formRuntime = $this->finisherContext->getFormRuntime();
    $elements = $formRuntime->getFormDefinition()
      ->getRenderablesRecursively();
    foreach ($elements as $element) {
      if (!$element instanceof FileUpload) {
        continue;
      }
      $value = $formRuntime[$element->getIdentifier()];
      if ($value instanceof FileReference) {
        $this->processFile($value->getOriginalResource());
      }
      if ($value instanceof ObjectStorage) {
        foreach ($value as $fileReference) {
          if ($fileReference instanceof FileReference) {
            $this->processFile(
              $fileReference->getOriginalResource()
            );
          }
        }
      }
    }
  }

  private function processFile(FileInterface $file): void
  {
    // Your custom logic, for example move, copy, or attach the file
  }
}
Copied!

Finisher Context 

The FinisherContext class takes care of transferring a finisher context to each finisher. If your finisher class extends AbstractFinisher the finisher context will be available via:

$this->finisherContext
Copied!

The cancel method prevents the execution of successive finishers:

$this->finisherContext->cancel();
Copied!

The method getFormValues returns the submitted form values.

$this->finisherContext->getFormValues();
Copied!

The method getFormRuntime returns the FormRuntime:

$this->finisherContext->getFormRuntime();
Copied!

Share data between finishers 

The method getFinisherVariableProvider returns an object ( FinisherVariableProvider ) which allows you to store data and transfer it to other finishers. The data can be easily accessed programmatically or inside your configuration:

$this->finisherContext->getFinisherVariableProvider();
Copied!

The data is stored in FinisherVariableProvider and is accessed by a user-defined 'finisher identifier' and a custom option value path. The name of the 'finisher identifier' should consist of the name of the finisher without the 'Finisher' appendix. If your finisher class extends AbstractFinisher , the finisher identifier name is stored in the following variable:

$this->shortFinisherIdentifier
Copied!

For example, if the name of your finisher class is 'CustomFinisher', this variable will contain 'Custom'.

There are 4 methods to access and manage data in the FinisherVariableProvider:

  • Add data:

    $this->finisherContext->getFinisherVariableProvider()->add(
        $this->shortFinisherIdentifier,
        'unique.value.identifier',
        $value
    );
    Copied!
  • Get data:

    $this->finisherContext->getFinisherVariableProvider()->get(
        $this->shortFinisherIdentifier,
        'unique.value.identifier',
        'default value'
    );
    Copied!
  • Check the existence of data:

    $this->finisherContext->getFinisherVariableProvider()->exists(
        $this->shortFinisherIdentifier,
        'unique.value.identifier'
    );
    Copied!
  • Delete data:

    $this->finisherContext->getFinisherVariableProvider()->remove(
        $this->shortFinisherIdentifier,
        'unique.value.identifier'
    );
    Copied!

In this way, finishers can access FinisherVariableProvider data programmatically. However, it is also possible to access FinisherVariableProvider data using form configuration.

Assuming that a finisher called 'Custom' adds data to a FinisherVariableProvider:

$this->finisherContext->getFinisherVariableProvider()->add(
    $this->shortFinisherIdentifier,
    'unique.value.identifier',
    'Wouter'
);
Copied!

other finishers can access the value 'Wouter' by setting {Custom.unique.value.identifier} in the form definition file.

public/fileadmin/forms/my_form.yaml
identifier: sample-form
label: 'Simple Contact Form'
prototype: standard
type: Form

finishers:
  -
    identifier: Custom
    options:
      yourCustomOption: 'Frans'

  -
    identifier: SomeOtherStuff
    options:
      someOtherCustomOption: '{Custom.unique.value.identifier}'
Copied!

Add finisher to backend UI 

After registering a new finisher in the yaml form definition file, you can also add it to the backend form editor for your backend users ( formEditor: section below) to work with in the GUI:

EXT:my_site_package/Configuration/Form/CustomFormSetup.yaml
prototypes:
  standard:
    formElementsDefinition:
      Form:
        formEditor:
          editors:
            900:
              # Extend finisher drop down
              selectOptions:
                35:
                  value: 'CustomFinisher'
                  label: 'Custom Finisher'
          propertyCollections:
            finishers:
              # add finisher fields
              25:
                identifier: 'CustomFinisher'
                editors:
                  100:
                    identifier: header
                    templateName: Inspector-CollectionElementHeaderEditor
                    label: "Custom Finisher"
                  # custom field (input, required)
                  110:
                    identifier: 'customField'
                    templateName: 'Inspector-TextEditor'
                    label: 'Custom Field'
                    propertyPath: 'options.customField'
                    propertyValidators:
                      10: 'NotEmpty'
                  # email field
                  120:
                    identifier: 'email'
                    templateName: 'Inspector-TextEditor'
                    label: 'Subscribers email'
                    propertyPath: 'options.email'
                    enableFormelementSelectionButton: true
                    propertyValidators:
                      10: 'NotEmpty'
                      20: 'FormElementIdentifierWithinCurlyBracesInclusive'
                  9999:
                    identifier: removeButton
                    templateName: Inspector-RemoveElementEditor
    finishersDefinition:
      CustomFinisher:
        formEditor:
          iconIdentifier: 'form-finisher'
          label: 'Custom Finisher'
          predefinedDefaults:
            options:
              customField: ''
              email: ''
        # displayed when overriding finisher settings
        FormEngine:
          label: 'Custom Finisher'
          elements:
            customField:
              label: 'Custom Field'
              config:
                type: 'text'
            email:
              label: 'Subscribers email'
              config:
                type: 'text'
Copied!

Configuration registration 

Place your YAML files in a form set directory — no PHP registration needed:

EXT:my_extension/
  Configuration/
    Form/
      MyFinisher/
        config.yaml
Copied!