abstract business code coder
TYPO3 Blog

Form-Framework Formulare in eigenen Extensions nutzen 

Dieser Artikel zeigt, wie man ein Formular, das mit dem Form Framework erstellt wird, innerhalb einer extbase Extension nutzen kann.

Wir wollen mittels Extbase/Fluid eine Extension zur Anzeige von Datensätzen, z.B. Produkten, mit einer Listenansicht und einer Detailansicht erstellen.

In der Detailansicht soll jeweils ein produktspezifisches Kontaktformular angezeigt werden, das den Namen des angezeigten Produkts mit übermittelt.

Das Formular soll mittels Form-Editor schnell und einfach erstellt werden können inkl. aller Validatoren und Finisher.

Wir möchten das Formular per HTML-Email mit dem E-Mail-Finisher versenden.

Das Ganze möchten wir nicht selbst in einer Action programmieren, sondern wir möchten die ganze serverseitige Formular-Logik vom Form-Framework nutzen.

Kontakt-Formular mit Form Framework in TYPO3 9.5

Das Formular soll mit dem Form Editor erstellt werden:

TYPO3 Form Editor

Vorarbeiten: Debugging-Einstellungen

Oops an error occurred in extbase plugins

Man kann statt der Meldung `Oops an error occurred` auch den Stacktrace ausgeben
lassen. Dazu ist folgendes TYPOSCRIPT im Setup des Templates erforderlich:
 

config.contentObjectExceptionHandler = 0

 

Erstellung der Extension

Wir erstellen mit dem Extension Builder eine einfache Demo-Extension, mit der man einfache Datensätze anlegen kann, die lediglich einen Titel enthalten.
Darüber hinaus erstellen wir ein Plugin, welches eine Listendarstellung der Datensätze ausgibt und eine Detailansicht eines einzelnen Datensatzes.

Anpassung der show Action

In der Show-Action möchten wir zusätzlich zur Anzeige der Artikeldetails ein Kontaktformular anzeigen, welches per E-Mail versendet wird und welches den Namen des angezeigten Produkts mit übermittelt.

An dieser Stelle möchten wir nun ein Formular einbinden, welches wir
mit dem Form Framework erstellen werden.

Pfad zu eigenen Form-Konfigurationen anlegen

Configuration/Form/FormSetup.yaml

YAML
TYPO3:
  CMS:
    Form:
      persistenceManager:
        allowedExtensionPaths:
          1591099960: EXT:test1/Resources/Private/Forms/
        allowSaveToExtensionPaths: true
        allowDeleteFromExtensionPaths: false

 

Eigene Configuration per TYPOSCRIPT einbinden

für das BE

ext_localconf.php

PHP
if (TYPO3_MODE === 'BE') {
    /**
     * Register custom EXT:form configuration
     */
    \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addTypoScriptSetup(
        trim('
                module.tx_form {
                    settings {
                        yamlConfigurations {
                            1591100383 = EXT:test_1/Configuration/Form/FormSetup.yaml
                        }
                    }
                }
            ')
    );
}

Jetzt kann man eine neue Formulardefinition im Form Editor im BE anlegen.

für das FE

analog zu der für das BE mit dem Key plugin.tx_form anstelle von module.tx_form:

Configuration/TypoScript/setup.typoscript

# register forms for FE
plugin.tx_form {
    settings {
        yamlConfigurations {
            # register your own additional configuration
            # choose a number higher than 30 (below is reserved)
            1591100383 = EXT:test1/Configuration/Form/FormSetup.yaml
        }
    }
}

Jetzt kann man das Formular mit dem `formvh:render` Viewhelper im Frontend rendern.
Vergisst man dieses TYPOSCRIPT, so erhält man im FE folgende Fehlermeldung:
The file xxx could not be loaded. Please check your configuration option "persistenceManager.allowedExtensionPaths"

Absenden des Formulars

Beim ersten Absenden des Formulars erhält man folgende Fehlermeldung:
 

Wir müssen also entweder eine neue Action perform zulassen oder das Rendering des Formulars so anpassen, dass statt dessen eine bereits vorhandene Action genutzt wird.

Erstellung der perform Action im Controller

Wir erstellen die perform action, die nichts anderes macht, als auf die show-Action weiterzuleiten.

typo3conf/ext/test1/Classes/Controller/TestRecordController.php

PHP
    /**
     * action perform (called after submitting the form)
     *
     * @param \Sitegeist\Test1\Domain\Model\TestRecord $testRecord
     * @return void
     */
    public function performAction(\Sitegeist\Test1\Domain\Model\TestRecord $testRecord)
    {
        $this->forward('show');
    }

Und wir registrieren diese neue Action als non-cacheable, da die Formularversendung bei jedem Submit komplett durchgeführt werden soll:

typo3conf/ext/test1/ext_localconf.php

PHP
        \TYPO3\CMS\Extbase\Utility\ExtensionUtility::configurePlugin(
            'Sitegeist.Test1',
            'Plugin1',
            [
                'TestRecord' => 'list, show, create, edit, update, delete, perform'
            ],
            // non-cacheable actions
            [
                'TestRecord' => 'edit, update, delete, perform'
            ]
        );

Wenn man das Formular jetzt erneut abschickt, dann erhält man folgende neue Fehlermeldung:

Das liegt daran, dass unsere Action als Parameter den Datensatz erwartet.
Diesen hat das Formular bisher nicht und wir wollen in im nächsten Schritt mit in das Formular aufnehmen.

Dem Formular den aktuellen Datensatz mit übergeben

Um dem Formular den aktuellen Datensatz mit zu übergeben, können wir dem formvh:render viewhelper den Parameter overrideConfiguration mitgeben und darin angeben, dass wir einen zusätzlichen Parameter mit übergeben wollen, nämlich die uid des aktiven Datensatzes:

typo3conf/ext/test1/Resources/Private/Templates/TestRecord/Show.html

XML
<formvh:render
    persistenceIdentifier="EXT:test1/Resources/Private/Forms/productContactForm.form.yaml"
    overrideConfiguration="{
        renderingOptions: {
            controllerAction:'perform',
            additionalParams: {
                'tx_test1_plugin1[testRecord]': testRecord
            }
        }    
    }"
