Implémentation d'un ORM en PHP

Administrateur PHPfrance
Administrateur PHPfrance | 3131 Messages

07 mars 2006, 03:35

ORM pour Object-Relational-Mapping.

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.
L'idée étant comme dans le framework Zend de conserver le plus possible d'implicite, j'ai éviter de multiplier les méthodes et les "customisations" possibles. On n'a donc que ces 3 éléments à modifier, sachant que seul l'attribut $_relation devrait être précisé dans l'idéal (encore que $_table peut se justifier facilement car on n'est pas obligé d'avoir pris le nom de la table idéal).

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.
Le code actuel de la méthode ne vous apportera finalement pas grand chose puisqu'il dépend fortement du wrapper Db utilisé, et il sera donc remanié pour coller à celui de PEAR :
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).

Administrateur PHPfrance
Administrateur PHPfrance | 3131 Messages

07 mars 2006, 05:01

Bon ben putain d'insomnies, j'ai donc repris le code avec le wrapper de PEAR :: Db. Et ça donner ceci (script complet d'exemple).

classe DataObject
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->tableInfo($this->_table);
        if ($table) {
            // la clé primaire associée à la table, si elle n'est pas forcée
            if (!$this->_primary) {
                foreach ($table as $field) if (preg_match('/primary_key/',$field['flags']))
                    $this->_primary = $field['name'];
            }
            // la liste des champs
            foreach ($table as $field)
                $this->_fields[] = $field['name'];
        }
        // on appelle directement get($id) si nécessaire
        if ($id !== null && $this->_primary)
            $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
        $id = $this->_db->quote($id);
        $key = $this->_db->quoteIdentifier($key);
        $table = $this->_db->quoteIdentifier($this->_table);
        $sql = "SELECT * FROM $table WHERE $key = $id LIMIT 1";
        $row =& $this->_db->getRow($sql, array(), DB_FETCHMODE_ASSOC);
        if (PEAR::isError($row) || !$row)
            return false;
        foreach ($row 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;
        }
        return true;
    }

    // sauvegarde les modifications dans la base de données
    function save() {
        // création d'une requête REPLACE
        $table = $this->_db->quoteIdentifier($this->_table);
        $fields = array();
        foreach ($this->_fields as $field)
            $fields[] = $this->_db->quoteIdentifier($field);
        $sql = 'REPLACE INTO '.$table.' ('.join(',',$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).')';
        $res = $this->_db->query($sql);
        return $res && !PEAR::isError($res);
    }

}
Exemple
<?php

include '/path/to/PEAR/DB.php';

$db =& DB::connect("sqlite:///test.db");
$db->setFetchMode(DB_FETCHMODE_ASSOC);

// création des données de démo
$db->query('DROP TABLE '.$db->quoteIdentifier('Adresses'));
$db->query('DROP TABLE '.$db->quoteIdentifier('Personnes'));
$db->query('CREATE TABLE '.$db->quoteIdentifier('Adresses').' ('.$db->quoteIdentifier('id_adresse').' INTEGER PRIMARY KEY,'.$db->quoteIdentifier('txt_adresse').' TEXT)');
$db->query('CREATE TABLE '.$db->quoteIdentifier('Personnes').' ('.$db->quoteIdentifier('id_personne').' INTEGER PRIMARY KEY,'.$db->quoteIdentifier('nom').' TEXT,'.$db->quoteIdentifier('adresse').' INT)');
$db->dropSequence('Adresses');  $db->createSequence('Adresses');
$db->dropSequence('Personnes'); $db->createSequence('Personnes');
$db->query('INSERT INTO '.$db->quoteIdentifier('Adresses').' ('.$db->quoteIdentifier('id_adresse').','.$db->quoteIdentifier('txt_adresse').') VALUES (?,?)', array($id_adresse=$db->nextId('Adresses'), 'Adresse1'));
$db->query('INSERT INTO '.$db->quoteIdentifier('Personnes').' ('.$db->quoteIdentifier('id_personne').','.$db->quoteIdentifier('nom').','.$db->quoteIdentifier('adresse').') VALUES (?,?,?)', array($db->nextId('Personnes'), 'Nom1', $id_adresse));
$db->query('INSERT INTO '.$db->quoteIdentifier('Adresses').' ('.$db->quoteIdentifier('id_adresse').','.$db->quoteIdentifier('txt_adresse').') VALUES (?,?)', array($id_adresse=$db->nextId('Adresses'), 'Adresse2'));
$db->query('INSERT INTO '.$db->quoteIdentifier('Personnes').' ('.$db->quoteIdentifier('id_personne').','.$db->quoteIdentifier('nom').','.$db->quoteIdentifier('adresse').') VALUES (?,?,?)', array($db->nextId('Personnes'), 'Nom2', $id_adresse));
$db->query('INSERT INTO '.$db->quoteIdentifier('Adresses').' ('.$db->quoteIdentifier('id_adresse').','.$db->quoteIdentifier('txt_adresse').') VALUES (?,?)', array($id_adresse=$db->nextId('Adresses'), 'Adresse3'));
$db->query('INSERT INTO '.$db->quoteIdentifier('Personnes').' ('.$db->quoteIdentifier('id_personne').','.$db->quoteIdentifier('nom').','.$db->quoteIdentifier('adresse').') VALUES (?,?,?)', array($db->nextId('Personnes'), 'Nom3', $id_adresse));

