Donnez-moi votre avis : est-ce que vous utilisez déjà un système équivalent ? Qu'est-ce que vous en attend(ri)ez ? Que pensez-vous de cette implémentation ?
J'ai découvert ça hier avec le Zend Framework, qui hélas n'implémente pas encore cette fonctionnalité fabuleuse.
L'idée c'est de travailler avec un objet au lieu de travailler avec des requêtes.
Un exemple vaut mieux qu'un long discours : je veux faire un agenda. Et voilà j'ai l'idée saugrenue (pour les besoins de l'exemple quoi...) de faire une table "Adresses" où je vais attribuer un id à chaque adresse, et une tables "Personnes" où je vais reprendre l'id de "Adresses".
Format des tables :
Adresses ( id_adresse INT, txt_adresse TEXT )
Personnes ( id_personne INT, nom VARCHAR(25), adresse INT ) PRIMARY KEY (id_personne).
Exemple d'utilisation idéale :
$personne = new Personne(1); // on récupère la personne d'id=1
echo $personne->adresse->txt_adresse; // l'ORM a fait immédiatement le lien
// et transformé le champ "adresse" de $personne en objet de type "Adresse".
Pour ce faire il faudrait définir plusieurs choses :- la façon de manipuler la base de donées bien sûr
- le nom des tables à manipuler
- les relations entre les tables
Voilà comment on procède avec ce que j'ai développé :
$db = new SQLiteDB("test.db"); // j'ouvre la connexion
// je définis une classe "Adresse" héritant de "DataObject".
// Implicitement cette classe est associé à la table "Adresses" (ajout d'un 's' en fin de nom).
// la clé primaire sera récupérée en analysant la table
class Adresse extends DataObject { }
// je définis une classe "Personne" héritant de "DataObject".
// Implicitement cette classe est associé à la table "Personnes" (ajout d'un 's' en fin de nom).
// Je précise que le champ "adresse" est une relation sur le DataObject "Adresse"
// L'objet fera automatiquement le lien via la clé primaire de "Adresses".
class Personne extends DataObject {
var $_relation = array('adresse' => 'Adresse');
}
// je récupère mon objet Personne, en cherchant la ligne dont la clé primaire vaut 2
$personne = new Personne($db, 2);
// j'affiche l'adresse de la personne
// $personne->adresse au lieu d'avoir une valeur scalaire a obtenu l'objet Adresse correspondant
echo $personne->adresse->txt_adresse;
// on modifie l'adresse
$personne->adresse->txt_adresse = "Ma nouvelle adresse";
// on sauvegarde les modifs : les modifications de $personne
// seront automatiquement reportés sur la table "Adresses" grâce au système de relation.
$personne->save();
Il ne s'agit que d'un premier jet, et j'ai eu la bêtise de passer par mon propre wrapper DB. Je vais donc reprendre l'implémentation de la classe DataObject :- en utilisant le wrapper PEAR :: Db
- en gérant les primary keys sur plusieurs champs
- De base, pour définir un DataObject il faut donc déclarer une classe héritant de DataObject, ayant pour nom celui de la table sans le 's' final.
- Pour modifier le nom de la table associé (si on ne veut pas qu'un 's' soit simplement ajouté au nom de la classe), il faut ajouter var $_table = 'NomDeMaTable'; dans la déclaration de la classe.
- Pour ajouter une ou plusieurs relations il faut ajouter var $_relation = array( relation1, relation2, ... ); dans la déclaration de la classe. Une relation est une assignation 'champ' => 'MonDataObject' et signifie que le champ "champ" de l'objet sera remplacé par un objet de la classe "MonDataObject", en faisant une recherche sur sa clé primaire avec la valeur originale du champ "champ".
- Pour préciser la clé primaire (si elle n'existe pas réellement dans le schéma de la table, ou si le programme n'arrive pas à la récupérer), il faut ajouter var $_primary = 'MaCléPrimaire'; dans la déclaration de la classe.
Un objet dérivant de DataObject dispose de 3 méthodes :
- le constructeur ($db [, $id])
$db est le wrapper pour la base de données.
$id est la valeur de clé primaire à rechercher (cf. get). - get($id)
$id est la valeur de clé primaire à rechercher.
Cette méthode va faire une recherche dans la table, et remplir les attributs de l'objets selon les champs récupérés. - save()
Cette méthode va mettre à jour la base en fonction des valeurs modifiées des attributs.
class DataObject {
// paramètres personnalisables
var $_table = null;
var $_primary = null;
var $_relation = array();
// attributs privés
var $_fields = array();
var $_db = null;
// constructeur
function DataObject($db, $id = null) {
// le wrapper Db
$this->_db = $db;
// si on n'a pas forcé le nom de la table, on ajoute un 's' au nom de la classe
if ($this->_table === null)
$this->_table = get_class($this).'s';
// on récupère les infos de la table
$table = $db->table($this->_table);
if ($table) {
// la clé primaire associée à la table, si elle n'est pas forcée
if (!$this->_primary)
$this->_primary = $table->primaryKey;
// la liste des champs
foreach ($table->fields as $field)
$this->_fields[] = $field->name;
}
// on appelle directement get($id) si nécessaire
if ($id !== null)
$this->get($id);
}
// récupérer les infos d'un objet selon son identifiant
// on peut spécifier ici une clé particulière si on n'a pas forcé la valeur de $_primary
// et si sa valeur automatique ne correspond pas à ce que l'on souhaite (utile pour faire
// une recherche sur un critère particulier par exemple)
function get($id, $key = null) {
if (!$key && !$this->_primary)
return false;
if (!$key)
$key = $this->_primary;
// on exécute une requкte SELECT toute simple, limitée à 1 résultat
$sql = 'SELECT * FROM '.$this->_table.' WHERE '.$key.' = '.$this->_db->quote($id).' LIMIT 1';
if (!$this->_db->query($sql))
return false;
$rows = $this->_db->fetchAll();
foreach ($rows[0] as $prop => $val) if (in_array($prop,$this->_fields)) {
// s'il y a une relation sur ce champ
if (isset($this->_relation[$prop]) && class_exists($this->_relation[$prop])) {
// on crée le DataObject correspondant, associé au mкme wrapper Db
$obj = new $this->_relation[$prop]($this->_db);
// on remplit l'objet avec le contenu actuel du champ
$obj->get($val);
// le champ correspondant devient l'objet au lieu d'une valeur scalaire simple
$val = $obj;
}
$this->$prop = $val;
}
}
// sauvegarde les modifications dans la base de données
function save() {
// création d'une requкte REPLACE
$sql = 'REPLACE INTO '.$this->_table.' ('.join(',',$this->_fields).') VALUES (';
$values = array();
foreach ($this->_fields as $field) {
// si le champ est un objet, il s'agit de la résolution d'une relation
if (is_object($this->$field) && method_exists($this->$field,'save')) {
// on propage donc la sauvegarde des modifs
$this->$field->save();
// on remplace le champ objet par la valeur scalaire de la clé primaire
$value = $this->$field->{$this->$field->_primary};
}
else $value = $this->$field;
$values[] = $this->_db->quote($value);
}
$sql .= join(',',$values).')';
$this->_db->query($sql);
}
}
ToDo (dans l'ordre) :
- Être bien sûr que je ne fais pas un gros doublon avec PEAR :: Db_DataObject (mais de toute évidence la philosophie n'est pas la même Db_DataObject demande beaucoup de code, et ne gère pas les relations entre tables). Et le cas échéant : trouver un autre nom que DataObject.
- Utiliser le wrapper PEAR :: Db plutôt que mon Db bricolé maison spécial SQLite.
- Gérer les clés primaires sur plusieurs champs.
- Optimiser la méthode save() en profitant des fonctionnalités objet de PHP5 (en particulier __set) pour savoir si un objet a réellement besoin d'être sauvegardé en base de données ou non (grosse optimisation).
- Optimiser la méthode get() en générant un gros SELECT avec les JOIN qui vont bien.
- Rendre le paramètre $id de get() optionnel afin d'autoriser la recherche selon les propriétés déjà correctement fournies dans l'objet.
- Débugger à mort.
- Faire une belle doc.
- Proposer le package à PEAR.
- Rire de Zend Framework qui donne des idées trop tôt, finalement pas assez poussées, et surtout pas implémentées (si jamais c'est encore le cas).