5. Un peu de refactorisation

Petit topo

Il serait maintenant temps de refactoriser le code de notre classe  Article  qui se répète légèrement.

La refactorisation consiste à réécrire son code d'une manière différente pour qu'il soit plus facile à lire, à maintenir et à faire évoluer.

Vous devez éviter au maximum de répéter votre code.

 

L'existant

Nous avons actuellement la classe  Database  qui a la structure suivante :

<?php

class Database
{
    //Nos constantes
    const DB_HOST = 'mysql:host=localhost;dbname=blog;charset=utf8';
    const DB_USER = 'root';
    const DB_PASS = 'root';

    //Méthode de connexion à notre base de données
    public function getConnection()
    {
        //Tentative de connexion à la base de données
        try{
            $connection = new PDO(self::DB_HOST, self::DB_USER, self::DB_PASS);
            $connection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
            //On renvoie la connexion
            return $connection;
        }
        //On lève une erreur si la connexion échoue
        catch(Exception $errorConnection)
        {
            die ('Erreur de connection :'.$errorConnection->getMessage());
        }

    }
}

La classe  Article  a la structure suivante :

<?php

class Article
{
    public function getArticles()
    {
        $db = new Database();
        $connection = $db->getConnection();
        $result = $connection->query('SELECT id, title, content, author, createdAt FROM article ORDER BY id DESC');
        return $result;
    }

    public function getArticle($articleId)
    {
        $db = new Database();
        $connection = $db->getConnection();
        $result = $connection->prepare('SELECT id, title, content, author, createdAt FROM article WHERE id = ?');
        $result->execute([
            $articleId
        ]);
        return $result;
    }
}

Il y a ici quelques problèmes :
- dans la classe  Database  , la méthode  getConnection()  est en public et peut être appelée depuis n'importe où.

- dans la classe  Article  , un objet $db est instancié à chaque méthode, et la méthode  getConnection()  est répétée.

Nous allons corriger ces incohérences dès maintenant 😄

 

La classe Database

 On va effectuer quelques modifications :
- modifier la méthode  getConnection  en private : pour récupérer la connexion à la base de données uniquement depuis notre classe

- créer une méthode  createQuery  , qui fera appel à notre méthode  getConnection  et va gérer nos requêtes

On commence par créer une méthode  createQuery  , qui va gérer nos requêtes :

<?php

class Database
{
    //Nos constantes
    const DB_HOST = 'mysql:host=localhost;dbname=blog;charset=utf8';
    const DB_USER = 'root';
    const DB_PASS = 'root';

    //Méthode de connexion à notre base de données
    public function getConnection()
    {
        //Tentative de connexion à la base de données
        try{
            $connection = new PDO(self::DB_HOST, self::DB_USER, self::DB_PASS);
            $connection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
            //On renvoie la connexion
            return $connection;
        }
        //On lève une erreur si la connexion échoue
        catch(Exception $errorConnection)
        {
            die ('Erreur de connection :'.$errorConnection->getMessage());
        }

    }

    protected function createQuery($sql, $parameters = null)
    {
        if($parameters)
        {
            $result = $this->getConnection()->prepare($sql);
            $result->execute($parameters);
            return $result;
        }
        $result = $this->getConnection()->query($sql);
        return $result;
    }
}
Je n'ai pas compris, tu peux nous expliquer ce que tu veux faire ici ? 😒

On vient de créer une nouvelle méthode, appelée  createQuery  , qui prend deux paramètres, une requête sql et des paramètres. Ces derniers par défaut sont null, étant donné qu'une requête  query  n'a pas besoin de paramètres particuliers. En revanche, pour une requête  prepare  , on a besoin de lui préciser les paramètres en question. On modifiera en conséquence notre classe  Article .

On va maintenant s'occuper d'un problème de répétition de la connexion à la base de données.

En quoi cela pose problème alors ? 😅

Lorsqu'une de vos pages fait appel à une seule méthode, pas de problème. Mais si votre page doit faire appel à plusieurs méthodes, et que ces dernières contiennent chacune au moins une requête, on risque d'ouvrir une connexion à chaque requête. Ce serait quand même mieux d'en faire une seule non ?

Oui mais à l'heure actuelle aucune de nos pages ne fait appel à plusieurs requêtes ? 😌

