Saturday, May 4, 2013

TYPO3 Extbase - own validators and multi step form validation using the old and new property mapper

TYPO3 Extbase comes with some standard validators which can be used to validate user input. When working with domain models, you can use those validators to validate the properties of the domain model.

When you use Extbase to create a form, where some part of the validation is done through an external API (e.g. logically validation) or you want to validate properties against other properties, the validation process can be more complicated. If you have a multiple step form, where all user input is collected, not persisted and finally sent to an external API which validates the input, it can be even more complicated to "jump back" to the desired step of the multiple step form and output the validation errors of the external API.

In this article I will show how to use Extbase validators to validiate a domain model and add several error messages for validation errors at once. I will also show how to display error messages for desired fields in a multiple step form after the validation process via @validate has been performed.

Besides this, the article also shows how to create a multiple step form in Extbase and how to handle validation and persistence.

At the time of writing of this article, TYPO3 6.1 with Extbase 6.1 was released. In Extbase 6.1, the new property mapper is enabled by default.

This article covers both the new and the old property mapper. I have created 2 GitHub repositories, which include all examples for the old and the new property mapper.
The code examples in this article are may be incomplete to save space. Please visit the GitHub repositories mentioned above to see the complete code.

Which property mapper to use - old or new one?

Writing this article, I spent some time with the validation classes of Extbase. Since Extbase 1.4, a lot of things like Tx_Extbase_MVC_Controller_ArgumentError or the $errors array in Tx_Extbase_Validation_Validator_AbstractValidator became deprecated and should be removed in TYPO3 6.0. Well, in TYPO3 6.0, those classed still existed and the deprecation notice now mentioned, that they will be removed in TYPO3 6.1. Some kind of confusing, I thought. Finally, as TYPO3 6.1 came out, the deprecation notice showed, that the deprecated stuff will be removed two versions after TYPO3 6.1 (so it will "survive" TYPO3 6.2 LTS). I guess, this is due to keep the backward compatibility to the upcoming LTS version of TYPO3 as high as possible.

For those who are unsure which property mapper to use, I recommend the following. If you create a new TYPO3 extension for TYPO3 6.0 or greater - use the new property mapper. It is configurable and extensible and ensures, that you don't use deprecated functions and classes.


Creating an own validator with validation errors for multiple properties

First I will show you how to create an own validator, which validates a given domain model and is able to add errors for multiple properties. The validator checks, if the given ZIP-code and city-name do match logically (e.g. for the ZIP "20095" the city name must be "Hamburg")

Assume you have a domain model "addressdata", which contains fields for a users addressdata. Straight validation like "Empty" or "Numeric" can is implemented in the domain model by using @validate annotations.

To implement the logical validation, I create a new validator in ExtBase.

Old property mapper

/**
 * Validates the given value
 *
 * @param mixed $value
 *
 * @return bool
 */
public function isValid($value) {
 $apiValidationResult = $this->apiService->validateAddressData($value);
 $success = TRUE;
 if ($apiValidationResult['zip']) {
  $error = $this->objectManager->get('Tx_Extbase_Validation_Error', $apiValidationResult['zip'], time());
  $this->errors['zip'] = $this->objectManager->get('Tx_Extbase_Validation_PropertyError', 'zip');
  $this->errors['zip']->addErrors(array($error));
  $success = FALSE;
 }
 if ($apiValidationResult['city']) {
  $error = $this->objectManager->get('Tx_Extbase_Validation_Error', $apiValidationResult['city'], time());
  $this->errors['city'] = $this->objectManager->get('Tx_Extbase_Validation_PropertyError', 'city');
  $this->errors['city']->addErrors(array($error));
  $success = FALSE;
 }
 return $success;
}

New property mapper

/**
 * Validates the given value
 *
 * @param mixed $value
 * @return bool
 */