// fonction pour afficher le contenu de nos deux tables
function afficherContenuTable($table) {
    global $db;
    $rows = $db->getAll('SELECT * FROM '.$table);
    echo "== Contenu de '$table' ==\r\n";
    foreach ($rows as $i => $row) {
        echo "\r\n$i";
        foreach ($row as $field => $value)
            echo "\t$field = $value\r\n";
    }
    echo "\r\n";
}
function afficherContenuTables() {
    afficherContenuTable('Personnes');
    afficherContenuTable('Adresses');
}

// on vérifie le contenu actuel des tables
afficherContenuTables();

// on crée nos DataObjects, et les relations associées : Personnes.adresse = CléPrimaire(Adresse)
class Adresse extends DataObject { }
class Personne extends DataObject { 
    var $_relation = array('adresse' => 'Adresse');
}

$personne = new Personne($db); // on récupère la personne d'id = 2
$personne->get(2);
// on note que la relation a automatiquement été résolue
echo "'{$personne->nom}' habite à '{$personne->adresse->txt_adresse}'\r\n"; 
// on modifie des éléments
$personne->nom .= ' (déménageur)';
$personne->adresse->txt_adresse = 'Ma nouvelle adresse';
echo "'{$personne->nom}' habite maintenant à '{$personne->adresse->txt_adresse}'\r\n\r\n"; 
// on sauvegarde
$personne->save();
// on vérifie le contenu actuel des tables
afficherContenuTables();

?>
Affichage attendu

Code : Tout sélectionner

== Contenu de 'Personnes' == 0 id_personne = 1 nom = Nom1 adresse = 1 1 id_personne = 2 nom = Nom2 adresse = 2 2 id_personne = 3 nom = Nom3 adresse = 3 == Contenu de 'Adresses' == 0 id_adresse = 1 txt_adresse = Adresse1 1 id_adresse = 2 txt_adresse = Adresse2 2 id_adresse = 3 txt_adresse = Adresse3 'Nom2' habite а 'Adresse2' 'Nom2 (dйmйnageur)' habite maintenant а 'Ma nouvelle adresse' == Contenu de 'Personnes' == 0 id_personne = 1 nom = Nom1 adresse = 1 1 id_personne = 2 nom = Nom2 (dйmйnageur) adresse = 2 2 id_personne = 3 nom = Nom3 adresse = 3 == Contenu de 'Adresses' == 0 id_adresse = 1 txt_adresse = Adresse1 1 id_adresse = 2 txt_adresse = Ma nouvelle adresse 2 id_adresse = 3 txt_adresse = Adresse3
Note pour ceux qui prennent peur, l'objectif n'est pas d'avoir une tartine de code là où on avait une autre tartine de code... Il faut bien être conscient que tout le code de l'exemple est à 95% destiné à l'exemple, la partie fonctionnelle démontrant réellement les possibilités du DataObject (je pense de plus en plus le renommer ORMObject mais il faudrait que je sois sûr de ce qu'est l'ORM exactement) est extrèmement réduite et c'est le but. En "vrai" on aurait plutôt ceci :
include '/path/to/PEAR/DB.php';
$db =& DB::connect("sqlite:///test.db");