Tout juste, mais un bon développeur doit être en mesure d'anticiper d'éventuels problèmes. Trêve de bavardages, nous avons du travail, nous allons :

- ajouter une propriété  $connection  qui va stocker la connexion s'il y en a une 

- ajouter une méthode  checkConnection()  qui va vérifier si une connexion est présente ou non

Voici notre classe Database actualisée :

<?php

class Database
{
    //Nos constantes
    const DB_HOST = 'mysql:host=localhost;dbname=blog;charset=utf8';
    const DB_USER = 'root';
    const DB_PASS = 'root';

    private $connection;

    private function checkConnection()
    {
        //Vérifie si la connexion est nulle et fait appel à getConnection()
        if($this->connection === null) {
            return $this->getConnection();
        }
        //Si la connexion existe, elle est renvoyée, inutile de refaire une connexion
        return $this->connection;
    }

    //Méthode de connexion à notre base de données
    public function getConnection()
    {
        //Tentative de connexion à la base de données
        try{
            $this->connection = new PDO(self::DB_HOST, self::DB_USER, self::DB_PASS);
            $this->connection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
            //On renvoie la connexion
            return $this->connection;
        }
        //On lève une erreur si la connexion échoue
        catch(Exception $errorConnection)
        {
            die ('Erreur de connection :'.$errorConnection->getMessage());
        }

    }

    protected function createQuery($sql, $parameters = null)
    {
        if($parameters)
        {
            $result = $this->checkConnection()->prepare($sql);
            $result->execute($parameters);
            return $result;
        }
        $result = $this->checkConnection()->query($sql);
        return $result;
    }
}

Quelques explications :
- l'attribut  $connection  stocke la connexion si celle-ci existe, sinon renvoie  null
- la méthode  checkConnection()  teste si  $connection  est  null  , et appelle  getConnection()  pour créer une nouvelle connexion. Si  $connection  a une connexion existante, la méthode renvoie celle-ci ;
- la méthode  getConnection()  fait la même chose que précédemment, mais renvoie la connexion dans la propriété  $connection
- la méthode  createQuery()  a été modifiée, pour vérifier si la connexion existe avant d'en faire une nouvelle au besoin.

Comment je sais si la méthode checkConnection() fonctionne bien ?

Il vous suffit de modifier la méthode  checkConnection()  en  public  et d'ajouter deux fois la fonction  var_dump()  comme ici :

<?php

public function checkConnection()
{
    //Vérifie si la connexion est nulle et fait appel à getConnection
    if($this->connection == null){
        var_dump('connexion inconnue');
        return $this->getConnection();
    }
    //Si la connexion existe, elle est renvoyée, inutile de refaire une connexion
    var_dump('connexion déjà existante');
    return $this->connection;
}

Et de faire appel à celle-ci plusieurs fois dans home.php  , comme ici :

<?php
//On inclut le fichier dont on a besoin (ici à la racine de notre site)
require 'Database.php';
//Ne pas oublier d'ajouter le fichier Article.php
require 'Article.php';
?>
<!DOCTYPE html>
<html lang="fr">
<head>
    <meta charset="utf-8">
    <title>Mon blog</title>
</head>

<body>
<div>
    <h1>Mon blog</h1>
    <p>En construction</p>
    <?php
    $db = new Database();
    $db->checkConnection();
    $article = new Article();
    $articles = $article->getArticles();
    while($article = $articles->fetch())
    {
        ?>
        <div>
            <h2><a href="single.php?articleId=<?= htmlspecialchars($article['id']);?>"><?= htmlspecialchars($article['title']);?></a></h2>
            <p><?= htmlspecialchars($article['content']);?></p>
            <p><?= htmlspecialchars($article['author']);?></p>
            <p>Créé le : <?= htmlspecialchars($article['createdAt']);?></p>
        </div>
        <br>
        <?php
    }
    $articles->closeCursor();
    $db->checkConnection();
    ?>
</div>
</body>
</html>

La page devrait vous afficher :


 

Cela fonctionne parfaitement 😉

Pensez à repasser la méthode  checkConnection()  en private et retirer les  var_dump()  dans la classe  Database  , ainsi que les deux appels à la méthode checkConnection() dans la page  home.php  ainsi que l'instanciation de l'objet $db.

On peut maintenant passer notre méthode  getConnection() en  private  , pour qu'elle ne soit appelée que depuis la classe  Database  .

