phpInfo.netLes ArchivesLes éléPHPants

  
  Accueil
  Trucs & Astuces
  Scripts
  Regex
  Annuaire
  Articles

.
      
 Articles   Introduction pratique à PEAR  Par Gilbert SAINT   Septembre 2001    »  PEAR : PHP Extension and Application Repository ???
 »  Pour commencer, le plus déroutant : où obtenir de la documentation ?
 »  Supposons que l'on continue par la solution la plus radicale : plonger ...
 »  Le code est en place !
 »  On y est ... presque : première étape
 »  Introduction de la classe Table
 »  Dernière étape : le formulaire de choix
 »  Conclusion
 »  Liens divers + Sources


PEAR : PHP Extension and Application Repository ???

On entend de plus en plus ces 4 lettres sans bien savoir ce qu'elles peuvent apporter et en les regardant avec une certaine méfiance : encore de nouvelles classes !

Pour donner une petite idée de l'utilisation qui peut en être faite, voici un exemple très simple qui met en oeuvre trois types d'actions les plus utilisées dans l'écriture d'un site dynamique : la consultation d'une base de données, la génération de tableaux à partir des résultats et l'écriture de formulaires.

La base de données utilisée regroupe les informations sur des manifestations sportives : on ne garde ici que trois champs, le lieu, la date de début et la date de fin.

Le formulaire servira à rechercher les manifestations dont le nom de lieu contient une chaîne particulière. Application on ne peut plus simple mais qui donnera toutes les idées pour compliquer à loisir tout en montrant l'intérêt d'utiliser PEAR.



Pour commencer, le plus déroutant : où obtenir de la documentation ?

Désolé, mais on est loin des documentations classiques : on en trouve à la fin du manuel PHP 4, mais qui oserait se lancer dans un développement avec ça ? Il s'agit plutôt d'un guide de développement pour PEAR lui-même que pour les utilisateurs !

En piochant à droite et à gauche, on trouve quelques articles succints sur PHPBuilder, PHPEveryWhere à partir desquels on peut remonter vers des bribes d'informations qui, mises bout à bout, commencent à donner quelques idées.

Mais la seule solution reste le package fourni avec PEAR lui-même, PHPDoc qui génère la documentation à partir du code pour peu qu'il soit documenté suivant des règles bien précises. La qualité de cette documentation dépend donc beaucoup du soin qu'a pris l'auteur du code : le plus souvent elle sert surtout à l'auteur lui-même à se souvenir de ce qu'il a voulu faire; donc il faut prévoir quelques séances de relecture du code pour bien emmagasiner les fonctions et leurs possibilités. Voilà un point qui peut certainement en faire abandonner plus d'un !!!!



Supposons que l'on continue par la solution la plus radicale : plonger ...

Première chose à faire, remplacer la version de PEAR distribuée avec PHP4 par la version la plus à jour disponible sous CVS.

Et oui, c'est un package qui évolue et qui évolue vite : si on peut considérer que la version disponible à une date donnée ne présente pas de bug majeur, celle qui sera disponible quelques jours plus tard intégrera peut-être quelques nouveautés (et surtout celles que vous avez vous même intégrées pour vos besoins !). Néanmoins, la facilité d'usage peut justifier d'intégrer une version bien identifiée dans un projet.

Pour récupérer la dernière version, utiliser son client CVS préféré et se connecter à l'adresse ':pserver:cvsread@cvs.php.net:/repository' avec le mot de passe 'phpfi' puis dans le répertoire php4/pear pour en vider toute l'arborescence.
De temps en temps, faire un 'update' en faisant attention à bien gérer vos différentes versions utilisées dans vos projets sinon ...surprises !



Le code est en place !

Pour pouvoir l'utiliser il faut intégrer le chemin d'accès au répertoire PEAR dans le 'include_path' de PHP, soit en modifiant le fichier php.ini, soit en ajoutant l'instruction "ini_set('include_path','.;chemin/PEAR/');" dans votre code (ne pas oublier le .; sinon vous ne pourrez plus intégrer vos propres fichiers).

Un moyen utile de le tester est de générer la documentation avec PHPDoc.
Pour cela, adapter les 3 lignes de code qui suivent la ligne 50 de PHPDoc/index.php pour fournir vos répertoires d'installation et de sortie de la documentation

A priori, cela doit fonctionner sans problème, pour peu que vous ayiez pris la peine d'adapter le temps limite d'exécution car documenter tout le package est très très long.
Un conseil, commencez par le minimum indispensable, c'est à dire la racine de PEAR et les répertoires DB et HTML.



On y est ... presque : première étape

