Pourquoi vous devriez utiliser PDO de PHP pour l’accès aux bases de données

On a tous aprpis en PHP à accéder aux bases de données en utilisant les extensions MySQL ou MySQLi. Depuis PHP 5.1, il existe une meilleure méthode. Les objets de données PHP (PDO) fournissent des méthodes pour les déclarations préparées et le travail avec des objets qui vous rendront beaucoup plus productif !

Introduction à PDO

Il ne tient pas compte de la syntaxe spécifique aux bases de données, mais permet de changer de base de données et de plate-forme sans trop de difficultés, simplement en modifiant la chaîne de connexion dans de nombreux cas.

Ce tutoriel n’a pas pour but d’être un mode d’emploi complet de SQL. Il est écrit principalement pour les personnes qui utilisent actuellement l’extension mysql ou mysqli pour les aider à faire le saut vers PDO, plus portable et plus puissant.

En ce qui concerne les opérations sur les bases de données en PHP, PDO offre de nombreux avantages par rapport à la syntaxe brute. En voici quelques-uns :

  • couche d’abstraction
  • syntaxe orientée objet
  • support des instructions préparées
  • meilleure gestion des exceptions
  • API sécurisées et réutilisables
  • support de toutes les bases de données courantes

Comment se connecter

Les différentes bases de données peuvent avoir des méthodes de connexion légèrement différentes. Ci-dessous, vous pouvez voir la méthode de connexion à certaines des bases de données les plus populaires. Vous remarquerez que les trois premières sont identiques, à l’exception du type de base de données, et que SQLite a sa propre syntaxe.

try {
  # MS SQL Server and Sybase with PDO_DBLIB
  $DBH = new PDO("mssql:host=$host;dbname=$dbname", $user, $pass);
  $DBH = new PDO("sybase:host=$host;dbname=$dbname", $user, $pass);
  
  # MySQL with PDO_MYSQL
  $DBH = new PDO("mysql:host=$host;dbname=$dbname", $user, $pass);
  
  # SQLite Database
  $DBH = new PDO("sqlite:my/database/path/database.db");
}
catch(PDOException $e) {
    echo $e->getMessage();
}

Veuillez prendre note du bloc try/catch. Vous devriez toujours envelopper vos opérations PDO dans un try/catch et utiliser le mécanisme d’exception – nous y reviendrons bientôt. En règle générale, vous n’établirez qu’une seule connexion – il y en a plusieurs dans la liste pour vous montrer la syntaxe. $DBH est l’abréviation de « database handle » (poignée de base de données) et sera utilisé tout au long de ce tutoriel.

Vous pouvez fermer une connexion en donnant la valeur null au handle.

# close the connection
$DBH = null;

Exceptions et PDO

PDO peut utiliser des exceptions pour gérer les erreurs, ce qui signifie que tout ce que vous faites avec PDO doit être enveloppé dans un bloc try/catch. Vous pouvez forcer PDO à utiliser l’un des trois modes d’erreur en définissant l’attribut error mode sur le handle de la base de données que vous venez de créer. Voici la syntaxe :

$DBH->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT );
$DBH->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_WARNING );
$DBH->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION );

Quel que soit le mode d’erreur défini, une erreur de connexion produira toujours une exception, et la création d’une connexion doit toujours être contenue dans un bloc try/catch.

Il s’agit du mode d’erreur par défaut. Si vous le laissez dans ce mode, vous devrez vérifier les erreurs de la manière à laquelle vous êtes probablement habitué si vous avez utilisé les extensions mysql ou mysqli. Les deux autres méthodes sont plus adaptées à la programmation DRY.

Ce mode émettra un avertissement PHP standard et permettra au programme de poursuivre son exécution. C’est utile pour le débogage.

PDO::ERRMODE_EXCEPTION

C’est le mode que vous souhaitez dans la plupart des situations. Il déclenche une exception, ce qui vous permet de gérer les erreurs avec élégance et de masquer les données qui pourraient aider quelqu’un à exploiter votre système. Voici un exemple d’utilisation des exceptions :