On va aussi passer la classe  Database  en classe abstraite, pour qu'on ne puisse plus l'instancier. Voici le résultat :

<?php

abstract class Database
{
    //Nos constantes
    const DB_HOST = 'mysql:host=localhost;dbname=blog;charset=utf8';
    const DB_USER = 'root';
    const DB_PASS = 'root';

    private $connection;

    private function checkConnection()
    {
        //Vérifie si la connexion est nulle et fait appel à getConnection()
        if($this->connection === null) {
            return $this->getConnection();
        }
        //Si la connexion existe, elle est renvoyée, inutile de refaire une connexion
        return $this->connection;
    }

    //Méthode de connexion à notre base de données
    private function getConnection()
    {
        //Tentative de connexion à la base de données
        try{
            $this->connection = new PDO(self::DB_HOST, self::DB_USER, self::DB_PASS);
            $this->connection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
            //On renvoie la connexion
            return $this->connection;
        }
        //On lève une erreur si la connexion échoue
        catch(Exception $errorConnection)
        {
            die ('Erreur de connection :'.$errorConnection->getMessage());
        }

    }

    protected function createQuery($sql, $parameters = null)
    {
        if($parameters)
        {
            $result = $this->checkConnection()->prepare($sql);
            $result->execute($parameters);
            return $result;
        }
        $result = $this->checkConnection()->query($sql);
        return $result;
    }
}
 Plus rien ne marche quand j'actualise mon navigateur web, c'est la catastrophe !  😢

C'est normal, il faut modifier la classe  Article  en conséquence.

Allons-y  😁

 

La classe Article

 On ne peut plus utiliser la classe en l'état, parce que les méthodes actuelles ne fonctionnent plus. Voici la classe  Article  , pour rappel :

<?php

class Article
{
    public function getArticles()
    {
        $db = new Database();
        $connection = $db->getConnection();
        $result = $connection->query('SELECT id, title, content, author, createdAt FROM article ORDER BY id DESC');
        return $result;
    }

    public function getArticle($articleId)
    {
        $db = new Database();
        $connection = $db->getConnection();
        $result = $connection->prepare('SELECT id, title, content, author, createdAt FROM article WHERE id = ?');
        $result->execute([
            $articleId
        ]);
        return $result;
    }
}

Nous devons donc modifier notre classe  Article  : 

<?php

class Article extends Database
{
    public function getArticles()
    {
        $sql = 'SELECT id, title, content, author, createdAt FROM article ORDER BY id DESC';
        return $this->createQuery($sql);
    }

    public function getArticle($articleId)
    {
        $sql = 'SELECT id, title, content, author, createdAt FROM article WHERE id = ?';
        return $this->createQuery($sql, [$articleId]);
    }
}

 Quelques explications : 
- on vient d'étendre la classe  Article  avec le mot clé  extends .
- les méthodes  getArticles  et  getArticle  ont été simplifiées, en faisant appel à la méthode  createQuery  créée précédemment.

Si vous actualisez votre page web, le résultat est le même.

Pourquoi nous avoir fait faire tout ça ? grrr  😠

Parce que les futures classes que nous allons créer vont suivre le même fonctionnement.

On va pouvoir gérer notre future classe Comment de la même façon ?

Exactement  😅

Finissons de refactoriser notre application en travaillant avec des... objets 😆

Non mais oh, tu te fiches de nous là ? C'est pas ce qu'on fait depuis le début ?😡

Suivez le guide 👲

 

Travailler avec des objets

On vient tout juste de refactoriser nos classes, mais au niveau de nos vues, ce serait bien de travailler avec des objets pour afficher nos articles, plutôt qu'avec des tableaux. Voyez-vous même ici le fichier home.php

<?php
//On inclut le fichier dont on a besoin (ici à la racine de notre site)
require 'Database.php';
//Ne pas oublier d'ajouter le fichier Article.php
require 'Article.php';
?>
<!DOCTYPE html>
<html lang="fr">
<head>
    <meta charset="utf-8">
    <title>Mon blog</title>
</head>