protected function isValid($value) {
 $apiValidationResult = $this->apiService->validateAddressData($value);
 $success = TRUE;
 if ($apiValidationResult['zip']) {
  $error = $this->objectManager->get('TYPO3\\CMS\\Extbase\\Validation\\Error',
   $apiValidationResult['zip'], time());
  $this->result->forProperty('zip')->addError($error);
  $success = FALSE;
 }
 if ($apiValidationResult['city']) {
  $error = $this->objectManager->get('TYPO3\\CMS\\Extbase\\Validation\\Error',
   $apiValidationResult['city'], time());
  $this->result->forProperty('city')->addError($error);
  $success = FALSE;
 }
 return $success;
}


The code above shows, that an external API is used to validate the adress data logically. If the external API returns errors for given fields, errors are manually added for each property to the ExtBase validator.

To use the newly created validator, you just have to use the @validate annotation in your action like shown below.

Old property mapper

/**
 * Create action
 *
 * @param Tx_ValidationExamples_Domain_Model_Addressdata $newAddressdata
 * @validate $newAddressdata Tx_ValidationExamples_Validation_Validator_AddressdataValidator
 * @return void
 */
public function createAction(Tx_ValidationExamples_Domain_Model_Addressdata $newAddressdata) {
 $this->addressdataRepository->add($newAddressdata);
 $this->view->assign('message', 'Addressdata has been created');
}

New property mapper

/**
 * Create action
 *
 * @param \derhansen\ValidationExamplesNew\Domain\Model\Addressdata $newAddressdata
 * @validate $newAddressdata \derhansen\ValidationExamplesNew\Validation\Validator\AddressdataValidator
 * @return void
 */
public function createAction(\derhansen\ValidationExamplesNew\Domain\Model\Addressdata $newAddressdata) {
 $this->addressdataRepository->add($newAddressdata);
 $this->view->assign('message', 'Your new Addressdata was created.');
}

The complete source for this example has been tagged in the Github repository. Below follows direct links to the tags.

Adding custom validation errors in a multiple step form after extbase domain object validation

Sometimes the validation of a form can't be implemented by using @validate annotations. Assume you have a multiple step form, where you just collect user input and validate it using an external API in the last step of your form.

There are several approaches to create multiple step forms in Extbase. For this article I use the approach of splitting the main domain model into several small part-domain models, saving them to session variables after each step and consolidate them in the end to the main domain-object which gets persisted.

Below is a chart of the multiple step form validation process I'm going to create.



One advantage of splitting the main domain model to several small part domain models is the fact, that you can use @validation directly in the domain model without caring about the actual step of the form, where you switch validation for single properties on or off.

I created a 3 step form to enter some addressdata. The first step requires first- and lastname, the second step requires the street and streetnumber and the third step requires the zip-code and the city. When all form data has been collected, a new addressdata object is persisted to the database.

The first version of the multiple step form is tagged in the GitHub repository as example2. It includes the main validation of Extbase and is able to save the form data, when no domain model validation errors are present.
Please note, that the example2-tag for the old property mapper misses this code change, which I first discovered after the repository has been tagged.

Now I've implemented the external API service, which does some logical validation for the given address data.

/**
 * Simulates validation of addressdata entered in the multiple steps form.
 * Returns an array of validation errors for each step of the multiple steps form
 *
 * @param Tx_ValidationExamples_Domain_Model_Addressdata $addressdata
 * @return array
 */
public function validateMultipleSteps(Tx_ValidationExamples_Domain_Model_Addressdata $addressdata) {
 $errors = array();
 if ($addressdata->getStreet() == 'Elbstra├če' && $addressdata->getStreetnr() > 145) {
  $errors['step2']['streetnr'] = 'Streetnr not valid for this street';
 }
 if ($addressdata->getZip() == 20095 && $addressdata->getCity() != 'Hamburg') {
  $errors['step3']['zip'] = 'ZIP Code and city do not match';
  $errors['step3']['city'] = 'ZIP Code and city do not match';
 }
 return $errors;
}

The call to the API service is implemented in the createAction() for the form. If the API service returns errors for some fields, then the createAction() saves the validation result to a session variable and redirects the user to the desired step in the multiple step form.

