Publié le 02/08/2020
Auteur fobec
Réseaux sociaux
0 partages
0 tweets
0 plus
0 commentaires

Systeme de cache pour page HTML

Comment soulager le moteur de base de données et les controler PHP ? Les sites internets répondent des requêtes en continue et il peut être intéressant de sauvegarder les ressources du serveur. L'affichage d'une page HTML nécessite de nombreuses opérations que ce soit du coté de PHP ou que ce soit au niveau de la base de donnée. La mise en cache est une technique pour améliorer les performances de son hébergement et d'améliorer la sécurité de son site web.

Pourquoi utiliser un cache HTML

La première raison qui vient à l'esprit est une question de bon sens. Lorsque les scripts PHP assemblent le code HTML d'une page, il y a peu de chance que 10 minutes plus tard, il faille à nouveau repartir de zéro. Dans ce cas, autant se resservir des calculs qui ont été effectués et de mettre à jour uniquement certaines parties du template.

La deuxième raison est le trafic croissant auquel est soumis un site internet. Entre les internautes, les réseaux sociaux et les crawlers en tout genre, une même page peut être interrogée une dizaine de fois en quelques minutes. Je vous invite à jeter un coup d'oeil au fichier log du serveur pour se rendre compte du nombre de requête pour une même URL. Du coup, autant afficher la même page HTML à chaque fois sans relancer les scripts et les requêtes vers la base de donnée.

Class PHP pour une mise en cache

Pour ce cas pratique, nous avons choisi de stocker les fichiers HTML dans une base Sqlite tout simplement pour le coté pratique. En effet, tout le cache est contenu dans un seul fichier et le moteur SQL permet de faire des requêtes sur les champs associés au fichier HTML.

<?php
 
namespace Vendor\Database;
 
/**
 * Page cache engine using Sqlite DB
 *
 * @author chris 24/12/2018
 */
use Exception;
use PDO;
 
class FrontCacheEngine {
 
    /**
     * Duree de vie 1 semaine 7 x 24 x 60 x 60
     */
    const LIFE_ONEWEEK = 604800;
 
    /**
     * Duree de vie 2 semaine 14 x 24 x 60 x 60
     */
    const LIFE_TWOWEEK = 1209600;
 
    /**
     * Duree de vie 1 mois 30 x 24 x 60 x 60
     */
    const LIFE_ONEONTH = 2592000;
 
    /**
     * Nom de la DB
     */
    const FILE_DB = 'db_front_html_cache.sqlite';
 
    /**
     * Insérer ou remplacer le cache HTML
     * @param string $uri
     * @param string $html
     * @param int $quality
     * @param int $cache_life
     * @return boolean
     */
    static public function update(string $uri, string $html, string $note = '', string $db_path = '', string $db_filename = '', int $cache_life = self::LIFE_TWOWEEK): bool {
        $SQL = 'INSERT OR REPLACE INTO tb_front_cache (FCA_URI,FCA_HTML,FCA_DATE_CREATED,FCA_DATE_EXPIRED,FCA_ETAG,FCA_NOTE) 
            values(:FCA_URI,:FCA_HTML,:FCA_DATE_CREATED,:FCA_DATE_EXPIRED,:FCA_ETAG,:FCA_NOTE)';
 
        $P = array();
        $ts = time();
 
        $P['FCA_URI'] = trim($uri);
        $P['FCA_HTML'] = gzcompress($html);
        $P['FCA_DATE_CREATED'] = date('Y-m-d H:i:s');
        $P['FCA_DATE_EXPIRED'] = date('Y-m-d H:i:s', ($ts + $cache_life));
        $P['FCA_ETAG'] = md5($uri . $P['FCA_DATE_EXPIRED']);
        $P['FCA_NOTE'] = $note;
 
        $file_db = self::get_db_filename($db_path, $db_filename);
        try {
            $pdo = new PDO('sqlite:' . $file_db);
            $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
 
            $stmt = $pdo->prepare($SQL);
            $b = $stmt->execute($P);
            if (!$b) {
                parent::log_error_msg('Update cache failed', __CLASS__, __LINE__);
            }
 
            return $b;
        } catch (Exception $ex) {            
            return FALSE;
        }
    }
 
    /**
     * Retourner la page en cache
     * @param string $uri
     * @param string $db_path
     * @return array
     */
    static public function read(string $uri, string $db_path = '', string $db_filename = ''): array {
        $SQL = 'SELECT * FROM tb_front_cache WHERE FCA_URI=:FCA_URI AND FCA_DATE_EXPIRED>:FCA_DATE_EXPIRED LIMIT 1';
 
        $P = array();
        $P['FCA_URI'] = $uri;
        $P['FCA_DATE_EXPIRED'] = date('Y-m-d H:i:s');
 
        $file_db = self::get_db_filename($db_path, $db_filename);
        $rs = [];
        try {
            $pdo = new PDO('sqlite:' . $file_db);
            $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
 
            $stmt = $pdo->prepare($SQL);
            $stmt->execute($P);
            $rs = $stmt->fetch(PDO::FETCH_ASSOC);
            $stmt->closeCursor();
        } catch (Exception $ex) {            
            $rs = [];
        }
 
        if (isset($rs['FCA_HTML'])) {
            $rs['FCA_HTML'] = gzuncompress($rs['FCA_HTML']);
        } else {
            $rs = [];
        }
        return $rs;
    }
 
