Classe de Log en PHP

Classe de Log en PHP

Tutoriel publié en février 2012 par Galdon dans la catégorie PHP

Bienvenue dans ce tutoriel consacré à la réalisation d'une classe de log en PHP.

Classe Logger Classe de gestion de fichiers log en PHP
Télécharger

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 :

Arborescence du dépôt de 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).

Classe Logger Classe de gestion de fichiers log en PHP
Télécharger

Allez donc jeter un oeil sur cette page : la violoncelliste de minuit.

Tags
Tutoriaux similaires
0 commentaire
facultatif
Facebook Twitter RSS Email
Forum Excel
Venez découvrir le nouveau forum excel question/réponse à la stackoverflow.com !
Forum Excel
hit parade n'en a rien a foutre du W3C Positionnement et Statistiques Gratuites Vincent Paré