// on crée nos DataObjects, et les relations associées : Personnes.adresse = CléPrimaire(Adresse)
class Adresse extends DataObject { }
class Personne extends DataObject { 
    var $_relation = array('adresse' => 'Adresse');
}

// on récupère la personne d'id = 2
$personne = new Personne($db); 
$personne->get(2);
// on modifie des éléments
$personne->nom .= ' (déménageur)';
$personne->adresse->txt_adresse = 'Ma nouvelle adresse';
// on sauvegarde
$personne->save();

Mammouth du PHP | 983 Messages

07 mars 2006, 10:23

Pourquoi n'utilises-tu pas PDO?

Eléphant du PHP | 219 Messages

07 mars 2006, 11:22

Salut :)
Je ne fais plus de php depuis un certain temps, mais je reste à l'écoute sur ce forum afin de ne pas perdre le fil, et j'avoue que ton post me tape particulièrement dans l'oeil car je m'étais aussi lancé dans un post sur la thématique des base de données, bien que de ton côté ce soit la mapping o-r qui t'intéresse.
Au niveau des interrogations que j'ai actuellement :
- Tu as l'air d'être soucieux de ne pas réinventer la roue, mais je crois que c'est mal parti. Au niveau de PEAR, le package DataContainer semble faire cela.
- Comment gères-tu les relations autres que 1-1 ?

En tout cas, je te souhaite bon courage ;)

Administrateur PHPfrance
Administrateur PHPfrance | 3131 Messages

07 mars 2006, 13:11

rami DB est compatible PHP4 il me semble, ça me parait important de conserver cette compatibilité, puis dans l'ensemble (surtout pour une librairie) je préfère utiliser de l'existant que du tout-neuf.
Rien n'empêche cela dit pour moi de faire un test sur la classe de "$db" et d'accepter ainsi les PDO.

daoud en effet, je n'ai pas envie de réinventer la roue, mais plutot d'intégrer l'existant de PEAR. Donc si un package autre que PEAR réalise déjà ça, ça ne me dérangera pas de le redéfinir pour intégrer PEAR, mais si ce modèle existe déjà dans ce framework, je vais plutôt essayer d'y contribuer pour l'améliorer (en partant du principe que mon développement initial est plutôt une manière d'appréhender le problème du ORM transparent que de l'implémenter réellement).

DataContainer semble en effet très bien, mais je ne comprends pas bien : pourquoi n'apparait-il nulle part sur pear.php.net ? La doc est bien succinte de plus (elle est peut-être plus fourni dans le package que je n'ai pas encore téléchargé).