    static public function read_html(string $uri, string $db_path = ''): string {
        $rs = self::read($uri, $db_path);
 
        if (isset($rs['FCA_HTML'])) {
            return $rs['FCA_HTML'];
        } else {
            return '';
        }
    }
 
    static public function compact_db() {
        $SQL = 'DELETE FROM tb_cw_search WHERE CWS_DATE_EXPIRED<:CWS_DATE_EXPIRED';
 
        $P = array();
        $P['CWS_DATE_EXPIRED'] = date('Y-m-d H:i:s');
 
        $file_db = rtrim($_SERVER['DOCUMENT_ROOT'], DIRECTORY_SEPARATOR) . self::FILE_DB;
 
        try {
            $pdo = new PDO('sqlite:' . $file_db);
            $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
 
            //delete old rows
            $stmt = $pdo->prepare($SQL);
            $b = $stmt->execute($P);
 
            //VACUUM
            $stmt = $pdo->prepare('VACUUM');
            $b = $stmt->execute();
        } catch (Exception $ex) {            
            $rs = FALSE;
        }
    }
 
    static public function get_table_status(): array {
        $n_stat = array('row_count' => 0);
 
        //Taille de la BDD
        $file_db = rtrim($_SERVER['DOCUMENT_ROOT'], DIRECTORY_SEPARATOR) . self::FILE_DB;
        $n_stat['file_size'] = filesize($file_db);
 
 
        try {
            $pdo = new PDO('sqlite:' . $file_db);
            $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
 
            //Nombre de ligne
            $SQL = "SELECT COUNT(*) as ROWCOUNT FROM tb_cw_search";
            $stmt = $pdo->prepare($SQL);
            $b = $stmt->execute();
            $rs = $stmt->fetch(PDO::FETCH_ASSOC);
            $stmt->closeCursor();
            if (isset($rs['ROWCOUNT'])) {
                $n_stat['row_count'] = (int) $rs['ROWCOUNT'];
            }
 
            //cache périmé
            $SQL = "SELECT COUNT(*) as ROWCOUNT FROM tb_cw_search WHERE CWS_DATE_EXPIRED<:CWS_DATE_EXPIRED";
            $P = array();
            $P['CWS_DATE_EXPIRED'] = date('Y-m-d', time()) . ' 00:00:00';
            $stmt = $pdo->prepare($SQL);
            $b = $stmt->execute($P);
            $rs = $stmt->fetch(PDO::FETCH_ASSOC);
            $stmt->closeCursor();
            if (isset($rs['ROWCOUNT'])) {
                $n_stat['expired_count'] = (int) $rs['ROWCOUNT'];
            }
            return $n_stat;
        } catch (Exception $ex) {            
            $rs = FALSE;
        }
 
 
        foreach ($this->databaseDefinition->getTablenameList() as $table_name) {
            $rs = $this->query_custom();
            if (isset($rs[0]['ROWCOUNT'])) {
                $n_stat[$table_name] = $rs[0]['ROWCOUNT'];
            } else {
                App_adminModelLogcat::sysWarning('Invalid table name: ' . $table_name, __CLASS__, __LINE__);
            }
        }
 
        return $n_stat;
    }
 
    /**
     * Créer la DB Sqlite
     * @param string $path
     * @return boolean
     * @throws Exception
     */
    static public function create_db(string $path = '', string $db_filename = '') {
        $file_db = self::get_db_filename($path, $db_filename);
 
        try {
            $pdo_out = new PDO('sqlite:' . $file_db);
            $pdo_out->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
 
            ####################################################################
            # Table cache
            ####################################################################
            $SQL = "CREATE TABLE IF NOT EXISTS tb_front_cache (";
            $SQL .= "FCA_ID INTEGER PRIMARY KEY AUTOINCREMENT,";
            $SQL .= "FCA_URI VARCHAR(128) UNIQUE NOT NULL,";
            $SQL .= "FCA_HTML TEXT NOT NULL,";
            $SQL .= "FCA_DATE_CREATED DATETIME NOT NULL,";
            $SQL .= "FCA_DATE_EXPIRED DATETIME NOT NULL,";
            $SQL .= "FCA_ETAG VARCHAR(32) UNIQUE NOT NULL,";
            $SQL .= "FCA_NOTE TEXT";
            $SQL .= ");";
 
            $pdo_out->exec($SQL);
 
            ####################################################################
            # Test
            ####################################################################
            $stmt = $pdo_out->prepare('PRAGMA table_info(tb_front_cache)');
            $stmt->execute();
            $rs = $stmt->fetch(PDO::FETCH_ASSOC);
            $stmt->closeCursor();
 
            if (!isset($rs['name']) || $rs['name'] != 'FCA_ID') {
                throw new Exception('Create DB or table failed: tb_front_cache');
            }
 
            return TRUE;
        } catch (Exception $ex) {            
            throw new Exception($ex);
        }
    }
 
