Bienvenue dans ce tutoriel consacré à la réalisation d'une classe de log en PHP.
Briefing
Un log (fichier journal en Français) est un fichier qui liste des événements (par exemple des erreurs), qui sont datés et stocké par ordre chronologique.
Les logs peuvent être très utiles sur un site web, pour garder une trace des erreurs qui se produisent lorsque vos utilisateurs visitent votre site par exemple (c'est l'une des utilisations les plus courantes), mais on peut aussi les utiliser pour collecter des statistiques, en enregistrant le nombre de clics sur un lien...
Cette classe de log va enregistrer les événements dans des fichiers de log, voici un exemple de ce va contenir un fichier log :
10/02/2012 10:05:35 Fonction login() : l'authentification a échoué 13/02/2012 05:07:01 Fonction mafonction() : paramètre 1 non défini 15/02/2012 20:24:05 Fonction mafonction() : paramètre 1 non défini 18/02/2012 17:27:59 Impossible de se connecter à la base de données 18/02/2012 17:28:01 Fonction mafonction() : paramètre 1 non défini
Et voilà comment on utilise la classe de log en PHP :
require 'Logger.class.php'; // Création d'un objet Logger $logger = new Logger('./logs/'); // Enregistrement d'un événement dans le fichier test_advanced.log : $logger->log('erreurs', 'err_utilisateurs', "Fonction login() : l'authentification a échoué", Logger::GRAN_MONTH);
Alors bien sûr, des classes PHP permettant de faire du logging il en existe déjà, dont Zend_Log notamment (un composant du Zend Framework), mais aucune de celle que j'ai trouvée ne permet de gérer l'archivage des fichiers log par mois, par année...
Du coup, on se retrouve avec des fichiers de log qui grossissent indéfiniment jusqu'à ce que quelqu'un fasse le ménage sur le serveur.
Je vais donc me focaliser, dans ce tutoriel, sur l'organisation des fichiers de log, ainsi que sur leur archivage. Pour cela, nous allons définir une série de caractéristiques :
- Tous les logs seront regroupés dans un dossier dédié, que nous appellerons le "dépôt"
Exemple :C:\EasyPHP\www\logs
. - Pour plus de souplesse, notre classe de log va permettre de gérer des catégories de log (type), on pourra ainsi créer une catégorie pour stocker les logs d'erreur, puis une autre pour les logs statistiques (exemple : enregistrement des clics sur un lien)
- Enfin, on pourra indiquer la granularité qui permettra d'archiver automatiquement les fichiers par année ou par mois
Au final, voilà à quoi pourrait ressembler le dépôt C:\EasyPHP\www\logs
:
Développement
Bien, maintenant que vous savez ce qu'on va faire, passons à la pratique ^^.
Avant tout, je précise que ce tutoriel utilise la programmation orientée objet et requiert donc PHP 5 (cela ne fonctionnera pas avec PHP 4).
Créez un nouveau fichier nommé Logger.class.php et ouvrez-le avec votre éditeur de texte (moi j'utilise Notepad++ sous Windows).
Nous allons y déclarer notre classe ainsi que ces attributs :
<?php class Logger { private $depot; // Dossier où sont enregistrés les fichiers logs private $ready; // Le logger est prêt quand le dossier de dépôt des logs existe // Granularité const GRAN_VOID = 'VOID'; // Aucun archivage const GRAN_MONTH = 'MONTH'; // Archivage mensuel const GRAN_YEAR = 'YEAR'; // Archivage annuel } ?>
L'attribut $depot va servir à stocker le chemin absolu vers le dépôt (ex: C:\EasyPHP\www\logs
).
L'attribut $ready permet de savoir si le logger est prêt à être utilisé ou pas. Nous verrons précisément à quoi il sert plus tard.
Les différentes granularités prises en charge par la classe sont stockées dans des constantes de classe, on n'est pas obligé de faire comme ça, mais c'est une bonne pratique de le faire, car ça permet d'une part de définir clairement les différentes valeurs possibles, et c'est également utile pour l'autocomplétion dans votre éditeur de code, qui vous proposera automatiquement les différentes valeurs lorsque vous appellerez le Logger.
Maintenant qu'on a nos attributs, il nous faut... un constructeur !
Si vous n'êtes pas familier avec la POO, sachez que le constructeur d'une classe est une fonction qui est appelée automatiquement par PHP lors de l'instanciation de celle-ci (quand vous faites un new Logger(...)
).
Depuis PHP 5, on déclare le constructeur en appelant la fonction __construct()
. Le constructeur de notre classe Logger va prendre en paramètre le chemin vers le dossier dépôt. On va aussi vérifier que le dossier existe bien, et c'est à ça que va nous servir l'attribut $ready :
public function __construct($path){ $this->ready = false; // Si le dépôt n'existe pas if( !is_dir($path) ){ trigger_error("<code>$path</code> n'existe pas", E_USER_WARNING); return false; } $this->depot = realpath($path); $this->ready = true; return true; }
Explication :
Au départ, on considère que logger n'est pas prêt, puisqu'on n’a encore rien vérifié.
Ensuite, on vérifie l'existence du dossier dépôt ($path). Si ce dossier n'existe pas, on ne peut pas aller plus loin puisqu'on ne pourra pas enregistrer les fichiers logs. Donc on affiche une erreur (un Warning), et on retourne false.
Par contre si le dossier existe bien, alors on appelle la fonction realpath() qui va "nettoyer" le chemin, et le convertir en chemin relatif en chemin absolu.
Ça n'est pas obligatoire, mais ça permet d'éviter d'avoir des raccourcis (liens symboliques sous Unix) ou des chemins comportant des . (dossier courant) ou des .. (dossier parent), tout en utilisant les bons séparateurs de dossier (slash sous Unix, antislash sous Windows).
Exemple: realpath('C:/EasyPHP\php\..\www/logs\./') = 'C:\EasyPHP\www\logs'
À ce stade, vous pouvez tester votre classe en essayant de l'instancier :
require 'Logger.class.php'; // Création d'un objet Logger (instanciation) // Avec un chemin relatif : $logger = new Logger('./logs/'); // Avec un chemin absolu : $logger2 = new Logger('C:\EasyPHP\www\logs');
Si le dossier que vous passez en paramètre au constructeur n'existe pas, vous devriez avoir
une erreur :
Warning: ./logs2
n'existe pas in C:\EasyPHP\www\Logger.class.php on line XX
Ok, là on a la base de notre classe : attributs et constructeur, reste le plus gros du travail à faire...
Méthodes
Nous allons ajouter plusieurs méthodes à la classe Logger :
- path($type, $name, $granularity)
Le job de cette méthode va être de créer les répertoires en fonction du type et de la granularité, et de retourner le chemin absolu vers le fichier.
Ex:$logger->path('erreurs', 'err_php', Logger::GRAN_YEAR) =
'J:\EasyPHP\logs\www\erreurs\2012\2012_err_php.log'
- log($type, $name, $row, $granularity)
Elle va utiliser path pour déterminer le chemin du fichier de log, ajouter la date et l'heure à $row et déclencher l'écriture du fichier log en appelant write()
- write($logfile, $row)
Ajouter $row (chaine de caractères) dans le fichier $logfile
Voilà le début de la méthode path() :
public function path($type, $name, $granularity = self::GRAN_YEAR){ // On vérifie que le logger est prêt (et donc que le dossier de dépôt existe if( !$this->ready ){ trigger_error("Logger is not ready", E_USER_WARNING); return false; } // Contrôle des arguments if( !isset($type) || empty($name) ){ trigger_error("Paramètres incorrects", E_USER_WARNING); return false; } // ... }
J'ai rendu le paramètre $granularity
facultatif en définissant une valeur par défaut : self::GRAN_YEAR
.
self
désigne la classe à laquelle appartient la méthode, écrire self::GRAN_YEAR
équivaut à écrire Logger::GRAN_YEAR
, mais il vaut mieux utiliser self car si plus tard on souhaite changer le nom de la classe Logger, on aura juste un seul endroit à modifier.
Vérifions maintenant que le dossier type existe bien. Les dossiers type se situent dans le dossier dépôt :
public function path($type, $name, $granularity = self::GRAN_YEAR){ // On vérifie que le logger est prêt (et donc que le dossier de dépôt existe if( !$this->ready ){ trigger_error("Logger is not ready", E_USER_WARNING); return false; } // Contrôle des arguments if( !isset($type) || empty($name) ){ trigger_error("Paramètres incorrects", E_USER_WARNING); return false; } // Si $type est vide, on enregistre le log directement à la racine du dépôt if( empty($type) ){ $type_path = $this->depot.'/'; } // Création dossier du type else { $type_path = $this->depot.'/'.$type.'/'; if( !is_dir($type_path) ){ mkdir($type_path); } } // ...
Il ne reste plus qu'à créer le dossier correspondant à la granularité choisie :
public function path($type, $name, $granularity = self::GRAN_YEAR){ // On vérifie que le logger est prêt (et donc que le dossier de dépôt existe if( !$this->ready ){ trigger_error("Logger is not ready", E_USER_WARNING); return false; } // Contrôle des arguments if( !isset($type) || empty($name) ){ trigger_error("Paramètres incorrects", E_USER_WARNING); return false; } // Si $type est vide, on enregistre le log directement à la racine du dépôt if( empty($type) ){ $type_path = $this->depot.'/'; } // Création dossier du type else { $type_path = $this->depot.'/'.$type.'/'; if( !is_dir($type_path) ){ mkdir($type_path); } } // Création du dossier granularity if( $granularity == self::GRAN_VOID ){ $logfile = $type_path.$name.'.log'; } elseif( $granularity == self::GRAN_MONTH ){ $mois_courant = date('Ym'); $type_path_mois = $type_path.$mois_courant; if( !is_dir($type_path_mois) ){ mkdir($type_path_mois); } $logfile = $type_path_mois.'/'.$mois_courant.'_'.$name.'.log'; } elseif( $granularity == self::GRAN_YEAR ){ $current_year = date('Y'); $type_path_year = $type_path.$current_year; if( !is_dir($type_path_year) ){ mkdir($type_path_year); } $logfile = $type_path_year.'/'.$current_year.'_'.$name.'.log'; } else{ trigger_error("Granularité '$granularity' non prise en charge", E_USER_WARNING); return false; } return $logfile; }
Ouf !
Rassurez-vous, cette fonction est la plus complexe de la classe Logger.
Passons maintenant à la fonction log() :
public function log($type, $name, $row, $granularity = self::GRAN_YEAR){ // Contrôle des arguments if( !isset($type) || empty($name) || empty($row) ){ trigger_error("Paramètres incorrects", E_USER_WARNING); return false; } $logfile = $this->path($type, $name, $granularity); if( $logfile === false ){ trigger_error("Impossible d'enregistrer le log", E_USER_WARNING); return false; } // Ajout de la date et de l'heure au début de la ligne $row = date('d/m/Y H:i:s').' '.$row; // Ajout du retour chariot de fin de ligne si il n'y en a pas if( !preg_match('#\n$#',$row) ){ $row .= "\n"; } $this->write($logfile, $row); }
Rien de bien compliqué ici, on ne fait que vérifier la valeur des arguments et appeler la méthode path.
Vous aurez aussi remarqué le $this->
au niveau de l'appel à la fonction path(). $this désigne l'instance courante de l'objet Logger. Il ne faut pas le confondre avec self
: $this fait référence à l'instance (l'objet) courante, alors que self fait référence à la classe, il est statique.
J'ai aussi ajouté l'insertion automatique d'un retour chariot à la fin de la ligne. Si on ne fait pas ça, on pourrait se retrouver avec un fichier log d'une seule ligne très très longue et illisible !
Enfin, voici la dernière méthode, write() :
private function write($logfile, $row){ if( !$this->ready ){return false;} if( empty($logfile) ){ trigger_error("<code>$logfile</code> est vide", E_USER_WARNING); return false; } $fichier = fopen($logfile,'a+'); fputs($fichier, $row); fclose($fichier); }
Le mode a+
permet d'ouvrir un fichier (et de le créer automatiquement s’il n'existe pas au passage) en plaçant le pointeur à la fin du fichier (ce qui permet d'écrire à la fin du fichier).
Allez donc jeter un oeil sur cette page : la violoncelliste de minuit.