Hydrater un objet PHP à la volée avec PDO

Le mardi 21 septembre 2010 dans PHP / AFUP, Bases de données

PDO est la couche native d'accès aux bases de données depuis PHP 5.1. Il ne s'agit pas véritablement d'une couche d'abstraction de base de données au sens propre du terme car il est toujours de la responsabilité du développeur d'écrire lui-même ses requêtes SQL. De plus, il manque encore quelques méthodes à l'API de PDO pour en faire une véritable couche d'abstraction de base de données comme l'est par exemple le composant DBAL de Doctrine2. Néanmoins, PDO offre une fonctionnalité très intéressante dont l'usage est beaucoup moins documenté sur Internet.

L'API de PDO offre plusieurs méthodes capables de récupérer un jeu de résultats ou bien un enregistrement unique dans une table. Il s'agit par exemple des méthodes PDO::query(), PDOStatement::fetch() ou PDOStatement::fetchAll() pour les plus connues.

Le listing ci-dessous présente quelques exemples d'utilisation de ces méthodes en guise de rappel.

<?php
 
$dbh = new PDO('mysql:host=localhost;dbname=test', 'root', 'pwd');
 
$statement = $dbh->query('SELECT id, name, birthdate FROM student');
 
// Returns a mixed result set.
// Each column value can be accessed with a numeric index or associative key
$students  = $statement->fetchAll();
 
echo 'Name: ', $students[0][1];
echo 'Name: ', $students[0]['name'];
 
// The result set is only composed of associative arrays
$students  = $statement->fetchAll(PDO::FETCH_ASSOC);
 
echo 'Name: ', $students[0]['name'];
 
// The result set is only composed of indexed arrays
$students  = $statement->fetchAll(PDO::FETCH_NUM);
 
echo 'Name: ', $students[0][1];
 
// The result set is composed of stdClass objects
$students  = $statement->fetchAll(PDO::FETCH_OBJ);
 
echo 'Name: ', $students[0]->name;

Par défaut, PDO retourne les jeux de résultats sous forme de tableaux. Les tableaux sont des structures de données faciles à manipuler grâce aux nombreuses fonctions offertes par PHP. Néanmoins, leur utilisation s'en trouve vite limitée lorsqu'il s'agit de représenter des données plus complexes comme celles d'une base de données ayant des relations les unes avec les autres.

Les objets métier offrent une manière plus naturelle et flexible de représenter l'information. Il est en effet plus facile pour un développeur de percevoir un enregistrement d'une base de données sous la forme d'un objet métier PHP. Un objet encapsule les propriétés de l'enregistrement mais il a l'avantage, par rapport aux tableaux, de pouvoir aisément appliquer des traitements sur ces données grâce aux méthodes.

Comment est-il possible de convertir un modèle orienté objet avec un modèle relationnel de base de données en PHP ? C'est le rôle des bibliothèques d'ORM telles que Propel et Doctrine qui offrent une API de haut niveau permettant aux développeurs d'abstraire la complexité du langage SQL et du moteur de base de données connecté. Pour y parvenir, ces couches intermédiaires entre le code du développeur et la base de données transforme un enregistrement SQL sous la forme d'un objet métier PHP. Propel et Doctrine sont deux couches d'abstraction de base de données reposant sur PDO.

PDO offre une manière simple et efficace de transformer les enregistrements d'une table SQL sous la forme d'objets métiers PHP. Par défaut, les objets créés sont de type stdClass, la classe native de PHP dont tous les objets héritent. Le listing de code ci-dessous montre comment rapatrier des objets stdClass en utilisant la constante PDO::FETCH_OBJ dans les méthodes de récupération de jeu de résultats.

<?php
 
$dbh = new PDO('mysql:host=localhost;dbname=test', 'root', 'pwd');
 
foreach ($dbh->query('SELECT id, name, birthdate FROM student', PDO::FETCH_OBJ) as $student)
{
  // $student is of type stdClass
  echo $student->name;
  echo $student->id;
  echo $student->birthdate;
}

