phpcakephp-3.0

beforeMarshal does not modify request data when validation fails


Bug or Feature? If I change request data with beforeMarshal and there is a validation error, the request data will not be given back modified.

This question may be related to How to use Trim() before validation NotEmpty?.

Modifying Request Data Before Building Entities If you need to modify request data before it is converted into entities, you can use the Model.beforeMarshal event. This event lets you manipulate the request data just before entities are created. Source: CakePHP 3 Documentation

According to the book I would expect the request data is always changed, no matter if there is a validation error or not.

Example or test case:

// /src/Model/Table/UsersTable.php
namespace App\Model\Table;
use Cake\ORM\Table;
// Required for beforeMarshal event:
use Cake\Event\Event;
use ArrayObject;
// Required for Validation:
use Cake\Validation\Validator;

class UsersTable extends Table {

  public function beforeMarshal(Event $event, ArrayObject $data, ArrayObject $options) {
    $data['firstname'] = trim($data['firstname']);
  }

  public function validationDefault(Validator $validator) {
    $validator
      ->add('firstname', [
        'minLength'   => [ 'rule' => ['minLength', 2], 'message' => 'Too short.' ],
      ])
      ;
    return $validator;
  }
}

If I enter " d" (Space-d) the validation error is shown, but the space itself is not removed in the form. I would expact the form showing only "d" because the space is removed from the request data with the beforeMarshal event. So... bug or feature?

My solution would be to use the trim()-function in the controller instead of the beforeMarshal event:

// /src/Controller/UsersController.php
// ...
public function add() {
  $user = $this->Users->newEntity();
  if ($this->request->is('post')) {
    // Use trim() here instead of beforeMarshal?
    $this->request->data['firstname'] = trim($this->request->data['firstname']);
    $user = $this->Users->patchEntity($user, $this->request->data );
    if ( $this->Users->save($user) ) {
      $this->Flash->succeed('Saved');
      return $this->redirect(['controller' => 'Users', 'action' => 'index']);
    } else {
      $this->Flash->error('Error');
    }
  }
  $this->set('user', $user);
}

This way the space will be removed even if there is a validation error. Or did I miss another function similar to beforeMarshal which is really modifying the request data?


Solution

  • The main purpose of beforeMarshal is to assist the users to pass the validation process when simple mistakes can be automatically resolved, or when data needs to be restructured so it can be put into the right columns.

    The beforMarshal event is triggered just at the start of the validation process, one of the reasons is that beforeMarshal is allowed to change the validation rules and the saving options, such as the field whitelist. Validation is triggered just after this event is finished.

    As documentation explains, if a field does not pass validation it will automatically removed from the data array and not be copied into the entity. This is to prevent having inconsistent data in the entity object.

    More over, the data in beforeMarshal is a copy of the request. This is because it is important to preserve the original user input, as it may be used elsewhere.

    If you need to trim your columns and display the result of the trimming to your user, I recommend doing it in the controller:

    $this->request->data = array_map(function ($d) {
        return is_string($d) ? trim($d) : $d;
    }, $this->request->data);