I. Introduction

I-1. Prérequis

Le lecteur devra avoir une maitrise correcte de PHP, quelques notions d'objets, une connaissance d'un SGBD. Cet article est d'un niveau intermédiaire.

I-2. Pourquoi cet article ?

Je me suis rendu compte au travers du forum php de développez.com que les questions concernant des problèmes de compatibilité entre SGBD étaient récurrents ainsi que les problèmes de portage de site. Cet article ne traitera pas des étapes d'installation et configuration de chaque SGBD mais uniquement de leur utilisation par php.

I-3. Qu'est-ce que PEAR ?

Afin de définir PEAR, je citerais un article de ce même forum :

PEAR (pour PHP Extension and Application Repository) est une bibliothèque de scripts PHP. Tous les scripts déposés dans PEAR respectent un certain nombre de règles qui les rend portables (indépendants de la plate-forme) et réutilisables.

Cet article vous permettra d'ailleurs d'installer de facon manuelle les packages qui vous intéressent.

I-4. Problématique abordée

Lors d'un développement de site php/mysql, il est courant d'utiliser les API standards : mysql_connect, mysql_query, mysql_fetch_array etc... Cependant, lors d'un portage vers un autre SGBD et parfois même lors d'une montée de version du même SGBD, l'utilisation des API pose problème puisqu'une réécriture s'impose afin de rendre le code compatible. C'est ici qu'interviennent les couches d'abstraction de données.
MDB2 fait partie des ces librairies vous permettant de vous affranchir de ces problèmes de compatibilité. Attention toutefois, l'utilisation de cette couche d'abstraction aux données vous permettra d'éviter les problèmes d'API mais pas celle de compatibilité des requêtes. Si par exemple vous utilisez la fonction NOW() dans vos requêtes SQL mysql, celle ci ne passera pas sous ORACLE.
Dans le cadre de cet article nous parlerons donc d'une couche d'abstraction de donnée fournie par PEAR : MDB2, issu de PEAR::DB et Metabase.

II. Caractéristiques

II-1. Drivers

MDB2 s'utilise avec des drivers. Un driver n'est autre qu'une interface (objet dans le cas de MDB2) permettant l'accès à différentes bases de données. Les bases de données actuellement disponibles pour MDB2 sont :

  • MySQL
  • PostgreSQL
  • Oracle
  • Frontbase
  • Querysim
  • Interbase/Firebird
  • MSSQL
  • SQLite

II-2. Fonctionnalités

MDB2 est une librairie orientée objet. Sa grande force repose sur la portabilité. Voici quelques unes des caractéristiques principales listées dans la doc officielle :

  • Abstraction des types de données et conversion sur demande
  • Multiples options de fetch (par nom, par indice, les deux etc...)
  • Code d'erreurs portables
  • Emulation du mode prepare execute pour les SGBD qui ne le supportent pas
  • Emulation des séquences
  • Emulation des sous requêtes
  • Support des transactions
  • Support des Large Objects
  • Support des clés uniques, des index, des clés primaires
  • Emulation de l'auto increment
  • Emulation de LIMIT
  • Fonctions de lecture des informations schémas
  • Fonctions de gestion du SGBD (création, altération etc...)
  • Fonctions d'engenierie inversé a partir d'une base de données existantes

Le paquet est documenté avec phpdoc. Cette documentation technique de l'API peut être trouvé sur le site de PEAR.

III. Cas d'usage

III-1. Connexion

Pour se connecter par MDB2, il faut utiliser un DSN. La documentation la plus complète sur les DSN est celle de la documentation de PEAR::DB : http://pear.php.net/manual/fr/package.database.db.intro-dsn.php.
Pour résumer le DSN est une chaine de caractères comportant les parametres de connexion à votre base de données. Pour un utilisateur de mysql avec un accès classique, la chaine sera donc :

php
Sélectionnez

mysql://username:password@hostspec/database

