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.



Posté par Laurentj - Il y'a environ about 1 year
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
ç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
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
// 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
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