On veut consulter notre base de données pour en lister tous les enregistrements dans un tableau. Inutile de recopier ici la méhode utilisant les fonctions de base de PHP pour accéder à MySQL, passons tout de suite au code intégrant la classe DB de PEAR :

<?php

require_once("conf/config.php");
require_once(
"conf/errhandler.php");
require_once(
"DB.php");

echo
"<div align='center'>";
$dsn = "mysql://$user:$pass@$host/$db_name";
$db = DB::connect($dsn);

$sql = "select nom, datedeb, datefin from conc";
$sql .= " order by datedeb asc";
$result = $db->query($sql);

echo
"<table bgcolor=\"lightblue\"><tr bgcolor=\"yellow\"><th>Lieu</th>";
echo
"<th>Date de début</th><th>Date de fin</th></tr>";

while (
$row = $result->fetchRow())
{
    echo
"<tr><td>$row[0]</td><td>$row[1]</td><td>$row[2]</td></tr>";
}
echo
"</table>";

echo
"</div>";

?>

Ces quelques lignes pour produire la table ci-dessous :

Commentaires sur le code (on laisse de côté tout ce qui concerne les headers nécessaires, comme dirait Perrich, c'est du HTML et même du X...):

  1. on n'utilise que les instructions require_once(...) pour éviter les duplications fatales de portions de code.
  2. le fichier conf/config.php (qui en fait par sécurité ne s'appelle jamais comme ça, jamais .inc et bien protégé par un .htaccess...) définit les variables $user, $pass, $host et $db_name. Il inclut la ligne de code 'ini_set('include_path','.;c:/chemin/PEAR/');'. Il définit aussi d'autres variables que nous verrons par la suite.
  3. le fichier DB.php situé dans le répertoire de PEAR décrit la classe générique DB, intégre (par un require_once) le fichier PEAR.php
  4. les puristes auront remarqué même à la lecture rapide de ce code qu'il n'y a aucune référence à une gestion des erreurs sauf par le fichier errhandler.php sur lequel nous reviendrons. Pour l'instant supposons que tout ne puisse que bien se passer !!!
  5. La définition de la base de données à utiliser se fait grâce à la chaîne $dsn (pour Data Source Name) qui intégre le type de base de données (ici MySQL, l'utilisateur et son mot de passe, le serveur ainsi que le nom de la base de données).
    Premier avantage, pour la très grande majorité des bases de données existantes, seul le DSN change (par exemple en remplaçant mysql par pgsql pour utiliser PostGreSQL) toutes les autres instructions permettant de travailler avec la base de données restent inchangées !
  6. la connexion à la base, l'envoi d'une requête SQL et la récupération des données se fait en utilisant des méthodes de la classe DB suivant un modèle classique, avec de nombreuses variantes équivalentes à fetchRow() qui permettent de récupérer des tableaux associatifs ou non, les colonnes (voir la documentation (;-o)...).
    A noter aussi la possibilité de création de séquences qui peuvent se révéler bien utiles à la place de champs auto-incrémentés.
  7. le remplissage du tableau se fait de façon tout à fait classique (n'oublions pas les < ou > ou \" ou / pour XHTML...)

A quoi ressemble le fichier config.php ?

<?php

// intégrer le path de PEAR dans include_path
// adapter syntaxe du chemin path au système

$path = ini_get('include_path');

if (
$path)
    
ini_set('include_path','c:/www/PEAR;'.$path);
else
    
ini_set('include_path','.;c:/www/PEAR/');

$host    = "localhost";
$user    = "root";
$pass    = "";
$db_name = "concours";

?>

La première ligne permet d'inclure les fichiers du répertoire PEAR. Les lignes suivantes initialisent les variables permettant d'accéder à la base. La suite initialise certaines variables utilisées par le gestionnaire d'erreurs dont le code est le suivant (errhandler.php) :

<?php

define
("GS_NL_SEP","'gs'nl'");      // mon séparateur équivalent à nl
define("GS_TAB_SEP","'gs'tab'");    // mon séparateur équivalent à tab
define("ERR_ON_FILE",        1);
define("ERR_ON_HTML_WIN",    2);
define("ERR_BY_MSG",        4);

error_reporting(0);

function
htmlize_gs_serial($err) {
// transforme la chaîne fournie en HTML :
//    remplace GS_NL_SEP par <br> et GS_TAB_SEP par des blancs
//    échappe les caractères qui doivent l'être
$err =  str_replace(GS_NL_SEP,"<br />",htmlentities($err));
$err = str_replace(GS_TAB_SEP,"&nbsp; &nbsp; &nbsp;",$err);
return
addslashes($err);
}

function
fileize_gs_serial($err) {
// transforme la chaîne fournie en chaîne imprimable
//    remplace GS_NL_SEP par \n et GS_TAB_SEP par \t
$err =  str_replace(GS_NL_SEP,"\n",$err);
$err = str_replace(GS_TAB_SEP,"\t",$err);
return
$err;
}

function
gs_serialize_array($var,$prelig="") {
// génère une chaîne indiquant le type, le nom et la valeur des variables du
// tableau $var
//    effectue des appels récursifs si des variables sont des arrays ou des objets
//    indente les pésentation grâce à un préfixe de ligne

if (sizeof($var)==0) return "$prelig<empty>".GS_NL_SEP;
$str = "";
reset($var);
while (list (
$key, $val) = each ($var))
{
    
$gtype = gettype($val);
    if (
$gtype == "array")
    {
        if (
$key != "GLOBALS")
        
$str .= "$prelig$gtype : $key";
        
$str .= GS_NL_SEP.gs_serialize_array($val,$prelig.GS_TAB_SEP);
    }
    else if (
$gtype == "object")
    {
        
$str .= "$prelig$gtype : $key";
        
$str .= GS_NL_SEP.gs_serialize_array(get_object_vars($val),
                                             
$prelig.GS_TAB_SEP);
    }
    else if (
$gtype =="resource") {
        
$str .= "$prelig$gtype : $key => ".get_resource_type($key).GS_NL_SEP;
    }
    else if (
$gtype == "string")
    {
        
$str .=  "$prelig$gtype : $key => ".addcslashes($val,"\0..\31").GS_NL_SEP;
    }
    else
        {
$str .=  "$prelig$gtype : $key => $val".GS_NL_SEP; }
    }
return
$str;
}

function
gs_error_handler($errno, $errstr, $errfile, $errline, $vars) {
// gestionnaire d'erreurs pour toutes les erreurs sauf E_NOTICE
//    génère une chaîne contenant date, fichier, ligne et message d'erreur ainsi
//    que la liste des types, noms et valeurs des variables en cours
//    suivant la valeur de ERR_REPORT, sortie dans une fenêtre de navigateur
//    ou sur le fichier ERR_FILE avec possibilité d'envoi d'un mail au webmaster
//    --> dans la page fournie au surfeur, n'affiche qu'un message d'excuses...
if ($errno == E_NOTICE) return;

$date = date("d/m/Y H:i:s");
$err  = "-----------------------------------------------------".GS_NL_SEP;
$err .= "$date | dans $errfile, ligne : $errline -- err n°$errno, $errstr";
$err .= GS_NL_SEP."Variables :".GS_NL_SEP;
if (
strpos($errstr,'PEAR : ') === false) { $err .= gs_serialize_array($vars);}
reset($vars);
$glob = sizeof(array_keys($vars,"PHP_SELF"));
if (
$glob==0) $err .= GS_NL_SEP."GLOBALS".GS_NL_SEP.gs_serialize_array($GLOBALS);
if (
ERR_REPORT & ERR_ON_HTML_WIN) {
    
$erra = htmlize_gs_serial($err);
    echo
"<SCRIPT type=\"text/javascript\">\n";
    echo
"<!--\n";
    echo
"    errwin = window.open (\"\",\"ErrWin\",\"toolbar=no,location=no,";
    echo
"directories=no,status=no,scrollbars=yes,resizable=yes,copyhistory=no,";
    echo
"width=600,height=600\");\n";
    echo
"errwin.document.write(\"<html><head>";
    echo
"<title>Errs</title></head><body>\");\n";
    echo
"errwin.document.write(\"$erra\");\n";
    echo
"errwin.document.write(\"</body></html>\");\n";
    echo
"//-->\n";
    echo
"</SCRIPT>\n";
    }
if (
ERR_REPORT & ERR_ON_FILE) {
    
$erra = fileize_gs_serial($err);
    
error_log($erra, 3, ERR_FILE);
    }
if (
ERR_REPORT & ERR_BY_MSG) {
    
$erra = fileize_gs_serial($err);
    
mail(ERR_MSG_ADDR,ERR_MSG_SUBJ,$erra);
    }

die(
"<br><br><b>".ERR_SITE_MSG."</b>");
}

set_error_handler('gs_error_handler');

function
pear_error_handler($err_obj) {
$error_string =$err_obj->getMessage()."/".$err_obj->getDebugInfo();
trigger_error("PEAR : ".$error_string, E_USER_ERROR);
}

require_once(
"PEAR.php");
PEAR::setErrorHandling(PEAR_ERROR_CALLBACK,'pear_error_handler');

?>

Le principe du gestionnaire est le suivant : toutes les erreurs sont piégées (error_reporting(0)) et un message d'erreur est construit indiquant la date, le fichier php où se produit l'erreur, la ligne, le code et l'intitulé.
Le message contient aussi la liste des variables définies en cours au moment de l'erreur avec pour chacune son type, son nom et sa valeur et lorsqu'il s'agit de tableaux ou d'objets la description indentée de tous leurs éléments. Ce message d'erreur peut être affiché de différentes façons : pour l'instant, si la variable ERR_REPORT vaut HTML_WIN, le message est transcrit en HTML et affiché dans une fenêtre de navigateur, sinon il est écrit dans un fichier de nom ERR_FILE (plus tard on fera du XML).

La conversion en texte ou HTML est assuré par les deux fonctions textize_gs_serial et htmlize_gs_serial. A noter aussi la possibilité d'envoyer un mail au webmaster avec ce message.

Pour ajouter le cas PEAR à ce gestionnaire hors PEAR (vous suivez ?), il suffit d'ajouter comme CallBack la fonction pear_error_handling qui déclenche une erreur PHP.

On peut certainement faire beaucoup mieux mais ce gestionnaire respecte quelques impératifs :

  1. traiter les cas d'erreurs hors du code principal, il devient donc inutile de persiller le code avec des tests d'erreur un peu partout (une restriction toutefois : quand une erreur survient dans PEAR ce sont les variables du gestionnaire d'erreurs PEAR qui sont listées, aucune indication n'est donnée sur la fonction appelante qui a causé l'erreur; une idée ?)
  2. ne pas afficher les erreurs (juste un message d'excuses), c'est une question de sécurité pour un serveur opérationnel; dans ce cas, activer l'envoi du message vers le webmaster ou l'écriture dans un fichier log des erreurs,
  3. pendant la phase de développement, afficher les erreurs avec leur environnement dans une fenêtre séparée


Introduction de la classe Table

Maintenant que les requêtes sont enregistrées, que les erreurs sont traitées, comment faire pour se simplifier la vie en n'oubliant pas les balises, les quotes echappés pour construire un tableau ?
Le code suivant montre comment introduire la classe table avec quelques unes de ses méthodes les plus simples :

<?php

require_once("conf/config.php");
require_once(
"conf/errhandler.php");
require_once(
"DB.php");
require_once(
"HTML/table.php");

echo
"<div align='center'>";
$dsn = "mysql://$user:$pass@$host/$db_name";
$db = DB::connect($dsn);

$sql = "select nom, datedeb, datefin from conc";
$sql .= " order by datedeb asc";
$result = $db->query($sql);

$tbl = new HTML_Table("align= 'center' cellspacing='2' bgcolor='darkblue'");
$tbl->addRow(array("Lieu","Date de début","Date de fin"),"bgcolor='yellow'","th");

$result = $db->query($sql);
while (
$row = $result->fetchRow())
{
    
$tbl->addRow($row);
}
$tbl->altRowAttributes(1,"bgcolor='lightgreen'","bgcolor='lightblue'");
$tbl->display();

echo
"</div>";

?>

Ce qui donne l'affichage suivant :

Les lignes de code correspondant à la génération du tableau sont facilement compréhensibles : on crée une table centrée, avec un espacement entre cellules de 2 et un fond bleu foncé. La première ligne est une ligne d'entêtes sur un fond jaune. Les lignes suivantes proviennent de la récupération des enregistrements de la base de données. Enfin on ajoute une touche de lisibilité en alternant la couleur de fond des lignes de données.

Tout ceci n'est qu'un tout petit échantillon des méthodes de la classe Table : chaque élément, chaque ligne ou chaque colonne est accessible individuellement, aussi bien bien pour son contenu que sa mise en forme. A noter toutefois que certaines modifications sont à apporter à la classe disponible aujourd'hui pour générer du XHTML correct ou autoriser les regroupements de cellules facilement.



Dernière étape : le formulaire de choix

Pour l'exemple nous n'introduisons qu'une zone de saisie de texte appelée 'cond' et deux boutons, l'un pour valider l'entrée, l'autre pour effacer le champ d'entrée. A la soumission, le formulaire appelle de nouveau le même fichier qui maintenant teste la variable $cond pour voir si elle doit être prise en compte dans la requête.

Le code final est le suivant :

<?php

require_once("conf/config.php");
require_once(
"conf/errhandler.php");
require_once(
"DB.php");
require_once(
"HTML/Form.php");
require_once(
"HTML/table.php");

//-------------------------------------------------------------
/* ERR_REPORT indique comment afficher les erreurs. valeurs reconnues, seules
   ou combinées:
    ERR_ON_HTML_WIN : afficher les erreurs dans une fenêtre de navigateur
    ERR_ON_FILE : écrire les erreurs dans un fichier, dans ce cas ERR_FILE donne
    le nom du fichier
    ERR_BY_MSG : envoyer un message d'erreur,
        ERR_MSG_ADDR est l'adresse du destinataire, ERR_MSG_SUBJ est le sujet

    Le site affiche le message ERR_SITE_MSG
    */
define("ERR_REPORT",ERR_ON_HTML_WIN);
define("ERR_SITE_MSG","Désolé, erreur sur le serveur...");
//--------------------------------------------------------------

echo "<div align='center'>";
$dsn = "mysql://$user:$pass@$host/$db_name";
$db = DB::connect($dsn);

$k = 1/0;
$sql = "select nom, datedeb, datefin from conc";
if (isset(
$cond)) $sql .= " where nom like '%$cond%'";

$sql .= " order by datedeb asc";
$result = $db->query($sql);

echo
"<table bgcolor=\"lightblue\"><tr bgcolor=\"yellow\"><th>Lieu</th>";
echo
"<th>Date de début</th><th>Date de fin</th></tr>";

while (
$row = $result->fetchRow()) {
    echo
"<tr><td>$row[0]</td><td>$row[1]</td><td>$row[2]</td></tr>";
    }
echo
"</table>";

$tbl = new HTML_Table("align= 'center' cellspacing='2' bgcolor='darkblue'");
$tbl->addRow(array("Lieu","Date de début","Date de fin"),"bgcolor='yellow'","th");

$result = $db->query($sql);
while (
$row = $result->fetchRow()) {
    
$tbl->addRow($row);
    }
$tbl->altRowAttributes(1,"bgcolor='lightgreen'","bgcolor='lightblue'");
$tbl->display();

$frm = new HTML_Form("index.php","post","frm");
$frm->addText("cond","Partie de mot à rechercher : ","");
$frm->addSubmit("submit","Rechercher");
$frm->addReset("Effacer");
$frm->display();

echo
"</div>";

?>

Ce qui donne l'affichage suivant :

Et voilà : le risque d'oublier des guillemets, éventuellement échappés, est nettement diminué, le code est beaucoup plus lisible donc peut être repris par soi-même ou les amis pour d'autres applications.

Bien sûr, d'autres méthodes de la classe Form sont disponibles (à l'aide PhpDoc !): certaines sont d'un intérêt douteux, les méthodes de base manquent cruellement de gestion des évènements mais il est très rapide de les ajouter tout en restant dans l'esprit PEAR.



Conclusion

Ce petit exemple montre l'ossature de l'utilisation de PEAR dans une application. Le développement complet à partir des méthodes attachées aux classes de base présentées ici et des autres classes disponibles dans le package se trouve simplifié, la lisibilité du code aide à la mise au point : mais ce sont là les qualités que devrait avoir toute classe correctement développée.

Alors pourquoi PEAR plutôt que d'autres ? Peut-on trouver un argument décisif ? (Pour Perrich certainement pas...) Mettons en avant la hiérarchisation qui existe déjà sur un nombre de classes important, le fait que des efforts de normalisation sont faits pour l'écriture du code (la documentation officielle porte quasi uniquement sur les règles de codage), que le package fait partie de la distribution PHP (même si il doit être mis à jour régulièrement) : à partir de là, on peut faire le pari que ce package peut devenir très répandu donc profiter comme tous les logiciels Open Source d'apports nombreux qui l'enrichissent... A terme, plus le nombre de programmeurs PHP faisant ce pari sera grand, plus on pourra trouver là des facilités de programmation (codage, mise au point) efficaces !

Alors pourquoi pas un petit espace PEAR sur phpInfo.net, avec un magnifique logo comme notre illustrateur attitré sait si bien les faire, un logo en forme de poire ???

PS : un point important n'est pas traité ici mais il y aura certainement un contributeur pour le faire, ne serait-ce que pour essayer de contrebalancer les avantages indiqués ici (;-D) : quelles sont les performances en termes de mémoire supplémentaire occupée, de temps de calcul 'perdu' en intégrant ces appels de fonctions supplémentaires ? Ou, exprimé d'une autre façon, quel est le coût de la facilité de développement quand un site développé suivant ces règles devient opérationnel ?



Liens divers + Sources

 » Manuel PHP - 'PEAR: the PHP Extension and Application Repository'
 » PHPBuilder - 'PHP Extension and Add-on Repository (PEAR)'
 » PHP Everywhere - 'PEAR Documentation and Tutorials'

 » Les sources de l'article
Synseo