# connect to the database
try {
  $DBH = new PDO("mysql:host=$host;dbname=$dbname", $user, $pass);
  $DBH->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION );
  
  # UH-OH! Typed DELECT instead of SELECT!
  $DBH->prepare('DELECT name FROM people');
}
catch(PDOException $e) {
    echo "I'm sorry, Dave. I'm afraid I can't do that.";
    file_put_contents('PDOErrors.txt', $e->getMessage(), FILE_APPEND);
}

Il y a une erreur intentionnelle dans l’instruction select, ce qui provoque une exception. L’exception envoie les détails de l’erreur dans un fichier journal et affiche un message amical (ou pas si amical) à l’utilisateur.

Insertion et mise à jour en PDO

L’insertion de nouvelles données (ou la mise à jour de données existantes) est l’une des opérations de base de données les plus courantes. En utilisant PHP PDO, il s’agit normalement d’un processus en deux étapes. Tout ce qui est abordé dans cette section s’applique également aux opérations UPDATE et INSERT.

Voici un exemple du type d'insert le plus basique 
# STH means "Statement Handle"
$STH = $DBH->prepare("INSERT INTO folks ( first_name ) values ( 'Cathy' )");
$STH->execute();

Vous pouvez également effectuer la même opération en utilisant la méthode exec(), avec un appel de moins. Dans la plupart des cas, vous utiliserez la méthode longue pour tirer parti des instructions préparées. Même si vous ne l’utilisez qu’une seule fois, l’utilisation des instructions préparées vous protégera des attaques par injection SQL.

Requêtes préparées

Une instruction préparée est une instruction SQL précompilée qui peut être exécutée plusieurs fois en envoyant uniquement les données au serveur. Elle présente l’avantage supplémentaire de protéger automatiquement les données utilisées dans les espaces réservés contre les attaques par injection SQL.

Vous utilisez une instruction préparée en incluant des placeholders dans votre SQL. Voici trois exemples : un sans placeholders, un avec des placeholders non nommés et un avec des placeholders nommés.

# sans placeholders mais risque d'injection SQL
$STH = $DBH->prepare("INSERT INTO restaurants (name, addr, city) values ($name, $addr, $city)");
  
# sans placeholders
$STH = $DBH->prepare("INSERT INTO restaurants (name, addr, city) values (?, ?, ?)");
  
# avec placeholders
$STH = $DBH->prepare("INSERT INTO restaurants (name, addr, city) values (:name, :addr, :city)");

Vous voulez éviter la première méthode ; elle est ici à titre de comparaison. Le choix d’utiliser des espaces réservés nommés ou non nommés aura une incidence sur la façon dont vous définissez les données pour ces déclarations.

Placeholders sans nom

# assign variables to each place holder, indexed 1-3
$STH->bindParam(1, $name);
$STH->bindParam(2, $addr);
$STH->bindParam(3, $city);
  
# insert one row
$name = "Daniel"
$addr = "1 Wicked Way";
$city = "Arlington Heights";
$STH->execute();
  
# insert another row with different values
$name = "Steve"
$addr = "5 Circle Drive";
$city = "Schaumburg";
$STH->execute();

Il y a deux étapes ici. Tout d’abord, nous affectons des variables aux différents espaces réservés (lignes 2 à 4). Ensuite, nous attribuons des valeurs à ces espaces et exécutons l’instruction. Pour envoyer un autre ensemble de données, il suffit de modifier les valeurs de ces variables et d’exécuter à nouveau l’instruction.

Cela vous semble-t-il un peu compliqué pour les instructions comportant un grand nombre de paramètres ? C’est le cas. Cependant, si vos données sont stockées dans un tableau, il existe un raccourci facile :

# the data we want to insert
$data = array('Cathy', '9 Dark and Twisty Road', 'Cardiff');
  
$STH = $DBH->prepare("INSERT INTO folks (name, addr, city) values (?, ?, ?)");
$STH->execute($data);

Les données du tableau s’appliquent aux espaces réservés dans l’ordre. $data[0] va dans le premier espace réservé, $data[1] dans le second, etc. Cependant, si les index de votre tableau ne sont pas dans l’ordre, cela ne fonctionnera pas correctement, et vous devrez réindexer le tableau.

