CakePHP, soumettre un formulaire en Ajax et retourner les erreurs de validations - Wenovio
1-844-WENOVIO English

CakePHP, soumettre un formulaire en Ajax et retourner les erreurs de validations

Lecture de 7 minutes

CakePHP, soumettre un formulaire en Ajax et retourner les erreurs de validations

Afin d’améliorer l’expérience utilisateur, de plus en plus d’applications utilisent la technologie Ajax, nous allons voir dans ce tutoriel une manière de soumettre un formulaire avec Cakephp sans recharger la page. De nombreux articles présentent déjà des solutions mais la plupart conseille de désactiver le composant CSRF ce qui est dommageable pour la sécurité de votre site. De plus nous allons aussi retourner les erreurs de validation directement dans le formulaire.

Installation

La version de CakePHP utilisée est la 3.2 On utilisera Bootstrap 3, Jquery et Jquery UI Afin d’utiliser facilement Bootstrap, on installe également l’extension cakephp3-bootstrap-helpers On ajoute donc la ligne "holt59/cakephp3-bootstrap-helpers": "dev-master" au fichier composer.json et on lance un ‘composer update’. Voici la partie ‘require’ de composer.json

"require": {
 "php": ">=5.5.9",
 "cakephp/cakephp": "~3.2",
 "mobiledetect/mobiledetectlib": "2.", "cakephp/migrations": "~1.0", "cakephp/plugin-installer": "",
 "holt59/cakephp3-bootstrap-helpers": "dev-master"
 }

Afin d’utiliser les helpers de cette extension, il faut ajouter dans /src/AppController.php

public $helpers = array(
 'Html' => [
 'className' => 'Bootstrap.BootstrapHtml'
 ],
 'Form' => [
 'className' => 'Bootstrap.BootstrapForm',
 ],
 'Paginator' => [
 'className' => 'Bootstrap.BootstrapPaginator'
 ],
 'Modal' => [
 'className' => 'Bootstrap.BootstrapModal'
 ],
 'Paginator'
 );

Bien sur dans config/bootstrap.php, on n’oublie pas d’ajouter

Plugin::load('Bootstrap');

Base de données

Concernant la base de données, on part sur une table ‘users’ Voici le code SQL si vous souhaitez l’installer

--
 -- Table structure for table users
 CREATE TABLE IF NOT EXISTS users (
 id int(11) NOT NULL,
 name varchar(255) NOT NULL,
 email varchar(255) NOT NULL,
 created datetime NOT NULL,
 modified datetime NOT NULL
 ) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=latin1;
 --
 -- Indexes for table users
 ALTER TABLE users
 ADD PRIMARY KEY (id), ADD KEY id (id);

Afin de créer le model, controller et les templates rapidement, on utilise en ligne de commande

$ bin/cake bake model users
$ bin/cake bake controller users
$ bin/cake bake template users

Fenêtre modale

Voyons comment ouvrir une fenêtre modale avec le formulaire d’ajout d’utilisateur à l’intérieur Dans /src/Template/Users/index.ctp, il faut ajouter une class sur le lien qui permet d’ajouter un utilisateur Ce lien doit charger l’action ‘call_modal’ du controller Users

<li><?= $this->Html->link(__('New User'), ['action' => 'call_modal'], ['class' => 'overlay-add-user']) ?></li>

Toujours dans le fichier index, tout à la fin on ajoute le code afin de créer la structure de la fenêtre modale

<div class="modal fade" id="dialogModalAddUsers" role="dialog">
    <div class="contentWrapAddUsers"></div>
</div>

Le contenu va être insérer en Javascript dans la div ‘contentWrapAddUsers’ On crée un fichier Javascript dans /webroot/js

// ------------
// Création modal Add Category
// ------------
$(document).on("click", ".overlay-add-user", function(event){ //(1)
 event.preventDefault();
 $('.contentWrapAddUsers').load($(this).attr("href")); //(2)
 $('#dialogModalAddUsers').modal('show'); //(3)
});

Explications:

  1. Detecte le click sur le lien qui a la class ‘overlay-add-user’
  2. Charge le contenu de la page dans la div contentWraloAddUsers
  3. Et affiche la fenêtre modale

Controller Users

Voici le contenu du UsersController.php

