Tutoriel Vidéo PHP Gestion d'un espace membre (refactorisation)

Télécharger la vidéo Télécharger les sources

Dans ce tutoriel je vous propose de faire un peu de refactoring du code écrit dans la vidéo précédente en plaçant les blocs de notre code dans des classes.

Pour cette vidéo, afin de la rendre la plus accessible possible, je n'utiliserai pas les namespaces ni composer. Mais si vous connaissez déjà ces technologies je vous invite évidemment à les utiliser pour une meilleur organisation.

Un objet database

Le premier objet que l'on va créer sera un objet pour gérer la connexion à la base de données afin d'effectuer les requêtes plus facilement.

<?php
class Database{

    private $pdo;

    public function __construct($login, $password, $database_name, $host = 'localhost'){
        $this->pdo = new PDO("mysql:dbname=$database_name;host=$host", $login, $password);
        $this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
        $this->pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_OBJ);
    }

    /**
     * @param $query
     * @param bool|array $params
     * @return PDOStatement
     */
    public function query($query, $params = false){
        if($params){
            $req = $this->pdo->prepare($query);
            $req->execute($params);
        }else{
            $req = $this->pdo->query($query);
        }
        return $req;
    }

    public function lastInsertId(){
        return $this->pdo->lastInsertId();
    }

}

Cette classe aura besoin d'être initialisé avec les identifiants de connexion à la base de données, ce qui peut s'avérer relativement pénible et surtout répétitif.

Un factory

Pour éviter de se répéter lors de l'initialisation de nos objets on va se créer une classe qui permettra de générer les instances dont on a besoin. Pour se simplifier, la tache on va utiliser un nom court App

<?php
class App{

    static $db = null;

    static function getDatabase(){
        if(!self::$db){
            self::$db = new Database('root', 'root', 'tuto');
        }
        return self::$db;
    }

}

Nous mettrons dans cette classe des méthodes qui permettront d'obtenir des instances de certaines classes qui nécessite une construction spécifique. Cela nous permettra d'éviter au maximum la répétition et da pouvoir modifier la configuration à un seul endroit.

Mort au require !

Quand on se décide à utiliser des classes un problème qui apparait assez rapidement est la multitude de fichiers créés et on se retrouve rapidement à faire 200 require. Heureusement on peut remédier au problèmes en utilisant un autoloader.

<?php
spl_autoload_register('app_autoload');

function app_autoload($class){
    require "class/$class.php";
}

Cette autoloader devra être inclut au début de l'exécution du script et se chargera d'inclure les fichiers lorsque l'on demandera à PHP d'accéder à une classe en particulier.

Rangeons nos functions

Dans notre application nous avons des fonctions qui ont une utilité assez simple comme str_random qui n'a comme objectif que de générer une chaîne aléatoire. Inutile de créer un objet instanciable pour cela car nous n'aurons pas besoin de variable d'instance. Ce que je vous conseille dans ce cas là c'est de créer des méthodes statiques qui permettront d'organiser les méthodes au sein d'un certain contexte. Par exemple une classe Str pour la manipulation des chaînes de caractères, une classe Arr pour la manipulation des tableau et ainsi de suite.

<?php
class Str{

    static function random($length){
        $alphabet = "0123456789azertyuiopqsdfghjklmwxcvbnAZERTYUIOPQSDFGHJKLMWXCVBN";
        return substr(str_shuffle(str_repeat($alphabet, $length)), 0, $length);
    }

}

La Session

Un autre problème que l'on avait dans la conception précédente c'était la session. En effet, on ne savait jamais si la session était démarré et on devait faire la vérification en permanence. Nous allons utiliser une classe pour interagir avec la session et en profiter pour mettre le démarrage dans le constructeur.

<?php
class Session{

    public function __construct(){
        session_start();
    }

}

Le problème c'est qu'à chaque instantiation, un session_start sera fait, ce qui va évidemment poser des problèmes. Pour remédier à ce souti nous allons utiliser le principe du singleton. C'est à dire qu'il ne sera pas possible d'avoir plusieurs instances de cette classe dans notre application. Pour cela on va créer une méthode statique getInstance() qui permettra d'instancier la classe et nous sauvegarderons l'instance dans une variable statique.

<?php
class Session{

    static $instance;

    static function getInstance(){
        if(!self::$instance){
            self::$instance = new Session();
        }
        return self::$instance;
    }

    public function __construct(){
        session_start();
    }

}

Du coup dès qu'on aura besoin d'interagir avec la session il nous suffira d'obtenir une instance en faisant Session::getInstance(). Nous pouvons donc créer les méthodes qui remplaceront le code écrit en dur dans notre application pour interagir avec la session.

<?php
class Session{

    static $instance;

    static function getInstance(){
        if(!self::$instance){
            self::$instance = new Session();
        }
        return self::$instance;
    }

    public function __construct(){
        session_start();
    }

    public function setFlash($key, $message){
        $_SESSION['flash'][$key] = $message;
    }

    public function hasFlashes(){
        return isset($_SESSION['flash']);
    }

    public function getFlashes(){
        $flash = $_SESSION['flash'];
        unset($_SESSION['flash']);
        return $flash;
    }

    public function write($key, $value){
        $_SESSION[$key] = $value;
    }

    public function read($key){
        return isset($_SESSION[$key]) ? $_SESSION[$key] : null;
    }

    public function delete($key){
        unset($_SESSION[$key]);
    }

}

La classe Auth