In the action for the given step, I've implemented a check for the validation results of the API service. This check sets validation errors to the given properties of the domain model. If there already are validation errors for the domain model, the new ones from the external API validation are added.

Old property mapper


/**
 * Sets validation errors for fields in the given step
 *
 * @param string $step The step
 * @return void
 */
protected function setApiValidationErrors($step) {
 $apiresults = $GLOBALS['TSFE']->fe_user->getKey('ses', 'apiresults');
 if (array_key_exists($step, $apiresults)) {
  /* Set Form Errors manually */
  $origErrors = $this->controllerContext->getRequest()->getErrors();
  if ($origErrors) {
   $errors = $origErrors[$step . 'data'];
  } else {
   $errors = $this->objectManager->get('Tx_Extbase_MVC_Controller_ArgumentError' ,$step . 'data');
  }

  $propertyErrors = array();

  /* Add validation errors */
  foreach ($apiresults[$step] as $key => $value) {
   $propertyErrors[$key] = $this->objectManager->get('Tx_Extbase_Validation_PropertyError', $key);
   $message = $apiresults[$step][$key];
   $propertyError = $this->objectManager->get('Tx_Extbase_Validation_Error', $message, time());
   $propertyErrors[$key]->addErrors(array($propertyError));
  }
  $errors->addErrors($propertyErrors);

  $this->controllerContext->getRequest()->setErrors(array($errors));
 }
}

New property mapper


/**
 * Sets validation errors for fields in the given step
 *
 * @param string $step The step
 * @return void
 */
protected function setApiValidationErrors($step) {
 $apiresults = $GLOBALS['TSFE']->fe_user->getKey('ses', 'apiresults');
 if (array_key_exists($step, $apiresults)) {
  /* Set Form Errors manually  - get results from property mapper and add new errors */
  $result = $this->getControllerContext()->getRequest()->getOriginalRequestMappingResults();

  /* Add validation errors */
  foreach ($apiresults[$step] as $key => $value) {
   $error = $this->objectManager->get('TYPO3\\CMS\\Extbase\\Validation\Error',
    $apiresults[$step][$key], time());
   $result->forProperty($step . 'data.' . $key)->addError($error);
  }
  $this->getControllerContext()->getRequest()->setOriginalRequestMappingResults($result);
 }
}

Since the code above is specially made for handling multiple steps / fields (like this example), below follows a more common example which shows how to set a validation error for a special property of a domain object.

Old property mapper


/* Set validation error for property */
$errors = $this->objectManager->get('Tx_Extbase_MVC_Controller_ArgumentError', 'addressdata'); 

$propertyErrors = array();

$propertyErrors['streetnr'] = $this->objectManager->get('Tx_Extbase_Validation_PropertyError', 'streetnr');
$message = 'Validation message for streetnr';
$propertyError = $this->objectManager->get('Tx_Extbase_Validation_Error', $message, time());
$propertyErrors['streetnr']->addErrors(array($propertyError));

$errors->addErrors($propertyErrors);

$this->controllerContext->getRequest()->setErrors(array($errors));

New property mapper

$result = $this->getControllerContext()->getRequest()->getOriginalRequestMappingResults();
$error = $this->objectManager->get('TYPO3\\CMS\\Extbase\\Validation\Error', 'Validation message for streetnr', time());
$result->forProperty('addressdata.streetnr')->addError($error);
$this->getControllerContext()->getRequest()->setOriginalRequestMappingResults($result);

This code adds an error for the domain object "addressdata" and sets a single validation message for the property "streetnr".

The final version of the multiple step form is tagged in the GitHub repository as example3.

Conclusion

Using the techniques shown above gives you flexibility when working with Extbase and external validation services. It also shows how to set validation errors for multiple properties at once and how to control validation results after the property mapper has processed domain validation.

As you may have noticed, the examples for the new property mapper in Extbase look more clear, contains lesser code and are better readable, since it does not use arrays to collect validation errors but objects.