Cependant ce formalisme vous permettra aussi d'utiliser des connexions par socket, des connexions sur port non standard, des options de lancement, une connexion ssl etc...
A noter que cette représentation est utilisée par les autres projets PEAR (Auth, LiveUser, LiveAdmin etc...)
La connexion se réalise ensuite avec la fonction statique MDB2::Connect. Celle ci accepte une chaine pour le $dsn ou un array. Les deux syntaxes suivantes sont donc équivalentes :

php
Sélectionnez

$dsn = 'mysql://someuser:apasswd@localhost/thedb';
$options = array(
    'debug'       => 2,
    'portability' => MDB2_PORTABILITY_ALL,
);

$db = & MDB2::connect($dsn, $options);
php
Sélectionnez

$dsn = array(
    'phptype'  => 'mysql',
    'username' => 'someuser',
    'password' => 'apasswd',
    'hostspec' => 'localhost',
    'database' => 'thedb',
);

$options = array(
    'debug'       => 2,
    'portability' => MDB2_PORTABILITY_ALL,
);

$db = & MDB2::connect($dsn, $options);

Le & signifie que vous récupérez une référence sur l'objet et non une copie. Il est important de le conserver. $options est facultatif, c'est un tableau associatif : parametre => valeur.
L'option la plus intéressante concerne la portabilité. Ici nous avons demandé d'activer toutes les options de portabilité (MDB2_PORTABILITY_ALL).
Voici quelques options possibles :

  • MDB2_PORTABILITY_DELETE_COUNT : force les requêtes de delete a renvoyer le nombre de lignes supprimé.
  • MDB2_PORTABILITY_EMPTY_TO_NULL : convertit les valeurs vides en valeur null.
  • MDB2_PORTABILITY_ERRORS : rend compatible les messages d'erreurs
  • MDB2_PORTABILITY_FIX_ASSOC_FIELD_NAMES : supprime les informations de table et base de données des index associatif
  • MDB2_PORTABILITY_FIX_CASE : convertit les noms des tables et des champs dans la casse définit par l'option "field_case"
  • MDB2_PORTABILITY_NONE : désactive les options de compatibilité
  • MDB2_PORTABILITY_RTRIM : supprime les blancs a droite des résultats

Remarque : Il est aussi possible de travailler avec un singleton :

php
Sélectionnez

$db =& MDB2::singleton($dsn); 

Ou avec une factory :

php
Sélectionnez

$db =& MDB2::factory($dsn); 

La déconnexion se fait avec la fonction disconnect :

php
Sélectionnez

$db->disconnect();

III-2. Requêtes

MDB2 vous permet d'interroger la base de données grace à différentes fonctions. On peut citer les classiques :
query (utilisé pour des select, update, insert, delete )
queryAll (utilisé pour des select, renvoie un tableau associatif avec les valeurs demandés)
queryCol (renvoie un tableau de valeurs correspondant a la colonne demandé)
queryOne (renvoie un seul résultat pour la première colonne, première ligne)
queryRow (renvoie la première ligne)
exec (utilisé pour des update, insert, delete ). Cette fonction vous renvoie le nombre de lignes affectés.

Pour toutes ces requêtes, il est très important d'avoir spécifié avant le mode de FETCH. Par exemple pour obtenir un tableau associatif :

php
Sélectionnez

$db->setFetchMode(MDB2_FETCHMODE_ASSOC);

Les modes suivants sont disponibles : MDB2_FETCHMODE_ORDERED, MDB2_FETCHMODE_ASSOC, MDB2_FETCHMODE_OBJECT , MDB2_FETCHMODE_DEFAULT , MDB2_FETCHMODE_FLIPPED.
Ainsi pour effectuer un update, nous ferons :

php
Sélectionnez

$result = $db->exec ( "UPDATE machin set truc = 'bidule'" );
// $result contient le nombre de lignes mise à jour.

Pour une selection :

php
Sélectionnez