    /**
     * Retourner le dossier et fichier de la DB
     * @param string $path
     * @return string
     * @throws Exception
     */
    static protected function get_db_filename(string $path = '', string $db_filename = ''): string {
        $db_path = '';
 
//Utiliser le dossier transmis
        if (!empty($path)) {
            $path = realpath($path);
            if (!is_dir($path) && !is_writable($path)) {
                throw new Exception('Dir not exists or not writable: ' . $path);
            }
            $db_path = rtrim($path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
        }
 
        //Aucun dossier
        if (empty($db_path)) {
            throw new Exception('Dir not set');
        }
 
        //Nom de dla DB
        if (!empty($db_filename)) {
            return $db_path .$db_filename;
        } else {
            return $db_path . self::FILE_DB;
        }
    }
}
 
Cette classe est utilisée dans un projet utilisant les namespace, les lignes 3 à 11 peuvent être supprimées dans un contexte PHP standard. Lors de la sauvegarde de la page, vous pouvez spécifier la durée de validité du cache exprimée en seconde, par défaut l'intervalle est fixé à deux semaines.

Exemple de sauvegarde de page HTML

Pour illustrer l'utilisation de la classe FrontCacheEngine, voici un script qui reprend les trois étapes à suivre:
-> créer la base de donnée (à la première utilisation)
-> stocker le code source d'une url courte
-> lire le code HTML mis en cache


<?php
require_once $_SERVER['DOCUMENT_ROOT'] . 'FrontCacheEngine.php';
//Dossier de stockage de Sqlite
$PATH=$_SERVER['DOCUMENT_ROOT'];
 
//Création de la base de donnée
\Vendor\Database\FrontCacheEngine::create_db($PATH);
 
//Code source HTML pour l'exemple
//dans les projets, le HTML est généré par le moteur de template
$html=file_get_contents('http://www.fobec.com');
 
//Sauvegarde de la page
$uri='/';
\Vendor\Database\FrontCacheEngine::update($uri, $html,'',$PATH);
 
//Lecture
$n_cache=\Vendor\Database\FrontCacheEngine::read($uri,$PATH);
if (isset($n_cache['FCA_HTML'])) {
    echo $n_cache['FCA_HTML'];
} else {
    //page à regénérer
    //cause: absence de la page ou durée de validité dépassée
}



Pour ceux qui souhaitent optimiser le système de cache, la classe intègre la gestion des etag qui sont insérés dans le header de la page internet. L'exemple devra être adapté à votre propre système de génération du code HTML à partir d'un moteur de template ou en directement à la sortie du script controler de l'application.
La mise en cache reste une solution intéressante lorsque les données à mettre à jour sont assez faibles. Par exemple, pour un site web disposant d'une zone membre ou proposant un fil d'actualité, le code source est enregistré dans le cache puis au moment voulu seule la zone membre ou le fil d'actualité est modifié avec l'affichage.

Comme on peut le voir dans l'entête, la class FrontCacheEngine est utilisée depuis deux ans sur un site internet. Du coté des chiffres, la base Sqlite comporte 100 000 pages et le trafic tourne autour du million sur un mois. Pour cette solution facile à mettre en place, les performances sont à mon sens bonne puisque le temps de chargement dépasse rarement les 10 ms et que le script a permis de résister à un pic de trafic de 15 000 pages en 4 minutes !

Ajouter un commentaire

Les champs marqués d'un * sont obligatoires, les adresses emails se sont pas publiées.

A lire aussi

Réseaux sociaux
Présentation de l'article
Catégorie
php5 - class
Mise a jour
02/08/2020
Visualisation
vu 199 fois
Public
Internaute
Auteur de la publication
Fobec
Admin
Auteur de 267 articles
|BIO_PSEUDO|
Commentaires récents

Publié par vieux dans CMS

et comment ce le procurent ton ?

Publié par Fobec dans news

Bonjour,
la localisation des adresses ip utilise plusieurs algo de recherche de position geographique. La precision du rapport d'analyse correspond la qualite de la localisation:
9/10 la locali...

Publié par Connan dans php5

j'utilise mysql phpmyadmin et j'ai cree une table IP mais j'ai mis varchart(15) mais quese qui faut que je fasse pour stocker l'ip des joueurs qui s'inscrivent

Publié par Ludwig dans tuto

Bonjour,
Il me semble que les plugins de gestion d'images sont payants pour TinyMCE et CKeditor, est-ce toujours le cas ?
Il manque amha l'excellent Xinha dans cette liste, qui lui est Open Sour...

Publié par Fobec dans php5

a priori il manque simplement le R dans le nom de la constante
PDO::ATTR_ERRMODE en remplacement de PDO::ATT_ERRMODE