En lisant attentivement la documentation officielle de PHP, on découvre que l'on peut en effet fournir un autre argument correspondant au nom de la classe PHP à utiliser pour créer des objets de ce type à la volée. La constante spécifiée en second argument est aussi remplacée par la constante PDO::FETCH_CLASS.

<?php
 
$dbh = new PDO('mysql:host=localhost;dbname=test', 'root', 'pwd');
 
$stmt = $dbh->prepare('SELECT id, name, birthdate FROM student WHERE id = :id');
$stmt->bindValue(':id', 3);
$stmt->execute();
 
// $student is an objet of type Student
$student = $stmt->fetch(PDO::FETCH_CLASS, 'Student');
 
echo $student->getName();
echo $student->getBirthdate();
echo $student->makeHomework();

Pour que PDO puisse automatiquement instancier et initialiser la classe Student, cette dernière doit au préalable être définie et incluse dans le script. De plus, PDO va chercher à initialiser des propriétés publiques de l'objet dont le nom correspond à une clé dans le tableau du jeu de résultat.

Construisons simplement la classe Student associée à une table SQL student composée de trois champs: id, name et birthdate. En créant une classe PHP Student définissant trois propriétés publiques de même nom que les colonnes de la table, PDO sera capable de retourner des jeux de résultats composés d'objets Student initialisés et prêts à l'emploi.

<?php
 
class Student
{
  public $id;
  public $name;
  public $birthdate;
}

C'est tout ! Avec seulement ces quelques lignes, PDO sera capable d'instancier la classe Student et d'hydrater les propriétés publiques. Bien entendu, il convient ensuite de créer autant de méthodes que nécessaire afin d'offrir des manières simples de modifier les données de l'objet.

La visibilité publique n'est pas la meilleure car elle rompt le principe d'encapsulation. Les propriétés de l'objet ne devraient pas être accessibles directement. Il convient donc de transformer la visibilité publique en visibilité privée et d'ajouter des méthodes supplémentaires à la classe afin de garantir le contrôle d'accès aux propriétés de l'objet.

<?php
 
class Student
{
  private $id;
  private $name;
  private $birthdate;
 
  public function getId()
  {
    return $this->id;
  }
 
  public function getName()
  {
    return $this->name;
  }
 
  public function getBirthdate()
  {
    return $this->birthdate;
  }
}

Le problème à présent, c'est que PDO est incapable d'hydrater les propriétés internes de l'objet lorsqu'il le crée à la volée car les attributs sont déclarés privés. Pour y parvenir, il suffit d'avoir recours à la méthode magique __set() de PHP qui permettra à PDO d'accéder aux propriétés privées comme si elles étaient publiques.

Lorsqu'elle est définie dans la classe, la méthode magique __set() est invoquée automatiquement par PHP quand il y a une tentative d'accès à une propriété inexistante. Cette méthode accepte deux arguments : le nom de la propriété que l'on a cherché à écrire et sa valeur. En implémentant la méthode magique __set() à la classe Student, PDO sera désormais capable d'initialiser chacune des propriétés internes de l'objet.

<?php
 
class Student
{
  // ...
  private $virtualColumns = array();
 