$array = $db->queryAll ( "SELECT * from truc" );
foreach ($array as $record )
{
    print_r($record);
}

IV. Cas d'utilisation plus poussé

Nous pourrions nous arrêter la et pour la plupart des utilisateurs qui ne désire que utiliser les fonctionnalités de base, la lecture du second chapitre suffit. Cependant, pour aller plus loin, l'utilisation de MDB2 permet, quelque soit le système, d'utiliser des fonctionnalités intéressantes. Nous allons en détailler quelques unes.

IV-1. La gestion des erreurs

La gestion des erreurs respecte le formalisme de PEAR. Si vous avez déjà utilisé des paquets PEAR, vous ne serez pas dépaysé. La fonction statique isError permet de déterminer si une valeur de retour est une erreur MDB2. Cet objet erreur contient des informations intéressantes, notamment un message d'erreur.
Exemple :

php
Sélectionnez

    	$db = MDB2::connect($this->dsn,$options);
    	
        // Vérification des erreurs
        if (MDB2::isError($db)) 
        {
            echo $db->getDebugInfo().'<BR/>';
            echo $db->getMessage();
        }


    	$result = $db->query($sql);
    	
        // Vérification des erreurs
        if (MDB2::isError($result)) 
        {
            echo $result->getDebugInfo().'<BR/>';
            echo $result->getMessage();
        }

L'objet MDB2_Error dérive lui même de PEAR_Error c'est à dire qu'il possède les mêmes méthodes et les mêmes attributs. La gestion des erreurs est différente suivant un parametre qu'on peut positionner en début de programme.

php
Sélectionnez

PEAR::setErrorHandling(PEAR_ERROR_PRINT);

Le paramêtre passé a cette fonction va déterminer le comportement attendu en cas d'erreur.

  • PEAR_ERROR_CALLBACK
  • PEAR_ERROR_DIE
  • PEAR_ERROR_TRIGGER
  • PEAR_ERROR_RETURN
  • PEAR_ERROR_PRINT

Ces différentes options permettront en cas d'erreur d'appeler une fonction de callback, de stopper le script par un die, de renvoyer un code d'erreur ou d'afficher une erreur. La fonction de callback demande que l'on précise une fonction de callback en second paramêtre.

IV-2. Les transactions et les insertions par tableaux

Une transaction est un regroupement de requêtes qu'on désire exécuter si et seulement si aucune n'échoue. Au contraire d'une requête atomique exécute quelque soit le résultat des autres requêtes. Ces transactions sont émulées par MDB2, il faut cependant que votre SGBD les supporte.
Une transaction se débute par beginTransaction et se termine par la propagation des résultats (commit). Un retour en arrière peut se faire avec la fonction rollback.
Les transactions sont intéressantes à utiliser avec les insertions par tableau de valeurs (host array). Ici il s'agit de préparer une requête génériques et de donner un ensemble de valeur utilisé pour cette requête. Si votre SGBD est optimisé pour ce type d'opérations, celles ci sont bien plus rapides. Et le mode transactionnel vous assure de la réussite totale ou non de votre demande empêchant ainsi de vous retrouver dans un état intermédiaire.
Exemple d'utilisation :

php
Sélectionnez

    $alldata = array(
                     array(1, 'one', 'un'),
                     array(2, 'two', 'deux'),
                     array(3, 'three', 'trois'),
                     array(4, 'four', 'quatre')
    );
    $stmt = $mdb2->prepare('INSERT INTO numbers VALUES(?,?,?)', array('integer', 'text', 'text'), false);
    foreach ($alldata as $row) 
    {
        echo('running execute<br>');
        $stmt->bindParamArray($row);
        $stmt->execute();
   }

IV-3. Les sous requêtes

Les sous requêtes ne sont pas gérés par tous les SGBD mais MDB2 permet de les émuler :