/>

Jetzt wird das Formular erfolgreich versendet und danach die show action wieder angezeigt, wobei das Formular nun nicht mehr erscheint.

Alternative: Verwendung einer vorhandenen Action

Anstatt die Standard-Action perform zu verwenden, können wir die Definition des Formulars auch so anpassen, dass eine andere Action verwendet wird.
Wichtig dabei ist, dass diese Action nicht gecached wird, damit die Finisher auch bei jeder Versendung des Formulars aufgerufen werden.
Ferner ist wichtig, dass diese Action in ihrer View auch das Formular wieder anzeigt und darin den formvh:render Viewhelper verwendet. Denn der übernimmt das komplette Rendering des Formulars im richtigen Zustand und die Ausführung der Finisher bzw. die korrekte Anzeige der Steps bei einem Multi-Step-Formular.

Um eine eigene Action zu definieren, ist im obigen Beispiel für den Parameter controllerAction einfach ein anderer Actionname anzugeben.

Formularfeld für den Titel des Datensatzes ergänzen

In der Mail, die man durch den E-Mail-Finisher erhält, ist nun allerdings das Produkt nicht enthalten.
Das liegt daran, dass versteckte Felder normalerweise nicht mit übergeben werden in dem E-Mail Finisher.

Wir werden daher das Formular so erweitern, dass der Name des Datensatzes auch in einem sichtbaren Feld mit übergeben wird. Dazu können wir den Form-Editor benutzen, um das Feld hinzuzufügen.

Das führt nach dem Speichern zu folgender Erweiterung in unserer Yaml Formulardefinition:

Resources/Private/Forms/productContactForm.form.yaml

YAML
renderables:
  -
    renderables:
      -
        defaultValue: ''
        type: Text
        identifier: text-4
        label: Product
        properties:
          elementDescription: 'Name of the Product'

Das führt nach einem Reload unserer Detailseite zu folgendem neuen Formular:

Nun müssen wir noch dafür sorgen, dass dieses Feld korrekt vorbelegt wird mit dem Titel unseres Datensatzes.
Das erledigen wir im nächsten Schritt.

Vorbelegung eines Feldes im Formular

Wir möchten nun das Feld Product vorbelegen mit dem Titel unseres Datensatzes. In unserem Fluid-Template steht der Datensatz als Fluid-Variable testRecord zur Verfügung. Wir erhalten also den Titel des Datensatzes mit testRecord.title.

Diesen können wir nun dem formvh:render view-helper in dem Parameter overrideConfiguration mit übergeben und müssen uns dazu genau die hierarchische Struktur unserer yaml Formulardefinition anschauen und diese via fluid im Bereich renderables nachbilden:

XML
<formvh:render
    persistenceIdentifier="EXT:test1/Resources/Private/Forms/productContactForm.form.yaml"
    overrideConfiguration="{
        renderingOptions: {
            controllerAction:'perform',
            additionalParams: {
                'tx_test1_plugin1[testRecord]': testRecord
            }
        },    
        renderables: {
            0: {
                renderables: {
                    4: {
                        defaultValue: testRecord.title
                    }
                }
            }
        }

    }"