Placeholders nommés

Vous pouvez probablement deviner la syntaxe, mais voici un exemple :


$STH->bindParam(':name', $name);

Vous pouvez également utiliser un raccourci ici, mais il fonctionne avec les tableaux associatifs. Voici un exemple :

$data = array( 'name' => 'Cathy', 'addr' => '9 Dark and Twisty', 'city' => 'Cardiff' );
  

$STH = $DBH->prepare("INSERT INTO folks (name, addr, city) value (:name, :addr, :city)");
$STH->execute($data);

Les clés de votre tableau ne doivent pas nécessairement commencer par un deux-points, mais elles doivent correspondre aux caractères de remplacement nommés. Si vous disposez d’un tableau de tableaux, vous pouvez les parcourir par itération et appeler simplement la commande execute avec chaque tableau de données.

Une autre caractéristique intéressante des caractères de remplacement nommés est la possibilité d’insérer des objets directement dans votre base de données, en supposant que les propriétés correspondent aux champs nommés. Voici un exemple d’objet et la manière d’effectuer l’insertion :

class person {
    public $name;
    public $addr;
    public $city;
  
    function __construct($n,$a,$c) {
        $this->name = $n;
        $this->addr = $a;
        $this->city = $c;
    }
    # etc ...
}
  
$cathy = new person('Cathy','9 Dark and Twisty','Cardiff');
  
# here's the fun part:
$STH = $DBH->prepare("INSERT INTO folks (name, addr, city) value (:name, :addr, :city)");
$STH->execute((array)$cathy);

Le fait de convertir l’objet en un tableau dans l’exécution signifie que les propriétés sont traitées comme des clés de tableau.

Sélection des données

Les données sont obtenues par l’intermédiaire de la méthode ->fetch(), une méthode de votre handle de déclaration. Avant d’appeler fetch, il est préférable d’indiquer à PDO comment vous souhaitez que les données soient récupérées. Vous avez les options suivantes :

  • PDO::FETCH_ASSOC : renvoie un tableau indexé par le nom de la colonne.
  • PDO::FETCH_BOTH (par défaut) : renvoie un tableau indexé à la fois par le nom et le numéro de la colonne.
  • PDO::FETCH_BOUND : assigne les valeurs de vos colonnes aux variables définies avec la méthode ->bindColumn().
  • PDO::FETCH_CLASS : assigne les valeurs de vos colonnes aux propriétés de la classe nommée. Il créera les propriétés si les propriétés correspondantes n’existent pas.
  • PDO::FETCH_INTO : met à jour une instance existante de la classe nommée.
  • PDO::FETCH_LAZY : combine PDO::FETCH_BOTH/PDO::FETCH_OBJ, en créant les noms des variables objet au fur et à mesure de leur utilisation.
  • PDO::FETCH_NUM : retourne un tableau indexé par le numéro de colonne.
  • PDO::FETCH_OBJ : retourne un objet anonyme dont les noms de propriétés correspondent aux noms des colonnes.

En réalité, il y en a trois qui couvrent la plupart des situations : FETCH_ASSOC, FETCH_CLASS, et FETCH_OBJ. Afin de définir la méthode de récupération, la syntaxe suivante est utilisée :

$STH->setFetchMode(PDO::FETCH_ASSOC);

Vous pouvez également définir le type de récupération directement dans l’appel de la méthode ->fetch().

FETCH_ASSOC

This fetch type creates an associative array, indexed by column name. This should be quite familiar to anyone who has used the mysql/mysqli extensions. Here’s an example of selecting data with this method:

$STH = $DBH->query('SELECT name, addr, city from folks');
  
# setting the fetch mode
$STH->setFetchMode(PDO::FETCH_ASSOC);
  
while($row = $STH->fetch()) {
    echo $row['name'] . "\n";
    echo $row['addr'] . "\n";
    echo $row['city'] . "\n";
}

La boucle while continuera à parcourir l’ensemble des résultats, ligne par ligne, jusqu’à ce qu’elle soit complète.

