Ein Compare-Validator für das TYPO3 Form-Framework

Das TYPO3-Form-Framework

Mit TYPO3 8LTS hat TYPO3 ein sehr leistungsfähiges neues Formular-Framework erhalten, das seitdem mit jeder Version nochmals zugelegt hat und sehr einfach erweiterbar ist.

Die normalen Anforderungen an Formulare sind damit sehr gut abzubilden. Da die Formulardefinitionen in YAML-Dateien gespeichert werden, die sich sehr gut versionieren lassen, können auch komplexe Formulare gut vorbereitet und sicher auf verschiedene Server deployed werden. 

In der Vergangenheit haben wir für unsere Kunden spezielle Formularfeldelemene erstellt, Formularfelder mit spezifischen Daten aus der Datenbank oder anderen Quellen befüllt oder alternative Finisher erstellt - um z.B. die Formulardaten nach dem Absenden automatisch an weitere Systeme zu versenden o.ä.

Eine unerwartete Herausforderung

Insofern waren wir auch nicht sonderlich erschrocken, als ein Kunde uns als Anforderung die Validierung von Formularfeldern in Abhängigkeit anderer Formularfelder genannt hat. Schließlich können im TYPO3 Form-Framework problemlos auch eigene Validatoren eingebunden werden. 

Tatsächlich stellte sich diese Anforderung aber doch als zumindest kleines Problem dar: die Validatoren erhalten lediglich den zu validierenden Wert des entsprechenden Formularfeldes. Zugriff auf die anderen Formularfelder hat der Validator nicht - und damit wird es in der Tat schwierig, den korrekten Wert in Abhängigkeit eines anderen Feldes zu berechnen.

Nichts ist unmöglich

Als Entwickler freuen wir uns immer über neue Herausforderungen. Und nachdem wir diese gelöst haben, teilen wir diese auch gerne. Als Beispiel möchten wir daher an dieser Stelle einen Compare-Validator vorstellen, der überprüft, ob zwei Formularfelder identisch ausgefüllt wurden. Sofern eine andere Abhängigkeit abgefragt werden soll, ist das natürlich entsprechend adaptierbar.

 

Für unser Beispiel fügen wir unserem Formular ein E-Mail-Feld hinzu. Um Fehleingaben zu vermeiden, fragen wir diese zweimal ab - und unser Validator soll prüfen, ob in beiden Fällen die gleiche Adresse eingegeben wurde.

Hierfür definieren wir zunächst einen neuen Validator im Formular-Prototype:

 

validatorsDefinition:
  Compare:
    implementationClassName: 'F7\Sitepackage\Domain\Validation\CompareValidator'

 

und natürlich die entsprechende PHP-Klasse in unserer Extension sitepackage/Classes/Domain/Validation/CompareValidator.php

 

<?php

namespace F7\Sitepackage\Domain\Validation;

use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Object\ObjectManager;
use TYPO3\CMS\Extbase\Validation\Error;
use TYPO3\CMS\Extbase\Validation\Validator\AbstractValidator;
use TYPO3\CMS\Form\Domain\Model\Renderable\RenderableInterface;
use TYPO3\CMS\Form\Mvc\ProcessingRule;
use TYPO3\CMS\Form\Service\TranslationService;

class CompareValidator extends AbstractValidator
{

    protected $supportedOptions = [
        'compareTo' => 'string'
    ];

    public $compareTo = '1';

    public function __construct(array $options = []) {
        $this->compareTo = $options['compareTo'];
    }

    protected function isValid($value) {
        

    }
   
}

 

Wie leicht zu sehen ist, hat unsere Methode isValid() nur den Wert des aktuellen Feldes; und das ist ein Problem. An dieser Stelle ist es nur sehr schwierig, an die anderen Werte des Formulars zu kommen. Weil wir Wert auf eine saubere Lösung legen, suchen wir uns also eine andere Stelle und haben diese mit dem afterSubmit()-Event der FormElementHooks auch gefunden. 

Wir registrieren also einen neuen Hook in der ext_localconf.php: 

 

$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['afterSubmit'][1597841899] = F7\Sitepackage\Hooks\FormElementHooks::class;

 

und erstellen eine Klasse FormElementHooks.php in Classes/Hooks/ mit folgendem Inhalt: 

 

<?php
namespace F7\Sitepackage\Hooks;