/>

Hinweis: Macht man in der inline Konfiguration des Parameters `overrideConfiguration` einen Fehler, so dass Fluid daraus kein Objekt mehr erzeugen kann, so erscheint folgende Fehlermeldung:

Das kann z.B. passieren, wenn man hinter dem letzten Element noch ein Komma stehen hat.

Sehr unschön ist, dass man genau die numerischen Indizes (beginnend bei 0) für die einzelnen Elemente des Formulars aus der yaml Definition abzählen muss.
Ändert man später im Editor die Reihenfolge der Elemente oder fügt noch eins vor dem Produktfeld ein, so muss die Konfiguration hier entsprechend angepasst werden.

In unserem obigen Beispiel ist als Index eine 4 angegeben, das das Produktfeld das 5. Element innerhalb unseres Formulars ist und die Zählung der Elemente bei 0 beginnt.

Alternativ kann man das Formularfeld in der yaml-Konfiguration auch weglassen und statt dessen ein komplett neues Feld in dem Parameter overrideConfiguration des formvh:render Viewhelpers erstellen, das einen eindeutigen, alphanumerischen Index erhält. Für dieses muss man dann aber auch alle Attribute analog zur Yaml Konfiguration des Feldes mit angeben:

XML
<formvh:render
    persistenceIdentifier="EXT:test1/Resources/Private/Forms/productContactForm.form.yaml"
    overrideConfiguration="{
        renderingOptions: {
            controllerAction:'perform',
            additionalParams: {
                'tx_test1_plugin1[testRecord]': testRecord
            }
        },    
        renderables: {
            0: {
                renderables: {
                    productField: {
                        defaultValue: testRecord.title,
                        identifier: 'productField',
                        type: 'Text',
                        label: 'Dynamic Productfield',
                        properties: {
                            elementDescription: 'Name of the Product'
                        }
                    }
                }
            }
        }

    }"
/>

Bei diesem Vorgehen gibt es 2 Nachteile:

  1. Das Feld ist im Formulareditor nicht sichtbar und anpassbar.
  2. Man kann die Reihenfolge dieses Feldes nicht steuern, es wird immer am Ende des Formulars angehängt.

Debugging der Formular-Konfiguration

Das Überschreiben der Formular-Konfiguration kann ein mühsames Unterfangen werden, wenn ein Feld die Werte nicht erhält oder das Überschreiben nicht wie gewünscht funktioniert.
Für diese Fälle bietet es sich an, sich einmal vom Form Framework ausgeben zu lassen, wie denn die finale Konfiguration nach Anwendung unserer overrideConfiguration aussieht.

Dazu kann man temporär eine debug-Meldung ausgeben lassen, indem man die Methode renderStatic in dem Viewhelper typo3/sysext/form/Classes/ViewHelpers/RenderViewHelper.php
in Zeile 90 um eine debug-Ausgabe erweitert:

PHP
/**
     * @param array $arguments
     * @param \Closure $renderChildrenClosure
     * @param RenderingContextInterface $renderingContext
     * @return string
     */
    public static function renderStatic(array $arguments, \Closure $renderChildrenClosure, RenderingContextInterface $renderingContext)
    {
        $persistenceIdentifier = $arguments['persistenceIdentifier'];
        $factoryClass = $arguments['factoryClass'];
        $prototypeName = $arguments['prototypeName'];
        $overrideConfiguration = $arguments['overrideConfiguration'];
        $objectManager = GeneralUtility::makeInstance(ObjectManager::class);
        if (!empty($persistenceIdentifier)) {
            $formPersistenceManager = $objectManager->get(FormPersistenceManagerInterface::class);
            $formConfiguration = $formPersistenceManager->load($persistenceIdentifier);
            ArrayUtility::mergeRecursiveWithOverrule(
                $formConfiguration,
                $overrideConfiguration
            );
            $overrideConfiguration = $formConfiguration;
            $overrideConfiguration['persistenceIdentifier'] = $persistenceIdentifier;
        }
        
        // *******************************
        // debug the merged configuration in FE
        // *******************************
        debug($overrideConfiguration);
        
        // ...
    }

Die Ausgabe sieht dann so aus:

Vorteile gegenüber eines Formulars in Extbase/Fluid

  • schneller erstellt
  • Validatoren und Finisher vom Form-Framework können genutzt werden
  • (fast) keine eigene Action, d.h. keine serverseitige Logik erforderlich

Weiterführende Links