FETCH_OBJ

Ce type de récupération crée un objet de la classe std pour chaque ligne de données récupérées. Voici un exemple :

# creating the statement
$STH = $DBH->query('SELECT name, addr, city from restaurants');
  
# setting the fetch mode
$STH->setFetchMode(PDO::FETCH_OBJ);
  
# showing the results
while($row = $STH->fetch()) {
    echo $row->name . "\n";
    echo $row->addr . "\n";
    echo $row->city . "\n";
}

FETCH_CLASS

Cette méthode de récupération vous permet de récupérer des données directement dans une classe de votre choix. Lorsque vous utilisez FETCH_CLASS, les propriétés de votre objet sont définies AVANT que le constructeur ne soit appelé. Relisez bien ce passage, c’est important. Si les propriétés correspondant aux noms des colonnes n’existent pas, elles seront créées (comme publiques) pour vous.

Cela signifie que si vos données ont besoin d’une transformation après leur sortie de la base de données, celle-ci peut être effectuée automatiquement par votre objet lors de sa création.

Par exemple, imaginez une situation où l’adresse doit être partiellement masquée pour chaque enregistrement. Nous pourrions le faire en opérant sur cette propriété dans le constructeur. Voici un exemple :

class secret_person {
    public $name;
    public $addr;
    public $city;
    public $other_data;
  
    function __construct($other = '') {
        $this->address = preg_replace('/[a-z]/', 'x', $this->address);
        $this->other_data = $other;
    }
}

Lorsque des données sont extraites de cette classe, toutes les lettres minuscules a-z de l’adresse sont remplacées par la lettre x. Maintenant, l’utilisation de la classe et la transformation des données sont totalement transparentes :

$STH = $DBH->query('SELECT name, addr, city from restaurants') ;
$STH->setFetchMode(PDO::FETCH_CLASS, 'secret_person') ;
  
while($obj = $STH->fetch()) {
    echo $obj->addr ;
}

Si l’adresse est ‘5 Rosebud’, vous verrez ‘5 Rxxxxxx’ en sortie. Bien sûr, il peut y avoir des situations où vous voulez que le constructeur soit appelé avant que les données soient assignées. PDO vous couvre également pour cela.

Quelques autres méthodes utiles

Bien qu’il ne s’agisse pas de tout couvrir dans PDO (c’est une extension énorme !), il y a quelques méthodes supplémentaires que vous voudrez connaître afin de faire des choses de base avec PDO.

	
$DBH->lastInsertId();

La méthode ->lastInsertId() est toujours appelée sur l’identifiant de la base de données, et non sur celui de l’instruction, et renvoie l’identifiant auto-incrémenté de la dernière ligne insérée par cette connexion.

$DBH->exec('DELETE FROM folks WHERE 1');
$DBH->exec("SET time_zone = '-8:00'");

La méthode ->exec() est utilisée pour les opérations qui ne peuvent pas renvoyer de données autres que les lignes concernées. Les exemples ci-dessus sont deux exemples d’utilisation de la méthode exec.

$safe = $DBH->quote($unsafe);

La méthode ->quote() cite les chaînes de caractères afin qu’elles puissent être utilisées en toute sécurité dans les requêtes. C’est votre solution de repli si vous n’utilisez pas d’instructions préparées.

$rows_affected = $STH->rowCount();

La méthode ->rowCount() renvoie un nombre entier indiquant le nombre de lignes affectées par une opération. Dans au moins une version connue de PDO, cette méthode ne fonctionnait pas avec les instructions select. Cependant, elle fonctionne correctement dans les versions PHP 5.1.6 et supérieures.

Si vous rencontrez ce problème et que vous ne pouvez pas mettre à jour PHP, vous pouvez obtenir le nombre de lignes avec la méthode suivante :

$sql = "SELECT COUNT(*) FROM restaurants";
if ($STH = $DBH->query($sql)) {
    # check the row count
    if ($STH->fetchColumn() > 0) {
  
    # il n'y a pas de données
    }
    else {
        echo "No rows matched the query.";
    }
}