Concernant les relations 1-n ou n-n, pour tout dire : j'y refléchis intensément ^^
Je pense que pour gérer ce type de relation il faudra penser l'interface de la classe totalement différemment, afin d'autoriser le "parcours" (chose impossible en l'état, on ne gère qu'un objet, on n'a pas accès à son suivant ou son précédent). Mais c'est LE challenge intéressant, qui apporterait une réelle plus-value à ce projet.

Eléphant du PHP | 219 Messages

07 mars 2006, 14:45

Le package DBContainer qui se sert de PEAR n'apparait effectivement pas dans le site PEAR. Il semble cependant souffrir de quelques défauts. La documentation est visiblement inexistante, puis, il faut étendre la classe DataContainer pour créer son propre DataObject, et remplir les champs manuellement ce qui est une tâche assez longue qui devrait être automatisée. Cependant j'avais utilisé il y a quelque temps un script qui était disponible dans le package pear qui générait tous les objets, et j'ai vu que cela existait toujours et semblait même avoir été simplifié. :
You don't have to comply with DB DataObject's documentation as for generating the files - it says you should launch the generation from the Unix shell level.

The easiest thing to do is to launch a script which does that:



<?php
public function regenerateDataObjects() {
require_once 'DB/DataObject/Generator.php';
$generator = new DB_DataObject_Generator;
$generator->start();
}
?>


And all DataObjects will be regenerated automatically.

Of course the above fragment assumes that you've got DataObject's environment variables configured properly.
Cependant, si je me souviens bien, la situation n'était pas très claire pour les relations 1-n... :?
Il y a un élément qui je crois a aussi son importance, c'est la performance. Lorsque tu récupères ton objet par

Code : Tout sélectionner

$personne->get(2);
tu charges tous les champs,alors que tu peux n'avoir besoin que de un ou deux champs. Au niveau perf, cela peut être gênant sur des gros volumes.

Ainsi, je te rejoins complétement, il y a un vrai défi à ce niveau. Je n'ai jamais poussé plus loin l'étude des frameworks existants déjà, comme PROPEL qui avait l'air pas mal déjà il y a quelque temps, mais je pense que ce serait peut-être nécessaire pour en retirer les points forts/faibles.

a+ :)
Dernière édition par daoud le 07 mars 2006, 17:37, édité 1 fois.

Eléphant du PHP | 312 Messages

07 mars 2006, 15:17

Code : Tout sélectionner

$personne->get(2);
tu charges tous les champs,alors que tu peux n'avoir besoin que de un ou deux champs. Au niveau perf, cela peut être gênant sur des gros volumes.
D'ailleurs, si je me trompe pas, vu que le get() de la classe "mère" (en fait la classe n°1 de la relation) fait un get() de la classe (ou des classes) filles, tu récupères plus que des champs en trop, tu récupères carrément des objets dont tu ne te serviras peut-être pas (sans compter qu'il est aussi possible de voir apparaître une boucle infinie si la relation entre les deux classes est symétrique).

Administrateur PHPfrance
Administrateur PHPfrance | 3131 Messages

07 mars 2006, 20:42