  public function __set($key, $value) {
 
  // Set a real property
  if (property_exists($this, $key))
  {
    $this->$key = $value;
  }
  else
  {
    // Or set a virtual property
    $this->virtualColumns[$key] = $value;
  }
}

Maintenant, PDO est capable d'initialiser aussi bien les valeurs correspondantes à des colonnes réelles dans la table mais également les champs virtuels qui sont calculés dans une requête SQL. Par exemple, un comptage avec la fonction SQL COUNT().

Grâce à cette petite fonctionnalité pratique de PDO, vous pourrez très facilement créer des objets PHP à la volée correspondant à vos enregistrements de base de données. Rien ne vous empêche à présent de créer votre propre moteur d'ORM afin d'abstraire les requêtes SQL de modification ou de création d'enregistrement en les encapsulant dans vos objets métiers.

Commentaires

Posté par Laurentj - Il y'a environ about 1 year

Je ne vois pas en quoi, avec __set, tu empêches quelconque classe d'accéder aux propriétés et de changer leurs valeurs. Autant donc laisser ces propriétés publiques et ne pas avoir tout ces getters et __set. ça sera beaucoup moins de code à écrire, à parser, à exécuter et à maintenir.

Un peu de pragmatisme ne fait pas de mal parfois, pour éviter de faire du code bloatware. On fait du PHP, pas du JAVA. ces deux langages n'ont pas les mêmes contraintes opérationnelles. Il ne faut pas l'oublier.

Posté par desfrenes - Il y'a environ about 1 year

"Je ne vois pas en quoi, avec __set, tu empêches quelconque classe d'accéder aux propriétés et de changer leurs valeurs."

ça dépend de ce qu'on met dans le __set... dans le cas présent pas trop d'intérêt, mais on pourrait y faire des contrôles plus poussés, exclure certaines propriétés, appliquer un formatage, déclencher une action, etc. Autant de choses impossibles avec des propriétés publiques. Par contre si on par là-dessus je ne vois pas trop pourquoi conserver du getXXX, autant utiliser __get...

Posté par Hugo Hamon - Il y'a environ about 1 year

Effectivement je suis d'accord avec vous, le __set() ici n'est pas utile si on se limite seulement aux propriétés. L'avantage d'avoir le __set() c'est que l'on peut filtrer les informations qui proviennent du result set. Il y a des données du result set que l'on ne veut pas forcément inclure dans l'objet Student.

Par exemple, si j'ai cette requête :

SELECT s.id, s.name, s.birthdate, COUNT(c.id) AS nb_courses FROM student s LEFT JOIN course c ON c.student_id = s.id;

Je fais quoi de nb_courses dans mon result set ? Je serai bien obligé de faire quelque chose dans __set() pour choisir si je veux le stocker quelque part dans mon objet ou bien l'ignorer totalement.

Posté par moi-meme - Il y'a environ about 1 year

public function __set($key, $value) {

// Set a real property
if (property_exists($this, $key))
{
$this->$key = $value;
}

Personnellement, j'aurais rajouté si la propriété existe
$function = set . ucfirst($key);
$this->$function($value);

En prenant bien soin de faire des méthodes setName($val) par exemple, qui valideront les données :

function setName($val) {
if (!is_string($val)) throw new PrivateException ("Format invalide");
if (!preg_match("/[a-z éè-]/i",$val)) throw new PublicException ("Le nom doit comporter que des chaines alpha éè et tiret.");
$this->name = $val;
}

Posté par moi-meme-encore - Il y'a environ about 1 year

"On fait du PHP, pas du JAVA. ces deux langages n'ont pas les mêmes contraintes opérationnelles"

C'est sûr, mais après tout, il faut bien les valider qq part les données. Plutot que chaque formulaire vérifie les données, on peut donner cette charge à la classe qui va les enregistrer. C'est pas si illogique puisque si le nom par exemple fait 30 caractères mais que la table est un varchar de 20, qui est le mieux placer pour controler ça ? A priori, aucun controleur ni aucune vue n'est censée connaître la longueur max d'un champ dans une table. Donc, avoir des getters et setters c'est vraiment nécessaire, et ça fait remonter très rapidement les erreurs (par exemple, si le nom est un objet, on a une grave erreur, alors que si on controle pas, on peut passer 2h à chercher pk les choses ne vont pas).

Ce n'est pas seulement pour le coup un problème d'encapsulation et de cohérence au sein même d'un objet, mais bien les mêmes problèmes à un degré superieur : au sein même d'une table.

Enfin, c'est ma vision :p

Laisser un commentaire

Votre commentaire