<body>
<div>
    <h1>Mon blog</h1>
    <p>En construction</p>
    <?php
    $article = new Article();
    $articles = $article->getArticles();
    while($article = $articles->fetch())
    {
        ?>
        <div>
            <h2><a href="single.php?articleId=<?= htmlspecialchars($article['id']);?>"><?= htmlspecialchars($article['title']);?></a></h2>
            <p><?= htmlspecialchars($article['content']);?></p>
            <p><?= htmlspecialchars($article['author']);?></p>
            <p>Créé le : <?= htmlspecialchars($article['createdAt']);?></p>
        </div>
        <br>
        <?php
    }
    $articles->closeCursor();
    ?>
</div>
</body>
</html>
Est-ce qu'on peut avoir un fonctionnement "objet" plutôt que d'utiliser les tableaux ?

Bien sûr, et c'est ce qu'on va faire.

Commençons par modifier notre classe ... Database. Notre classe Database nous renvoie actuellement les données sous forme de tableaux, c'est le comportement par défaut de PDO lorsqu'on ne lui précise pas ce qu'on souhaite. 

Voici notre classe Database actualisée : 

<?php

abstract class Database
{
    //Nos constantes
    const DB_HOST = 'mysql:host=localhost;dbname=blog;charset=utf8';
    const DB_USER = 'root';
    const DB_PASS = 'root';

    private $connection;

    private function checkConnection()
    {
        //Vérifie si la connexion est nulle et fait appel à getConnection()
        if($this->connection === null) {
            return $this->getConnection();
        }
        //Si la connexion existe, elle est renvoyée, inutile de refaire une connexion
        return $this->connection;
    }

    //Méthode de connexion à notre base de données
    private function getConnection()
    {
        //Tentative de connexion à la base de données
        try{
            $this->connection = new PDO(self::DB_HOST, self::DB_USER, self::DB_PASS);
            $this->connection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
            //On renvoie la connexion
            return $this->connection;
        }
        //On lève une erreur si la connexion échoue
        catch(Exception $errorConnection)
        {
            die ('Erreur de connection :'.$errorConnection->getMessage());
        }

    }

    protected function createQuery($sql, $parameters = null)
    {
        if($parameters)
        {
            $result = $this->checkConnection()->prepare($sql);
            $result->setFetchMode(PDO::FETCH_CLASS, Article::class);
            $result->execute($parameters);
            return $result;
        }
        $result = $this->checkConnection()->query($sql);
        $result->setFetchMode(PDO::FETCH_CLASS, Article::class);
        return $result;
    }
}

Ici, on a ajouté la méthode SetFetchMode de PDO, en lui passant en premier paramètre le type (PDO::FETCH_CLASS), et en deuxième paramètre le nom de la classe. J'ai ici indiqué la classe Article, mais on peut faire encore mieux pour rendre cela réutilisable. On va lui passer le nom de la classe dynamiquement, comme ça nos futures classes pourront aussi utiliser cette même méthode 😉

Pour passer le nom de la classe qui a appelée la méthode dynamiquement, on peut utiliser la fonction get_called_class de PHP. Si vous regardez dans la documentation, on peut même remplacer cette fonction par static::class

Pour ceux qui utilisent PHPStorm, c'est aussi ce que vous conseille de faire l'IDE.

Voici notre classe Database actualisée : 

<?php

abstract class Database
{
    //Nos constantes
    const DB_HOST = 'mysql:host=localhost;dbname=blog;charset=utf8';
    const DB_USER = 'root';
    const DB_PASS = 'root';

    private $connection;

    private function checkConnection()
    {
        //Vérifie si la connexion est nulle et fait appel à getConnection()
        if($this->connection === null) {
            return $this->getConnection();
        }
        //Si la connexion existe, elle est renvoyée, inutile de refaire une connexion
        return $this->connection;
    }

    //Méthode de connexion à notre base de données
    private function getConnection()
    {
        //Tentative de connexion à la base de données
        try{
            $this->connection = new PDO(self::DB_HOST, self::DB_USER, self::DB_PASS);
            $this->connection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
            //On renvoie la connexion
            return $this->connection;
        }
        //On lève une erreur si la connexion échoue
        catch(Exception $errorConnection)
        {
            die ('Erreur de connection :'.$errorConnection->getMessage());
        }

    }

    protected function createQuery($sql, $parameters = null)
    {
        if($parameters)
        {
            $result = $this->checkConnection()->prepare($sql);
            $result->setFetchMode(PDO::FETCH_CLASS, static::class);
            $result->execute($parameters);
            return $result;
        }
        $result = $this->checkConnection()->query($sql);
        $result->setFetchMode(PDO::FETCH_CLASS, static::class);
        return $result;
    }
}

