Form-Framework Formulare in eigenen Extensions nutzen
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.
Das Formular soll mit dem Form Editor erstellt werden:
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
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
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
/**
* 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
\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
<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
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:
<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:
<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:
- Das Feld ist im Formulareditor nicht sichtbar und anpassbar.
- 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:
/**
* @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