tu récupères plus que des champs en trop, tu récupères carrément des objets dont tu ne te serviras peut-être pas (sans compter qu'il est aussi possible de voir apparaître une boucle infinie si la relation entre les deux classes est symétrique).
C'est le concept même du projet oui... remplacer des champs scalaires par des objets de façon à concrétiser la relation entre les tables.
Pour la récursivité infinie j'y ai bien évidemment pensé, et je pense que faire un SELECT immédiatement n'est pas une bonne idée. La façon dont fonctionnent les autres containers (avec une méthode load()) me parait plus logique (d'une part au niveau du fonctionnement mais également au niveau du nom de la fonction, symétrique de save() ;))
$personne = new Personne($db);
$personne->load(2); // équivalent à $personne->id_personne = 2; $personne->load();
// $personne->adresse devient un objet tel que $personne->adresse->id_adresse = la valeur scalaire du champ d'origine
// mais il n'est pas directement chargé
var_dump($personne->adresse->txt_adresse); // NULL
$personne->adresse->load(); // on effectue la requête
var_dump($personne->adresse->txt_adresse); // string(8) "chez moi"
Cela permet l'économie de requête, ça évite les boucles infinies, et n'empêche pas de rajouter un argument booléen à la méthode load() pour charger les "sous-objets" directement, avec les risques que ça comprend.

Cependant dans beaucoup de cas où tu as une boucle infinie c'est que tu as du définir des relations en trop ;)

Petit nouveau ! | 4 Messages

07 mars 2006, 23:50

Donnez-moi votre avis : est-ce que vous utilisez déjà un système équivalent ?


Je suis passé à php 5 pour son nouveau modèle objet qui est tout à fait satisfaisant. Le pb qui se pose maintenant c au niveau de la persistance des objets. Je suis attentif aux differents outils qui sont developpés pour combler le manque et je dois dire que pour le moment rien n'est completement satisfaisant. J'utilise qd même de tps en tps celui qui me semble le plus abouti à savoir ezpdo.
Sinon en framework plus général et de qualité qui prend en charge la persistance y a noas. Mais je le trouve trop contraignant.
Qu'est-ce que vous en attend(ri)ez ?


J'en attend qu'il prenne en charge toutes les problématiques du mapping objet/relationnel, qu'il se couple faiblement avec les classes du modèle et qu'il respecte les principes de base de la programmation objet. Par exemple on peut reprocher à ezpdo d'obliger de déclarer les propriétés de la classe persistante comme publiques. Entre autres choses ...
En gros donc qu'il permette de simplifier les developpements d'appli en permettant de concevoir le modèle du domaine en objets sans avoir à se préoccuper de l'interfaçage avec la bdd.
Que pensez-vous de cette implémentation ?
Je trouve qu'il y a de bonnes idées et de la bonne volonté. On voit aussi que tu t'inspires de zend framework pour la conception avec l'heritage de data_object et de l'histoire des s ds le nommage des tables/classes.
Mais on voit aussi que t'as pas pris la mesure de ce à quoi tu t'attaques. La construction d'un service de persistance relationnel-objet est une grosse charge de travail et certains problèmes subtils demande une expertise très pointue.
L'exemple de personnes et d'adresses que tu prends pour illustrer ta reflexion ne degage pas tous les pbs liés au mapping objet-relationnel.
Ceci dit pour le pb des relations 1-n faut que tu gères ça avec un tableau associatif qui enregistre l'id de chaque objet en relation avec en parallele une table destinée à gerer toutes les associations.
Nul doute que tu pourras trouver des solutions de persistance sur de petits exemples comme celui là. Mais en bossant sur un projet plus riche tu risques de grincer des dents avec entre autres des pbs:
- avec les references circulaires
- l'heritage (gestion des attributs communs et relations differentes classe mère/classe fille par ex)
- La gestion des collections
- les acces multi-utilisateurs et les strategies de verrouillage (2 utilisateurs chargent le même objet, un enregistre une modif, cette modif est perdue si le 2e enregistre son objet ...)
- une gestion solide des erreurs en cas d'echec(php 5 me parait plus adapté à ce type de projet)
- une tres bonne strategie de mise en cache (la materialisation et dematerialisation des objets sont couteuses en ressourses, avec un site un minimum fréquenté tu peux pas te le permettre pour chaque requete cliente)
- Et quand tu aurras pris en compte ttes les subtilités, capitaliser tes requetes sql pour les reduire au minimum.

J'en passe ...
C'est pas pour te décourager mais juste pour te rappeler ds quoi tu t'embarques.
Et puis vu le truc à quoi tu t'attaques je trouve que tu manques vraiment de méthode. Modélise un modèle de domaine plus important et dégage tous les cas d'utilisation de ton framework de persistance en fonction de ce domaine.
Ensuite elabore des diagrammes de sequence et crées ton diagramme de classe en parallele en t'appuyant sur les patterns grasp (expert, forte cohesion, createur ...), t'as tout à y gagner et tu verras petit à petit apparaitre bon nombre de subtilités.
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).
Personnellement je trouve ça un peu prétentieux.
Le zend framework est en preview donc c un peu normal que tout ne soit pas operationnel. C'est tjrs facile de critiquer une preview.
J'attendais bcp de ce framework, principalement pour la persistance objet, va falloir que je patiente :(
Mais j'ai un peu regardé ce qui tourne et faut dire qu'ils ont fait les choses proprement. Ce qui est vraiment appreciable c'est que tu peux choisir les composants que tu veux utiliser et ainsi eviter de te retrouver avec une tonne de code mort comme c'est souvent le cas avec bon nombre de framework.
L'architecture est claire et bien pensée.
Maintenant je suis assez déçu sur la presentation de leur orm. Je trouve par exemple que le choix de faire heriter chaque classe persistante de data_object est une erreur de conception. En gros c'est faire heriter des classes du domaine d'une classe de services techniques qui est une couche inferieure. Y a de forte chance que ça entraine des pbs de maintenance et de conception sur des appli amenées à evoluer.
Mais je suis convaincu qu'ils changeront ça rapidement.

Bon courage
A+

Mammouth du PHP | 983 Messages

08 mars 2006, 10:44

J'ai bossé sur Hibernate en Java et j'avoue avoir pris goût à l'ORM. En PHP, il existe Propel qui est assez complet avec notamment la gestion des relations. Cependant, il nécessite un couplage fort avec les objets métiers qui peut être assez génant.


Je pense sincèrement que tu t'embarques dans un projet d'envergure alors qu'il existe déjà quelques produits très satisfaisant ds ce domaine.
Je ne vois pas clairement en quoi ton projet se différencie des autres?

Administrateur PHPfrance
Administrateur PHPfrance | 3131 Messages

08 mars 2006, 12:00

Nul doute que tu pourras trouver des solutions de persistance sur de petits exemples comme celui là. Mais en bossant sur un projet plus riche tu risques de grincer des dents avec entre autres des pbs:
- avec les references circulaires
- l'heritage (gestion des attributs communs et relations differentes classe mère/classe fille par ex)
- La gestion des collections
- les acces multi-utilisateurs et les strategies de verrouillage (2 utilisateurs chargent le même objet, un enregistre une modif, cette modif est perdue si le 2e enregistre son objet ...)
- une gestion solide des erreurs en cas d'echec(php 5 me parait plus adapté à ce type de projet)
- une tres bonne strategie de mise en cache (la materialisation et dematerialisation des objets sont couteuses en ressourses, avec un site un minimum fréquenté tu peux pas te le permettre pour chaque requete cliente)
- Et quand tu aurras pris en compte ttes les subtilités, capitaliser tes requetes sql pour les reduire au minimum.
Il faut bien comprendre que quand j'emploie le terme "ORM" c'est un raccourci plus que le terme qui correspond tout-à-fait. La partie "ORM" que je prévois n'est que la gestion simple des relations qu'on rencontre dans 99% des projets (1-1, 1-n, rarement avec des définitions circulaires sauf souci d'optimisation de l'accès aux données, ce qui n'est pas le souci ici dans un premier temps).

J'ai conscience de tout cela, je ne suis pas développeur depuis 6 mois j'ai un petit bagage sous le coude tout de même, et je sais pertinemment que ce type de projet n'est pas réalisable seul, et encore moins en peu de temps.
C'est bien pour ça que l'objectif clairement affiché est dans un premier temps UNIQUEMENT l'accès aux données de manière simplifiée. Je n'ai nulle part abordé le problème de la persistance parce que j'estime que c'est une couche extrèmement complexe, et donc à gérer après coup quand la base est déjà largement fiable.
Je n'ai pas envie de partir dans l'idée d'un projet grandiose pour finalement abandonner parce que je me rends compte que seul 10% du projet est réalisé (et encore, version preview) après 3 mois de dév.

Il faut bien comprendre que je suis réaliste : je n'ai pas l'intention de développer avec mes petites mimines un framework complet qui sauvera les professionnels du développement. Je compte simplement développer quelque chose d'équivalent au Zend DataObject et autres PEAR :: DB_DataContainer, mais avec une gestion transparente des relations simples.

Il faut bien saisir quelle est la cible de cette librairie dans un premier temps. De même je n'ai pas abordé le problème de la persistance simplement parce que je ne compte pas le gérer dans un premier temps. Il faut faire les choses étape par étape, si je devais demain développer un CMS je ne partirai pas avec un cahier des charges >= SPIP, ce serait pure folie. C'est le rôle d'une version 2 que d'ajouter des fonctionnalités aussi importantes, ou d'un projet futur basé sur l'expérience de ce premier projet.
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).
Personnellement je trouve ça un peu prétentieux.
C'était bien sûr de l'humour, et je suis persuadé que Zend aura implémenté son DataObject bien avant que j'ai fini mon projet, tant mieux d'ailleurs je m'inspirerai de ceux dont je m'inspire déjà, avec pour objectif d'avoir un package compatible PEAR (et éventuellement prenant en charge les PDO de PHP5).
De plus ne t'attends pas à la gestion ultime de la persistance chez Zend, si tu lis la doc attentivement ils expriment clairement l'objectif : simplifier l'accès aux données, c'est tout. Pas de gestion des relations là-dedans (d'ailleurs je trouve que le terme ORM est d'autant plus inadapté) et la gestion de la persistance n'est pas abordée dans ce chapitre. Je pense qu'ils ont la sagesse qui est également la mienne de ne pas vouloir mettre la charrue avant les boeufs.

Quand j'apprends un langage, mon but n'est pas de réécrire un OS tout de suite, d'abord je fais "Hello, World", pourtant je pourrais très bien à terme écrire un OS avec ce langage. Là c'est pareil je me lance dans un domaine qui pourrait amener à écrire un framework complet rien que pour la gestion des données (ce que fait ezpdo), alors je vais peut-être déjà commencé par le "Hello, World" du mapping database-object :) Quand j'en serai satisfait on verra quelle suite lui donner.