Actualisez votre page http://localhost/blog/home.php

Une erreur d'affiche 😢

Je le savais que depuis le début, tu te moquais de nous 😔

Attendez, on n'a pas fini de refactoriser notre code encore. Mettez à jour votre fichier home.php

<?php
//On inclut le fichier dont on a besoin (ici à la racine de notre site)
require 'Database.php';
//Ne pas oublier d'ajouter le fichier Article.php
require 'Article.php';
?>
<!DOCTYPE html>
<html lang="fr">
<head>
    <meta charset="utf-8">
    <title>Mon blog</title>
</head>

<body>
<div>
    <h1>Mon blog</h1>
    <p>En construction</p>
    <?php
    $article = new Article();
    $articles = $article->getArticles();
    while($article = $articles->fetch())
    {
        var_dump($article);
        ?>
        <div>
            <h2><a href="single.php?articleId=<?= htmlspecialchars($article['id']);?>"><?= htmlspecialchars($article['title']);?></a></h2>
            <p><?= htmlspecialchars($article['content']);?></p>
            <p><?= htmlspecialchars($article['author']);?></p>
            <p>Créé le : <?= htmlspecialchars($article['createdAt']);?></p>
        </div>
        <br>
        <?php
    }
    $articles->closeCursor();
    ?>
</div>
</body>
</html>

J'ai ici ajouté un var_dump pour que vous puissiez voir ce que nous avons comme données... des objets 😉

Oui, mais pourquoi une erreur s'affiche alors ? 😩

Parce que notre syntaxe dans notre fichier est incorrecte. Mettez à jour votre fichier home.php (pensez à supprimer le var_dump): 

<?php
//On inclut le fichier dont on a besoin (ici à la racine de notre site)
require 'Database.php';
//Ne pas oublier d'ajouter le fichier Article.php
require 'Article.php';
?>
<!DOCTYPE html>
<html lang="fr">
<head>
    <meta charset="utf-8">
    <title>Mon blog</title>
</head>

<body>
<div>
    <h1>Mon blog</h1>
    <p>En construction</p>
    <?php
    $article = new Article();
    $articles = $article->getArticles();
    while($article = $articles->fetch())
    {
        ?>
        <div>
            <h2><a href="single.php?articleId=<?= htmlspecialchars($article->id);?>"><?= htmlspecialchars($article->title);?></a></h2>
            <p><?= htmlspecialchars($article->content);?></p>
            <p><?= htmlspecialchars($article->author);?></p>
            <p>Créé le : <?= htmlspecialchars($article->createdAt);?></p>
        </div>
        <br>
        <?php
    }
    $articles->closeCursor();
    ?>
</div>
</body>
</html>

Actualisez votre page, cela doit fonctionner maintenant. Pensez à en faire de même pour notre fichier single.php

<?php
//On inclut le fichier dont on a besoin (ici à la racine de notre site)
require 'Database.php';
//Ne pas oublier d'ajouter le fichier Article.php
require 'Article.php';
?>

<!DOCTYPE html>
<html lang="fr">
<head>
    <meta charset="utf-8">
    <title>Mon blog</title>
</head>

<body>
<div>
    <h1>Mon blog</h1>
    <p>En construction</p>
    <?php
    $article = new Article();
    $articles = $article->getArticle($_GET['articleId']);
    $article = $articles->fetch()
    ?>
    <div>
        <h2><?= htmlspecialchars($article->title);?></h2>
        <p><?= htmlspecialchars($article->content);?></p>
        <p><?= htmlspecialchars($article->author);?></p>
        <p>Créé le : <?= htmlspecialchars($article->createdAt);?></p>
    </div>
    <br>
    <?php
    $articles->closeCursor();
    ?>
    <a href="home.php">Retour à l'accueil</a>
</div>
</body>
</html>

Il est grand temps de s'occuper de notre classe Comment.

 

Quelques révisions

Si vous avez besoin de revoir certains points, voici les liens en conséquence :

- PDO
var_dump
classe abstraite

 

Bilan

Dans ce chapitre, nous avons refactorisé notre classe  Database  et adapté notre classe  Article , ainsi que nos fichiers home.php et single.php en conséquence.

Vous pouvez retrouver le code associé à ce chapitre sur GitHub.