php
Sélectionnez

    $sub_select = $mdb2->subSelect('SELECT test_name from test WHERE test_name = '.$mdb2->quote('gummihuhn', 'text'), 'text');
    $query_with_subselect = 'SELECT * FROM test WHERE test_name IN ('.$sub_select.')';
    $result = $mdb2->query($query_with_subselect);
    $array = $result->fetchAll();
    $result->free();

IV-4. La clause LIMIT

La clause LIMIT connu en mysql n'est pas disponible sur tous les SGBD. Elle est donc émulée par MDB2 de la facon suivante :

php
Sélectionnez

        $db->setLimit(3, 1);
        $result = $db->query('SELECT * FROM users');
        $numrows = $result->numRows(); // numrows doit etre égal a 3 dans notre exemple

IV-5. Les modules

IV-5-a. Gestion de base de données

Le paquet MDB2 peut utiliser des modules avec des fonctionnalités intéressantes. L'un d'entre eux permet la gestion de base de données :

php
Sélectionnez

$manager = & $db->loadModule('Manager', null, true);

Ce manager est une référence vers le gestionnaire interne de MDB2. Nous pourrons donc y faire référence de facon directe $manager-> ou bien par $db->manager->
Son API est décrite ici.
Ces fonctions vont nous permettre de créer une table a partir d'un tableau, de créer une base, de créer des séquences (émulé pour certains SGBD), de créer des contraintes etc... Ce gestionnaire est donc un parfait couteau suisse pour la gestion du schéma à proprement parler.
Voici par exemple une création de table :

php
Sélectionnez

$this->fields = array(
            'id' => array(
                'type'       => 'integer',
                'unsigned'   => true,
                'notnull'    => true,
                'default'    => 0,
            ),
            'textfield'      => array(
                'type'       => 'text',
                'length'     => 12,
            ),
            'booleanfield'   => array(
                'type'       => 'boolean',
            ),
            'decimalfield'   => array(
                'type'       => 'decimal',
            ),
            'floatfield'     => array(
                'type'       => 'float',
            ),
            'datefield'      => array(
                'type'       => 'date',
            ),
            'timefield'      => array(
                'type'       => 'time',
            ),
            'timestampfield' => array(
                'type'       => 'timestamp',
            ),
        );
        
$db->manager->createTable('exemple', $this->fields);

Nous remarquons dans cet exemple l'utilisation des types MDB2 pour la définition des champs, garantissant ainsi une portabilité sur l'ensemble des SGBD supporté.
Un autre exemple intéressant est l'utilisation de ces types MDB2 combiné avec une représentation sous forme de tableau pour exécuter des modifications de tables (ALTER):

php
Sélectionnez

        $changes = array(
            'add' => array(
                'quota' => array(
                    'type' => 'integer',
                    'unsigned' => 1,
                ),
                'note' => array(
                    'type' => 'text',
                    'length' => '20',
                ),
            ),
            'rename' => array(
                'sex' => array(
                    'name' => 'gender',
                    'definition' => array(
                        'type' => 'text',
                        'length' => 1,
                        'default' => 'M',
                    ),
                ),
            ),
            'change' => array(
                'id' => array(
                    'unsigned' => false,
                    'definition' => array(
                        'type'     => 'integer',
                        'notnull'  => false,
                        'default'  => 0,
                    ),
                ),
                'somename' => array(
                    'length' => '20',
                    'definition' => array(
                        'type' => 'text',
                        'length' => 20,
                    ),
                )
            ),
            'remove' => array(
                'somedescription' => array(),
            ),
            'name' => $newer,
        );

$result = $db->manager->alterTable('exemple2', $changes, true); 

Pour les personnes familières avec les séquences, MDB2 en propose aussi une émulation :

php
Sélectionnez

        $result = $db->manager->createSequence($seq_name);
        $array = $db->manager->listSequences();
        $id = $db->currID($seq_name); // récupération de la valeur courante de la séquence
        $id = $db->nextID ($seq_name); // incrémentation de la séquence
        $result = $db->manager->dropSequence($seq_name);

