
March 3, 2021 4 min read
Flexibly control which TYPO3 form finisher to run
Here I present a simple way to control from within a TYPO3 Form Finisher if and which subsequent finishers should be executed.
The TYPO3 form framework offers many possibilities and is easy to extend. Here I present a simple way how an individual finisher can influence the following.
A custom form finisher
In the TYPO3 Form Framework (ext:form), a finisher is a class that takes care of processing the data that the user has entered in the form. By the time the finisher comes into play, simple checks on the data, such as field validations, have already been completed. Finishers are now responsible for what to do with that data.
For many use cases, the Form Framework already comes with ready-made finishers. For example, a confirmation email can be sent to the user, or the site owner can be notified, or the data can be stored away in the database. And also the confirmation message or the redirection to another page is realized by a finisher.
A nice concept in the Form Framework is that you can switch individual finishers one after the other. So you don't have to decide if the user gets an email or if the data should be written to the database. From a software architecture point of view, the finishers remain very lean and implement exactly what they are supposed to do - e.g. pass data to SAP, address an API or whatever.
In principle, the finishers can also build on each other. For example, one finisher can generate a PDF from the data and another finisher can then send this PDF as an attachment to an e-mail.
The problem
For a customer project I recently had to implement a finisher that is the first finisher to analyze the input. This post is not about what exactly this finisher does and does not do. But part of the problem is that the finisher should have an influence on the following finishers - and not on all of them, but only on some of them.
For simplicity, let's think of an antiSPAM finisher that should be invisible to the outside world. So to the spammer it looks like his attempts are successful, but internally no records or emails are generated. (Whether this makes much sense or not remains to be seen - for the explanation and solution of the problem, the example is appropriate).
Workflow of the finisher
For our site we assume that the following finishers are processed one after the other:
- AntiSpam finisher - checks if the requests are SPAM.
- SaveToDB-Finisher - saves the request into the database
- EmailFinisher - sends an email to the page owner
- Confirmation - displays a "Thank you for your message" message.
Depending on whether the AntiSpam Finisher has detected spam or not, steps 2 + 3 should be skipped (or not). Step 4 should be executed in any case.
And why is this difficult?
The finishers are independent from each other and they don't know anything about each other. For example, finishers can set variables that other finishers can access - but we don't want to extend all standard finishers just to make them compatible with our AntiSpam finisher.
Furthermore, each finisher can cancel further processing. However, this then results in no further finisher being executed. Also not what we want.
In media res
But let's dive into the code first. First of all, we need a finisher that implements our logic.
For that, we'll implement an AntiSpamFinisher class:
<?php
namespace MarcWillmann\Sitepackage\Domain\Finishers;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Form\Domain\Finishers\AbstractFinisher;
class AntispamFinisher extends AbstractFinisher
{
/**
* @var string
*/
protected $shortFinisherIdentifier = 'Antispam';
/**
* @var mixed[]
*/
protected array $data = [];
protected function executeInternal(): void
{
// do whatever the finisher has to do
}
}
and make them known in the form framework:
TYPO3:
CMS:
Form:
prototypes:
default:
finishersDefinition:
Antispam:
implementationClassName: MarcWillmann\Sitepackage\Domain\Finishers\AntispamFinisher.
FormEngine:
label: 'IP Block (Anti spam)'
Now we can already use the finisher in our forms. To do this, we simply insert the finisher into the form.form.yaml, e.g. like this:
prototypeName: standard
finishers:
- identifier: Antispam
- identifier: EmailToReceiver
options:
subject: 'new order
recipientAddress: 'sales@domain.tld'
recipientName: '1001 TYPO3 solutions GmbH & Co. KG
senderAddress: 'no-reply@domain.tld'
senderName: '{firstname} {lastname}'
-
options:
message: 'Thank you for your message!'
contentElementUid: ''
identifier: ConfirmationThe solution to the riddle
At this point I was stuck for some time. I tried several approaches and discarded them again. But don't worry, it's much easier than I thought:
First, we set a finisher variable in the AntiSpamFinisher class, depending on whether we detected SPAM or not:
protected function executeInternal(): void
{
// do whatever the finisher has to do
if ($isSpam) {
$this->finisherContext->getFinisherVariableProvider()->add(
$this->shortFinisherIdentifier,
'enabled',
false
);
} else {
$this->finisherContext->getFinisherVariableProvider()->add(
$this->shortFinisherIdentifier,
'enabled',
true
);
}this looks a little strange at first glance. But it becomes clearer with the little trick that completes the solution. In the form.form.yaml:
prototypeName: default
finishers:
- identifier: Antispam
- identifier: EmailToReceiver
options:
subject: 'new order
recipientAddress: 'sales@domain.tld'
recipientName: '1001 TYPO3 solutions GmbH & Co. KG
senderAddress: 'no-reply@domain.tld'
senderName: '{firstname} {lastname}''
renderingOptions:
'{Antispam.enabled}'
-
options:
message: 'Thank you for your message!'
contentElementUid: ''
identifier: Confirmationwe leave the decision whether to execute each finisher or not to this very variable we set at the beginning. Quite simple really, isn't it? :)



Comments (5)
Karsten Soll
at 07.06.2022ich hätte mal eine Frage an jemanden, der sich offensichtlich damit auskennt :) Ich möchte mit der Typo3 EXT:form die Inhalte eines Formulars an unser CRM senden. Dazu muss eigentlich "nur" die entsprechende URL in das action-Attribut rein - Fertig! Ich kann aber nichts dazu finden, wie man einen eigenen Finisher dahingehend erstellt. - Hättest Du einen Hinweis für mich?
Gruß
Karsten
Marc Willmann
at 07.06.2022die Action-URL des Formulars willst Du eigentlich nicht verändern, weil sonst alle Deine Validatoren, andere Finisher usw. nicht mehr funktionieren und Du auch nicht zurückmelden kannst, welche Pflichtfelder ggf. nicht korrekt ausgefüllt wurden - die gesamten Daten würde dann ja nur das CRM-Formular bekommen.
Ich würde daher das Form komplett in ext:form lassen, und einen Finisher einbauen, der die gesammelten und validierten Daten (das macht ja alles ext:form) als "Client" an das CRM schickt. Im Prinzip kannst Du den Artikel hier weitestgehend dafür verwenden; in der executeInternal()-Methode nimmst Du dann die Daten, die Du haben willst und schiebst sie via PHP-curl an Dein externes CRM-Formular.
Falls gewünscht, kannst Du danach noch weitere Finisher nutzen (send Mail, store to DB, ...) oder auch nicht.
Marc
Alex
at 02.09.2022könnte man auf die Weise irgendwie einem Element Textfeld den Eintrag ... list="xyz" hinzufügen?
Gebe zu, bin da etwas verloren, welche Schritte notwendig wären.
Auf die Art könnte aber ein Textfeld mit einer Datalist arbeiten.
Prinzipiell funktioniert das auch, da hab ich mich die Tage durchgebissen. Nur eine echte Verbindung zu Form, da ist mir dann dezent der Kopf durchgeraucht ;)
Gruß, Alex
Marc Willmann
at 03.09.2022Was Du mit der DataList vorhat, hab ich auf Anhieb nicht verstanden. Hast Du ein Anwendungsszenario, das Du lösen möchtest?
Max
at 29.09.2023ich habe schwierigkeiten meinen Finisher anzubinden.
Bei mir kommt dieser Fehler:
he finisher preset identified by "CustomFinisher" could not be found, or the implementationClassName was not specified.
Ich weiß auch nicht genau in welchem Ordner ich die Dateien erstellen soll.
Viele Grüße
Max