21. La validation

Il est temps de passer à la validation de nos données. Attention à bien suivre dans ce chapitre, ce sera sans aucun doute l'un des plus complexes de ce cours.

 

La notion de contrainte de validation

Dans notre application, voici les règles que l'on va mettre en place concernant nos articles :

- notre champ  titre  de notre formulaire ne doit pas être vide, et doit contenir un minimum de 2 caractères ainsi qu'un maximum de 255 caractères 
- notre champ  contenu  de notre formulaire ne doit pas être vide, et contenir un minimum de 2 caractères
- notre champ  auteur  aura les mêmes caractéristiques que notre champ  titre

Attention, c'est bien à vous de définir vos propres règles dans votre application selon ce que vous souhaitez avoir ! Celles-ci varient d'une application à l'autre, alors soyez vigilants 😉

Les contraintes de validation vont donc nous permettre de nous assurer que l'on respecte bien les règles que l'on vient  de mettre en place.

Et donc d'éviter d'insérer un article avec les champs vierges ? 😅

Vous avez bien compris 😁

 

Mise en place de la validation

Il est temps de mettre en place nos contraintes de validation pour valider nos données. Ainsi, nos articles pourront donc être insérés dans la base de données avec des données valides.

Pour cela, on va créer : 

- une classe validation (qui sera appelée par notre contrôleur et qui prendra deux paramètres (les données à valider, et le nom de la classe que l'on veut valider)

- une classe ArticleValidation (qui devra valider que les données d'un article sont conformes)

- une classe Constraint (qui contiendra nos contraintes de validation).

Ces trois classes seront à mettre dans le dossier constraint (dans src).

 

On va aussi en profiter pour modifier notre classe Parameter, en ajoutant une méthode all qui nous permettra de récupérer tous nos paramètres : 

<?php

namespace App\config;

class Parameter
{
    private $parameter;

    public function __construct($parameter)
    {
        $this->parameter = $parameter;
    }

    public function get($name)
    {
        if(isset($this->parameter[$name]))
        {
            return $this->parameter[$name];
        }
    }

    public function set($name, $value)
    {
        $this->parameter[$name] = $value;
    }
    
    public function all()
    {
        return $this->parameter;
    }

}

Cette méthode va nous permettre de récuérer toutes les données saisies, vous en verrez l'utilité d'ici peu.

Commençons par notre classe Constraint :

<?php

namespace App\src\constraint;

class Constraint
{
    public function notBlank($name, $value)
    {
        if(empty($value)) {
            return '<p>Le champ '.$name.' saisi est vide</p>';
        }
    }
    public function minLength($name, $value, $minSize)
    {
        if(strlen($value) < $minSize) {
            return '<p>Le champ '.$name.' doit contenir au moins '.$minSize.' caractères</p>';
        }
    }
    public function maxLength($name, $value, $maxSize)
    {
        if(strlen($value) > $maxSize) {
            return '<p>Le champ '.$name.' doit contenir au maximum '.$maxSize.' caractères</p>';
        }
    }
}

Ici, rien de bien compliqué, nous avons juste trois méthodes, qui correspondent à trois contraintes et renvoient un message si les données que l'on reçoit sont incorrectes.

 

Voici notre classe ArticleValidation : 

<?php

namespace App\src\constraint;
use App\config\Parameter;

class ArticleValidation extends Validation
{
    private $errors = [];
    private $constraint;

    public function __construct()
    {
        $this->constraint = new Constraint();
    }

    public function check(Parameter $post)
    {
        foreach ($post->all() as $key => $value) {
            $this->checkField($key, $value);
        }
        return $this->errors;
    }

    private function checkField($name, $value)
    {
        if($name === 'title') {
            $error = $this->checkTitle($name, $value);
            $this->addError($name, $error);
        }
        elseif ($name === 'content') {
            $error = $this->checkContent($name, $value);
            $this->addError($name, $error);
        }
        elseif($name === 'author') {
            $error = $this->checkAuthor($name, $value);
            $this->addError($name, $error);
        }
    }

    private function addError($name, $error) {
        if($error) {
            $this->errors += [
                $name => $error
            ];
        }
    }

    private function checkTitle($name, $value)
    {
        if($this->constraint->notBlank($name, $value)) {
            return $this->constraint->notBlank('titre', $value);
        }
        if($this->constraint->minLength($name, $value, 2)) {
            return $this->constraint->minLength('titre', $value, 2);
        }
        if($this->constraint->maxLength($name, $value, 255)) {
            return $this->constraint->maxLength('titre', $value, 255);
        }
    }

    private function checkContent($name, $value)
    {
        if($this->constraint->notBlank($name, $value)) {
            return $this->constraint->notBlank('contenu', $value);
        }
        if($this->constraint->minLength($name, $value, 2)) {
            return $this->constraint->minLength('contenu', $value, 2);
        }
    }

    private function checkAuthor($name, $value)
    {
        if($this->constraint->notBlank($name, $value)) {
            return $this->constraint->notBlank('auteur', $value);
        }
        if($this->constraint->minLength($name, $value, 2)) {
            return $this->constraint->minLength('auteur', $value, 2);
        }
        if($this->constraint->maxLength($name, $value, 255)) {
            return $this->constraint->maxLength('auteur', $value, 255);
        }
    }
}

On a ici un peu plus de méthodes : 

- la méthode construct, qui va créer un nouvel objet basé sur la classe Contraint (créée précédemment)

- la méthode check (qui va récupérer toutes les données de la classe Parameter (via la méthode all crée juste avant), et fait appel à la méthode checkField

- la méthode checkField, qui va vérifier chaque champ 

- la méthode addError, qui ajoutera une erreur si un des champs n'est pas valide

- les méthodes checkTitle, checkContent et checkAuthor qui vont faire appels aux différentes contraines créées.

 

Voici la dernière classe créée, la classe Validation : 

<?php

namespace App\src\constraint;

class Validation
{
    public function validate($data, $name)
    {
        if($name === 'Article') {
            $articleValidation = new ArticleValidation();
            $errors = $articleValidation->check($data);
            return $errors;
        }
    }
}

Ici, rien de compliqué, la seule méthode existante est validate. Ce sera cette méthode qui sera appelée depuis notre contrôleur et renverra vers la classe ArticleValidation si c'est un article que l'on veut valider. Cette classe sera complétée lors de la gestion de nos commentaires.

 

Connectons maintenant tout ceci à notre application 😉

Commençons par faire appel à notre classe Validation depuis notre classe Controller : 

<?php

namespace App\src\controller;

use App\config\Request;
use App\src\constraint\Validation;
use App\src\DAO\ArticleDAO;
use App\src\DAO\CommentDAO;
use App\src\model\View;

abstract class Controller
{
    protected $articleDAO;
    protected $commentDAO;
    protected $view;
    private $request;
    protected $get;
    protected $post;
    protected $session;
    protected $validation;

    public function __construct()
    {
        $this->articleDAO = new ArticleDAO();
        $this->commentDAO = new CommentDAO();
        $this->view = new View();
        $this->validation = new Validation();
        $this->request = new Request();
        $this->get = $this->request->getGet();
        $this->post = $this->request->getPost();
        $this->session = $this->request->getSession();
    }
}

 

Faisons maintenant appel à notre méthode validate lors de la soumission de nos données (pour notre méthode addArticle) : 

<?php

namespace App\src\controller;

use App\config\Parameter;

class BackController extends Controller
{
    public function addArticle(Parameter $post)
    {
        if($post->get('submit')) {
            $errors = $this->validation->validate($post, 'Article');
            if(!$errors) {
                $this->articleDAO->addArticle($post);
                $this->session->set('add_article', 'Le nouvel article a bien été ajouté');
                header('Location: ../public/index.php');
            }
            return $this->view->render('add_article', [
                'post' => $post,
                'errors' => $errors
            ]);
        }
        return $this->view->render('add_article');
    }

    public function editArticle(Parameter $post, $articleId)
    {
        $article = $this->articleDAO->getArticle($articleId);
        if($post->get('submit')) {
            $this->articleDAO->editArticle($post, $articleId);
            $this->session->set('edit_article', 'L\' article a bien été modifié');
            header('Location: ../public/index.php');
        }
        return $this->view->render('edit_article', [
            'article' => $article
        ]);
    }
}

Pensez à afficher les éventuels messages d'erreurs sur votre vue add_article : 

<?php
$route = isset($post) && $post->get('id') ? 'editArticle&articleId='.$post->get('id') : 'addArticle';
$submit = $route === 'addArticle' ? 'Envoyer' : 'Mettre à jour';
?>
<form method="post" action="../public/index.php?route=<?= $route; ?>">
    <label for="title">Titre</label><br>
    <input type="text" id="title" name="title" value="<?= isset($post) ? htmlspecialchars($post->get('title')): ''; ?>"><br>
    <?= isset($errors['title']) ? $errors['title'] : ''; ?>
    <label for="content">Contenu</label><br>
    <textarea id="content" name="content"><?= isset($post) ? htmlspecialchars($post->get('content')): ''; ?></textarea><br>
    <?= isset($errors['content']) ? $errors['content'] : ''; ?>
    <label for="author">Auteur</label><br>
    <input type="text" id="author" name="author" value="<?= isset($post) ? htmlspecialchars($post->get('author')): ''; ?>"><br>
    <?= isset($errors['author']) ? $errors['author'] : ''; ?>
    <input type="submit" value="<?= $submit; ?>" id="submit" name="submit">
</form>

On a adapté les données dans notres vues :

- notre méthode addArticle renvoie en cas d'erreurs deux variables :  $post, qui est le contenu saisi par notre utilisateur, et $errors, qui sont les éventuelles erreurs remontées par notre validateur.

- notre vue affiche maintenant les données saisies par l'utilisateur dans chaque champ, et un message d'erreur si un champ est invalide. Attention, la condition pour obtenir la route a bien été adaptée aussi.

Il ne nous reste plus qu'à faire la même chose pour notre méthode editArticle.

Je vous laisse essayer, je vous mets le code en-dessous si vous bloquez : 

Voici le fichier BackController actualisé :

<?php

namespace App\src\controller;

use App\config\Parameter;

class BackController extends Controller
{
    public function addArticle(Parameter $post)
    {
        if($post->get('submit')) {
            $errors = $this->validation->validate($post, 'Article');
            if(!$errors) {
                $this->articleDAO->addArticle($post);
                $this->session->set('add_article', 'Le nouvel article a bien été ajouté');
                header('Location: ../public/index.php');
            }
            return $this->view->render('add_article', [
                'post' => $post,
                'errors' => $errors
            ]);
        }
        return $this->view->render('add_article');
    }

    public function editArticle(Parameter $post, $articleId)
    {
        $article = $this->articleDAO->getArticle($articleId);
        if($post->get('submit')) {
            $errors = $this->validation->validate($post, 'Article');
            if(!$errors) {
                $this->articleDAO->editArticle($post, $articleId);
                $this->session->set('edit_article', 'L\' article a bien été modifié');
                header('Location: ../public/index.php');
            }
            return $this->view->render('edit_article', [
                'post' => $post,
                'errors' => $errors
            ]);

        }
        $post->set('id', $article->getId());
        $post->set('title', $article->getTitle());
        $post->set('content', $article->getContent());
        $post->set('author', $article->getAuthor());

        return $this->view->render('edit_article', [
            'post' => $post
        ]);
    }
}

 

N'hésitez pas à relire ce chapitre pour bien comprendre toutes les subtilités mises en place ainsi que le fonctionnement de chaque classe.

 

En savoir plus sur les contraintes

Je vous en dis quand même un peu plus concernant les contraintes, voici quelques exemples de contraintes existantes :
NotBlank(pour éviter de recevoir des données vides)
Email(pour vérifier qu'un email soit valide)
Range (pour comparer des valeurs numériques)
... 

La liste est longue, voici un exemple des contraintes présentes dans le framework Symfony :
Liste des contraintes

La liste est... longue 😱

Bien évidemment, vous pouvez mettre en place vos propres contraintes.

 

Il est temps de passer à la suppression d'un article existant 😄