Custom finisher 

Write a custom finisher 

If you want to make the finisher configurable in the backend UI read here.

Finishers are defined as part of a prototype within a finishersDefinition. The property implementationClassName is to be utilized to load the finisher implementation.

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

Copied!

The custom form definition has to be registered.

If the finisher requires options, you can define those within the options property. The options will be used as default values and can be overridden using the form definition.

Define the default value 

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

Override the option 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!

Each finisher has to be programmed to the interface FinisherInterface and should extend the class AbstractFinisher . In doing so, the logic of the finisher should start with the method executeInternal().

Accessing finisher options 

If your finisher extends AbstractFinisher , you can access your finisher options with the help of the parseOption() method:

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

parseOption() is looking for 'yourCustomOption' in your form definition. If it cannot be found, the method checks

  1. the prototype configuration for a default value,
  2. the finisher class itself by searching for a default value within the $defaultOptions property:

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

If the option cannot be found by processing this fallback chain, null is returned.

If the option is found, 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 very end, it tries to translate the finisher options.

Accessing form runtime values 

By utilizing a specific notation, finisher options can be populated with submitted form values (assuming you are using the parseOption() method). You can access values of the FormRuntime and thus values of each single form element by encapsulating the option values with {}. If there is a form element with the identifier 'subject', you can access its value within the finisher configuration. Check out the following example to get the whole idea.

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!

In addition, you can use {__currentTimestamp} as a special option value. It will return the current UNIX timestamp.

Finisher Context 

The class FinisherContext takes care of transferring a finisher context to each finisher. Given the finisher is derived from AbstractFinisher the finisher context will be available via:

$this->finisherContext
Copied!

The method cancel prevents the execution of successive finishers:

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

The method getFormValues returns all of the submitted form values.

getFormValues:

$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 within your configuration:

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

The data is stored within the FinisherVariableProvider and is addressed 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 potential 'Finisher' appendix. If your finisher is derived from the class AbstractFinisher , the name of this construct is stored in the following variable:

$this->shortFinisherIdentifier
Copied!

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

There are a bunch of methods to access and manage the finisher data:

  • 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, each finisher can access data programmatically. Moreover, it is possible to retrieve the data via configuration, provided that a finisher stores the values within the FinisherVariableProvider.

Assuming that a finisher called 'Custom' sets data as follows:

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

... you are now able to access the value 'Wouter' via {Custom.unique.value.identifier} in any other finisher.

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 adding a custom finisher you can also add the finisher to the form editor GUI to let your backend users configure it visually. Add the following to the backend yaml setup:

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 

Make sure the setup file is registered in either a EXT:my_extension/ext_localconf.php file:

EXT:my_extension/ext_localconf.php
<?php

declare(strict_types=1);

use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;

defined('TYPO3') or die();

ExtensionManagementUtility::addTypoScriptSetup('
        # for the backend
        module.tx_form.settings.yamlConfigurations {
            123456789 = EXT:yourExtension/Configuration/Form/CustomFormSetup.yaml
        }
        # for the frontend - otherwise the custom finisher class is not found
        # because of the missing "implementationClassName"
        plugin.tx_form.settings.yamlConfigurations {
            123456789 = EXT:yourExtension/Configuration/Form/Backend.yaml
        }
    ');
Copied!

Changed in version 13.0

Registration of global TypoScript for the TYPO3 backend has to be done in an extensions ext_localconf.php using method ExtensionManagementUtility::addTypoScriptSetup.

Former methods of global TypoScript registration are not compatible with site sets.