@rami ce que vous me citez ce sont des frameworks d'une complexité ahurissante. Ce que je veux réaliser c'est une petite librairie, tu l'insères, tu déclares tes classes et tes relations, et après tu ne gère plus que des objets sans se soucier de l'accès aux données et du SQL qui tourne derrière. Question interface il n'y aura rien de plus que les 2 méthodes load() & save() de l'objet, et ses attributs. C'est en ça qu'il se démarquera :
- des grosses usines à gaz, de part sa simplicité.
- des packages simples existant, de part sa gestion transparente des relations usuelle.

Petit nouveau ! | 1 Messages

11 sept. 2006, 16:47

Salut,
je dois, pour mon projet de bts créer un système intégré assez complexe (genre de système exploitable sur internet, architecture client/serveur, avec serveur apache+php & mysql & openlaszlo, et client qui est un programme OpenLaszlo, voir www.openlaszlo.org pour plus d'infos...)

La première étape de création de mon s.i., cest de créer un moteur orm.

J'ai donc vu les choses directement du point de vue de l'objet.
Un objet peut être composé d'autres objets. S'il n'est pas composé d'autres objets, cela veut dire qu'il est la valeur.
par exemple, une entreprise a une enseigne. Ici entreprise est un objet alors qu'enseigne (qui n'est qu'une chaine de caractères) n'est qu'une propriété (un champ de la table) de l'objet.
Le moteur, d'après les informations de la classe (prenons enseigne) sait qu'il s'agit bien d'un champ de l'objet Entreprise, étant définit comme suit:
Entreprise
A UNE Enseigne(enseigne) PROPRE.


Un objet est créé à partir de sa définition, et la définition d'un objet, c'est sa classe.

J'ai donc créé un moteur qui a plusieurs methodes :
_retourne la définition d'un objet (en fournissant l'id de la classe), et
_retourne un objet (d'après sa def. avec ses objets/valeurs)
_modifier definition d'une classe
_modifier un objet
_sauver def.
_sauver obj

J'ai écris mes pensées à ce sujet, si vous voulez lire mes notes, envoyez moi un mp

[edit]
j'allais oublier de préciser, mon objectif est dans un premier temps de créer un système totalement dépendant de la base de données (non du type de sgbdr, mais bien des données qu'elle contient).
En fait, les classes d'objets sont définient dans une table du sgbdr.
Les relations aussi.
Je souhaite ajouter des actions aussi, avec du code PHP, permettant d'ajouter des methodes spécifiques aux objets.
J'ai déjà aussi prévu de gerer les superclasses (Un Client est une Personne, un Employé aussi, mais tous deux ont des propriétés en plus propres à elles m^).

Petit nouveau ! | 1 Messages

28 mars 2016, 17:52

bonsoir
je veux créer ma propre ORM comment je peux faire ???

Avatar de l’utilisateur
Administrateur PHPfrance
Administrateur PHPfrance | 7162 Messages

29 mars 2016, 03:42

Bonjour,
bonsoir
je veux créer ma propre ORM comment je peux faire ???
En commençant par te créer ton propre sujet sur le forum, vu que ta question n'est pas une réponse à ce sujet qui date d'il y a 10 ans :-D
Pour simplifier la lecture et favoriser les réponses, il est toujours préférable de faire 1 question = 1 sujet
Quand tout le reste a échoué, lisez le mode d'emploi...