Je ne vais pas vous copier toutes les méthodes ici car cela serait trop long donc on va se concentrer sur la méthode login. Cette méthode prendra donc 2 arguments, $username et $password et connectera l'utilisateur si les identifiants sont valides et retournera false sinon. Le problème c'est que cette classe va avoir besoin de plusieurs dépendances.

  • La session, pour vérifier si l'utilisateur est connecté ou pas, et même pour initialiser l'utilisateur,
  • La base de donnée pour vérifier les utilisateurs mais aussi pour en créer une nouveau et le stocker dans la base de données.

Lorsqu'il s'agit d'utiliser d'autres classes ce qu'il faut éviter à tout prix c'est de rendre les classes dépendantes les unes des autres en mettant des instanciations dans les méthodes. On préfèrera 'injecter' les dépendances dans le constructeur ou les méthodes.

La session sera utilisée par toutes les méthodes de notre class Auth, on l'injectera donc au niveau du constructeur. En revanche la connexion à la base de données ne sera pas forcément utile pour certaines méthodes, on l'injectera donc au cas par cas.

<?php
class Auth{

    private $session;

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

    public function connect($user){
        $this->session->write('auth', $user);
    }

    public function login($db, $username, $password){
        $user = $db->query('SELECT * FROM users WHERE (username = :username OR email = :username) AND confirmed_at IS NOT NULL', ['username' => $username])->fetch();
        if(password_verify($password, $user->password)){
            $this->connect($user);
            return $user;
        }else{
            return false;
        }
    }

    public function restrict(){
        if(!$this->session->read('auth')){
            $this->session->setFlash('danger', "Vous n'avez pas le droit d'accéder à cette page");
            header('Location: login.php');
            exit();
        }
    }
}

On voit donc que notre fonction login prend en premier paramètre l'objet Database créé précédemment et l'utilise donc pour vérifier si un utilisateur correspond aux identifiants qui ont été entrés. On sépare le code de connexion dans une méthode séparée car on va en avoir besoin dans d'autre méthodes (comme lors de l'étape de confirmation du compte par email).

Il ne vous reste plus qu'à modifier cette classe pour y intégrer le reste des méthodes que l'on avait mis en place dans le tutoriel précédent.

Une classe Validator

Un autre problème que l'on avait dans le code précédent c'était la partie validation des données qui entrainé beaucoup de répétition. On va donc se créer une classe pour gérer ça plus proprement. Comme souvent avec la programmation il n'y a pas qu'une solution pour résoudre le problème. Certains préfèreront créer une classe qui prend les règles de validation en paramètres, d'autres préfèreront créer des méthodes pour chaques méthodes. Il n'y a pas de bonne façon et de mauvaise façon de faire, l'important c'est de prendre l'approche qui vous semble la plus naturelle.

Notre classe fonctionnera de la manière suivante :

  • Les données à valider seront envoyées au moment de la construction de l'objet
  • Chaque validation se fera dans une méthode séparée
  • Les erreurs seront stockées si on souhaite valider plusieurs champs à la suite

Pour l'exemple nous ne mettrons en place que la validation de l'email. Je vous laisserais faire le reste.

<?php

class Validator
{

    private $data;
    private $errors = [];

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

    private function getField($field)
    {
        if (!isset($this->data[$field])) {
            return null;
        }
        return $this->data[$field];
    }

    public function isEmail($field, $errorMsg = false)
    {
        if (!filter_var($this->getField($field), FILTER_VALIDATE_EMAIL)) {
            $this->errors[$field] = $errorMsg;
            return false;
        }
        return true;
    }

    public function isValid()
    {
        return empty($this->errors);
    }

    public function getErrors()
    {
        return $this->errors;
    }

}

On se crée au début de la classe une méthode getField() qui permet simplement de vérifier si une index existe afin d'éviter de répéter un isset() dans chacune des méthode de notre validator. La méthode isEmail() va donc retourner false en cas d'erreur et true sinon, mais aussi stocker l'erreur dans la variable d'instance $errors ceci afin de permettre une validation multiple par exemple :

    $db = App::getDatabase();
    $validator = new Validator($_POST);
    if($validator->isAlpha('username', "Votre pseudo n'est pas valide (alphanumérique)")){
        $validator->isUniq('username', $db, 'users', 'Ce pseudo est déjà pris');
    }
    if($validator->isEmail('email', "Votre email n'est pas valide")){
        $validator->isUniq('email', $db, 'users', 'Cet email est déjà utilisé pour un autre compte');
    }
    $validator->isConfirmed('password', 'Vous devez rentrer un mot de passe valide');

    if($validator->isValid()){
        // On peut créer l'utilisateur
    } else {
        // On stocke les erreurs pour les envoyer dans le code HTML
        $errors = $validator->getErrors();
    }

Conclusion

Comme vous le voyez les objets permettent de beaucoup mieux s'organiser en groupant le code ayant attrait à un même concept à un seul endroit. L'autoloader permettant d'alléger grandement la tâche en se chargeant de la partie require et nous permet de séparer notre code sans forcément avoir à mettre 3 tonnes de require dans notre code. Enfin, on peut utiliser une classe sous forme de Factory afin de se charger du processus d'instanciation de nos différentes classes. En effet, en ne voulant pas rendre les classes trop dépendantes les unes des autres on va rapidement se retrouver avec des constructeur relativement chiant à utiliser.

Enfin, nous n'avons ici que très peu modifier le code en réorganisant certains blocs au sein de méthodes mais il reste beaucoup de travail à faire, voici quelques pistes :

  • Mettre en place composer pour pouvoir charger des librairies externes
  • Essayer de séparer le code PHP du code HTML
  • Créer un Routeur afin d'améliorer la séparation du code mais aussi pour pouvoir avoir de plus belles URLs
  • Peut être commencer à regarder ce qui se fait ailleurs en se penchant sur un framework ?