<?php
 namespace App\Controller;
 use App\Controller\AppController;
 use Cake\Event\Event;
 class UsersController extends AppController
 {
 public function initialize(){
 parent::initialize();
 $this->loadComponent('Security');
 $this->loadComponent('Csrf');
 }
 public function beforeFilter(Event $event){
 parent::beforeFilter($event);
 $this->Security->config('unlockedActions', ['add']);
 }
 public function index(){
 $users = $this->paginate($this->Users);
 $this->set(compact('users'));
 $this->set('_serialize', ['users']);
 }
 public function callModal(){
 $this->viewBuilder()->layout('ajax');
 if($this->request->is('ajax')){
 $user = $this->Users->newEntity();
 $this->set(compact('user'));
 $this->set('_serialize', ['user']);
 }
 }
 public function add(){
 $user = $this->Users->newEntity();
 $this->autoRender = false;
 if($this->request->is('ajax')){
 $user = $this->Users->patchEntity($user, $this->request->data);
 if ($this->Users->save($user)) {
 $response['status'] = 'success';
 $response['message'] = ("User Saved"); $response['data'] = $this->request->data; $response['id'] = $user->id; echo json_encode($response); } else { $errors = $user->errors(); $response['status'] = 'error'; $response['message'] = ("The User could not be saved. Please, try again.");
 $response['data'] = compact('errors');
 echo json_encode($response);
 }
 }
 }
?>

Explication:

$this->Security->config('unlockedActions', ['add']);

On désactive le component security pour l’action add puisqu’on va l’appeler avec une requête Ajax et ce composant va la bloquer. Voir Désactiver le Component Security pour des Actions Spécifiques

public function callModal(){
 $this->viewBuilder()->layout('ajax');
 if($this->request->is('ajax')){
 $user = $this->Users->newEntity();
 $this->set(compact('user'));
 $this->set('_serialize', ['user']);
 }
}

La méthode callModal va s’implement afficher le contenu de la fenètre modale avec le formulaire.

public function add(){
 $user = $this->Users->newEntity();
 $this->autoRender = false;
 if($this->request->is('ajax')){
 $user = $this->Users->patchEntity($user, $this->request->data);
 if ($this->Users->save($user)) { // (1)
 $response['status'] = 'success';
 $response['message'] = ("User Saved"); $response['data'] = $this->request->data; $response['id'] = $user->id; echo json_encode($response); } else { // (2) $errors = $user->errors(); $response['status'] = 'error'; $response['message'] = ("The User could not be saved. Please, try again.");
 $response['data'] = compact('errors');
 echo json_encode($response);
 }
 }
}

La méthode add va recevoir la requète du formulaire quand on va cliquer sur le bouton Submit Si la sauvegarde se passe correctement (1), une réponse sous la forme Json est renvoyé avec les données qui viennent d’être enregistrées ‘$response[‘data’]’ et l’id ‘$response[‘id’]’ de cet enregistrement. On renvoit aussi un status et un message qui seront traité par le javascript Si la sauvegarde échoue (2), on envoit aussi une réponse Json avec message, status et surtout les erreurs

$response['data'] = compact('errors');

Javascript

On reprend maintenant notre fichier javascript déjà créé, script_users.js

csrf_token = $("input[name='_csrfToken']").val();
 // ------------
 // Validation de la modal d'ajout de nouveau user
 // ------------
 $(document).on('click','#SubmitUserNew', function(e){
 var formSerialize = $('#formUserAdd').serialize();
 $.ajax({
 beforeSend: function(xhr) {
 xhr.setRequestHeader('X-CSRF-Token', csrf_token);
 $('#SubmitUserNew').text('Saving…');
 $('#SubmitUserNew').attr('disabled', true);
 $(".error-message").remove();
 $(".has-error").removeClass('has-error');
 },
 url: 'users/add/',
 data: formSerialize,
 type: "POST",
 dataType: "JSON",
 async: true,
 success: function (a){
 if (a.status === 'success'){
 $('#SubmitUserNew').text('Submit');
 $('#SubmitUserNew').attr('disabled', false);
 $('#dialogModalAddUsers').modal('hide');
 }
 if (a.status === 'error'){
 $('#SubmitUserNew').text('Submit');
 $('#SubmitUserNew').attr('disabled', false);
 $.each(a.data, function(model, errors) {
 for (var fieldName in this) {
 var element = $("[name='"+fieldName+"']");
 $.each(this[fieldName], function(label, text){
 text_error = text ;
 });
 var create = $(document.createElement('span')).insertAfter(element);
 create.addClass('error-message help-block').text(text_error);
 create.parent().addClass('has-error');
 }
 });
 }
 }
 });
});

Explications:

csrf_token = $("input[name='_csrfToken']").val();

On stocke le _csrfToken dans une variable afin de l’envoyer avec le formulaire. Sans ça, le requète sera bloqué par le component Csrf

$(document).on('click','#SubmitUserNew', function(e){
 var formSerialize = $('#formUserAdd').serialize();
 $.ajax({
 beforeSend: function(xhr) {
 xhr.setRequestHeader('X-CSRF-Token', csrf_token);
 $('#SubmitUserNew').text('Saving…');
 $('#SubmitUserNew').attr('disabled', true);
 $(".error-message").remove();
 $(".has-error").removeClass('has-error');
 },
 url: 'users/add/',
 data: formSerialize,
 type: "POST",
 dataType: "JSON",
 async: true,

Au click sur le bouton Submit, les valeurs du formulaire sont sérialisés puis envoyé en Ajax à l’action add du controller Users

xhr.setRequestHeader('X-CSRF-Token', csrf_token);

Permet d’envoyer le Csrf Token dans l’entête de la requète

success: function (a){
 if (a.status === 'success'){
 $('#SubmitUserNew').text('Submit');
 $('#SubmitUserNew').attr('disabled', false);
 $('#dialogModalAddUsers').modal('hide');
 }

Dans la variable [cci_javascript]a[/cci_javascript], on retrouve le Json renvoyé par la methode add() du controller Users. Si la réponse est un succès, on remet ‘Submit’ comme texte dans le bouton et on ferme la fenètre Modale

if (a.status === 'error'){
 $('#SubmitUserNew').text('Submit');
 $('#SubmitUserNew').attr('disabled', false);
 $.each(a.data, function(model, errors) {
 for (var fieldName in this) {
 var element = $("[name='"+fieldName+"']");
 $.each(this[fieldName], function(label, text){
 text_error = text ;
 });
 var create = $(document.createElement('span')).insertAfter(element);
 create.addClass('error-message help-block').text(text_error);
 create.parent().addClass('has-error');
 }
 });
 }
 }

Si la réponse est un échec, on récupère les erreurs de validation qui sont dans a.data C’est un tableau qui est comme ceci:

[
 'errors' => [
 'name' => [
 '_empty' => 'This field cannot be left empty'
 ],
 'email' => [
 '_empty' => 'This field cannot be left empty'
 ]
 ]
 ]

La boucle for permet de récupérer tous les ‘name’ des champs du formulaire (ici nous en avons deux: name et email), puis

$.each(this[fieldName], function(label, text){
 text_error = text ;
 });

permet de récupérer les messages d’erreurs (This field cannot be left empty) par exemple et de le mettre dans text_error.

var create = $(document.createElement('span')).insertAfter(element);
 create.addClass('error-message help-block').text(text_error);
 create.parent().addClass('has-error');

On crée ensuite un span dans lequel on met chaque élément avec la class ‘has_error’ histoire d’avoir les messages et les champs qui passent au rouge

Model

Pour info, voici le modèle utilisé: Userstable/php

namespace App\Model\Table;
 use App\Model\Entity\User;
 use Cake\ORM\Query;
 use Cake\ORM\RulesChecker;
 use Cake\ORM\Table;
 use Cake\Validation\Validator;
 /**
 Users Model
 *
 */
 class UsersTable extends Table
 { 
 /**
 Initialize method
 *
 @param array $config The configuration for the Table.
 @return void
 */
 public function initialize(array $config)
 {
 parent::initialize($config); 
 $this->table('users');
 $this->displayField('name');
 $this->primaryKey('id');
 $this->addBehavior('Timestamp');
 }
 /**
 Default validation rules.
 *
 @param \Cake\Validation\Validator $validator Validator instance.
 @return \Cake\Validation\Validator
 */
 public function validationDefault(Validator $validator)
 {
 $validator
 ->add('id', 'valid', ['rule' => 'numeric'])
 ->allowEmpty('id', 'create'); 
 $validator
 ->requirePresence('name', 'create')
 ->notEmpty('name');
 $validator
 ->email('email')
 ->requirePresence('email', 'create')
 ->notEmpty('email');
 return $validator;
 }
 /**
 Returns a rules checker object that will be used for validating
 application integrity.
 *
 @param \Cake\ORM\RulesChecker $rules The rules object to be modified.
 @return \Cake\ORM\RulesChecker
 */
 public function buildRules(RulesChecker $rules)
 {
 $rules->add($rules->isUnique(['email']));
 return $rules;
 }
 } 

Voici le contenu du fichier add.ctp

<nav id="actions-sidebar" class="large-3 medium-4 columns"></nav>
 <div class="users form large-9 medium-8 columns content"></div>
 <nav id="actions-sidebar" class="large-3 medium-4 columns"></nav>
 <div class="users form large-9 medium-8 columns content">
 <fieldset>
 <legend><!--?= __('Add User') ?--></legend></fieldset>
 </div>

Suite…

Dans le prochain tuto, nous verrons comment afficher un message de confirmation une fois qu’un utilisateur est bien enregistré.

3 Comments

  • Humbert

    thank you for your post, I have a question, where is the content of “contentWrapAddUsers” or add.ctp

    2017-11-01 à 21:17

Laisser un commentaire