IV-5-b. Le module fonction

Inutile de s'attarder longuement sur ce module, il s'agit d'un ensemble de fonctions pratiques réécrites pour être portable sur chaque SGBD. Il s'agit de :

  • concat
  • executeStoredProc
  • functionTable
  • now
  • substring

IV-5-c. Le module reverse

Ce module permet de récupérer des informations sur le schéma. Il est souvent couplé avec le module Manager.

php
Sélectionnez

$table_info = $db->reverse->tableInfo('users');

IV-5-d. Les Large Objects

L'un des types de données sur lequel nous pouvons nous attarder est certainement celui des Large Objects. Ce type de champ permet de stocker des fichiers en base. 2 types de LOB sont utilisés :

  • les BLOBS (fichiers binaires)
  • les CLOBS (fichiers texte normaux)

Avec MDB2, ces objets peuvent être insérés en utilisant les requêtes préparées (voir le point 2).

php
Sélectionnez

$query = $db->prepareQuery('INSERT INTO files (id, b_data, c_data) VALUES (1, ?, ?)', array('blob', 'clob'), MDB2_PREPARE_MANIP, array('b_data', 'c_data'));

$db->bindParam(0, $binary_lob); 		// binary_lob contiendra le contenu d'un fichier (ouvert par fopen par exemple)
$db->bindParam(1, $character_lob);		// character_lob contiendra une longue chaine de caractère

$result = $db->executeQuery($query);

Pour la sélection d'un LOB, nous utiliserons la même méthode que pour une requête de sélection classique.

php
Sélectionnez

$result = $db->query('SELECT b_data, c_data FROM files WHERE id = 1',array(blob, clob));

$row = $result->fetchRow();        
$blob = $row[0];
$clob = $row[1];

IV-5-e. La maintenance de schéma indépendante du SGBD

Remarque : Le paquet MDB2_Schema est, à l'heure actuelle, toujours en version béta. Son utilisation n'est donc pas sans risque.
Grace a MDB2, il est désormais possible de représenter son schéma à partir d'un fichier descriptif en format XML. Le paquet Schema directement dépendant de MDB2 permet ainsi de maintenir sa base de données à partir d'un formalisme non lié au SGBD utilisé.
L'une de ces utilisations permet de faire de l'ingénierie inversée à partir d'une base existante :

php
Sélectionnez

$schema =& MDB2_Schema::factory($dsn );

Vous remarquerez ici que nous utilisons MDB2_Schema qui est un paquet utilisant MDB2.

php
Sélectionnez

$dump_config = array(
                'output_mode' => 'file',
                'output' => 'monfichier.xml');
$operation = $schema->dumpDatabase($dump_config, MDB2_SCHEMA_DUMP_STRUCTURE);

Nous pouvons noter ici qu'il est possible de récupérer la structure du schéma, les données, ou les deux grâce au second argument :

  • MDB2_SCHEMA_DUMP_STRUCTURE
  • MDB2_SCHEMA_DUMP_CONTENT
  • MDB2_SCHEMA_DUMP_ALL

Cette représentation sous forme XML utilisera les types de données MDB2. Ceux-ci seront convertis dans leurs équivalents sur le SGBD utilisé lors d'un éventuel chargement avec updateDatabase.
Ce chargement se fera avec la syntaxe suivante :

php
Sélectionnez

$operation = $schema->updateDatabase('monfichier.xml', 'monfichier.xml.old', array(), false );

Cette opération crééra le fichier 'monfichier.xml.old', copie conforme du fichier initial.
Attention, cette même fonction est utilisé pour la mise à jour du schéma. Si le fichier 'monfichier.xml.old' existe, updateDatabase établira les différences entre les deux fichiers et feras une mise a jour de la base (ALTER).

Liens utiles

Merci à Lukas Smith pour ces remarques qui ont permis de corriger certaines parties de cet article.