use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Object\ObjectManager;
use TYPO3\CMS\Extbase\Validation\Error;
use TYPO3\CMS\Form\Domain\Model\Renderable\RenderableInterface;
use TYPO3\CMS\Form\Domain\Model\Renderable\RootRenderableInterface;
use TYPO3\CMS\Form\Domain\Runtime\FormRuntime;
use TYPO3\CMS\Form\Service\TranslationService;
use F7\Sitepackage\Domain\Validation\CompareValidator;
/**
 * Scope: frontend
 * @internal
 */
class FormElementHooks
{
    /**
     * This hook is invoked by the FormRuntime for each form element
     * **after** a form page was submitted but **before** values are
     * property-mapped, validated and pushed within the FormRuntime's `FormState`.
     *
     * @param FormRuntime $formRuntime
     * @param RenderableInterface $renderable
     * @param mixed $elementValue submitted value of the element *before post processing*
     * @param array $requestArguments submitted raw request values
     * @return mixed
     * @see FormRuntime::mapAndValidate()
     * @internal
     */
    public function afterSubmit(FormRuntime $formRuntime, RenderableInterface $renderable, $elementValue, array $requestArguments = [])
    {
        $formDefinition = $formRuntime['formDefinition'];
        $processingRules = $formDefinition->getProcessingRules();

        foreach ($processingRules as $field => $processingRule) {
            $validators = $processingRule->getValidators();
            if ($field === $renderable->getIdentifier()) {
                foreach ($validators as $validator) {
                    if ($validator instanceof CompareValidator) {
                        $compareTo = $validator->getCompareTo();
                        $validator->validateForFormFramework($requestArguments, $renderable, $processingRule);
                    }
                }
            }

        }
        return $elementValue;
    }
}

 

Immer wenn ein Formular abgesendet wurde, für das in einem Feld ein Compare-Validator konfiguriert ist, registriert dieser Hook das und ruft die Methode validateForFormFramework() mit den Informationen auf, die wir zur Prüfung brauchen. In der weitere oben erstellten Klasse Classes/Validation/CompareValidator.php nehmen wir also weitere Ergänzungen vor: 

 

 protected function isValid($value) {
        /* do literally nothing, as the check is located in F7\Sitepackage\Hooks\FormElementHooks
         * and the Validator is here for configuration purposes only
         *
         */
        return true;
    }

    /**
     * @param mixed[] $requestArguments
     * @param RenderableInterface $renderable
     * @param ProcessingRule $processingRule
     * @throws Exception
     */

    public function validateForFormFramework($requestArguments, $renderable, ProcessingRule  $processingRule) {
        $current = $requestArguments[$renderable->getIdentifier()];
        $target = $requestArguments[$this->compareTo];

        if ($current !== $target) {
            // comparison failed, add error
            $processingRule = $renderable->getRootForm()->getProcessingRule($renderable->getIdentifier());
            $processingRule->getProcessingMessages()->addError(
                GeneralUtility::makeInstance(ObjectManager::class)
                    ->get(
                        Error::class,
                        TranslationService::getInstance()->translate(
                            'validation.error.1597847961',
                            null,
                            'EXT:sitepackage/Resources/Private/Language/locallang.xlf'
                        ),
                        1597847961
                    )
            );
        }
    }

 

Damit sind wir schon fast am Ziel. Es fehlt in der Formulardefiniton lediglich noch, welche Felder verglichen werden sollen. Das geht für unser Beispiel so:

 

- identifier: email
  defaultValue: ''
  type: Email
  label: Email
  properties:
    fluidAdditionalAttributes:
      placeholder: 'Email-Adresse'
    validators:
      - identifier: EmailAddress
      - identifier: NotEmpty
- identifier: email2
  defaultValue: ''
  type: Email
  label: Email-Bestätigung
  properties:
    fluidAdditionalAttributes:
      placeholder: 'Email-Bestätigung'
    validators:
      - identifier: NotEmpty
      - identifier: Compare
        options:
          compareTo: 'email'

 

Hier ist sehr schön zu sehen, dass zum einen unser Validator mit anderen Standard-Validatoren wunderbar zusammen funktioniert. Beim Feld "Email-Bestätigung" ist die Konfiguration des Compare-Validators zu sehen.

Kommentare

Keine Kommentare

Kommentar schreiben

* Diese Felder sind erforderlich