Zend FR

Consultez la FAQ sur le ZF avant de poster une question

Vous n'êtes pas identifié.

#1 29-08-2009 10:29:58

Delprog
Administrateur
Date d'inscription: 29-09-2008
Messages: 670

[Réflexion]Model: Lazy load Mapper/Proxy/Objet métier

Bonjour,

Toujours dans l'idée de faire quelque chose de simple pour gérer le load à la demande sur les dépendances entre objets, j'ai des idées à moitié assemblées, et comme autour de moi il n'y a personne qui touche à ça, je viens vers vous pour vos avis.

Donc, je souhaite avoir quelque chose du genre, exemple pour du OneToOne:

Code:

class Model_User
{
    protected $_profil;
}

Et sur un $user->profil->toto, se fasse une requête implicite pour peupler l'objet profil.

Pour ce faire, je pensais faire un truc assez simple, utiliser le pattern proxy (un peu à la hibernate en JAVA), et via les proxy remonter les infos depuis les mappers.

J'aurais donc un truc du genre:
classe abstraite objet métier

Code:

abstract class Tight_Model_BaseObject
{          
    protected $_relations = array();

    public function setRelations(array $relations)
    {
        $this->_relations = $relations;
        return $this;
    }
}

Classe de base proxy

Code:

abstract class Tight_Model_ProxyObject
{          
    protected $_proxiedObject;
    
    private $_populated = false;

    protected function setProxiedObject($proxiedObject)
    {
        if (is_string($proxiedObject)) {
            $proxiedObject = new $proxiedObject();
        }
        if (!$proxiedObject instanceof Tight_Model_BaseObject) {
            throw new Tight_Model_ProxyObject_Exception(Tight_Model_ProxyObject_Exception::INVALID_PROXIED_OBJECT);
        }        
        $this->_proxiedObject = $proxiedObject;
        
        return $this;
    }
    
    protected function getProxiedObject() {
        if (null === $this->_proxiedObject) {
            throw new Tight_Model_ProxyObject_Exception(Tight_Model_ProxyObject_Exception::PROXIED_OBJECT_NOT_FOUND);
        }
        $this->setProxiedObject($this->_proxiedObject);
        
        if (false === $this->_populated) {
            $this->_populate();
        }
        
        return $this->_proxiedObject;
    }        
    
    /**
     * a redéfinir dans les classes filles     
     */
    public function populate()
    {}
    
    public function __get($name)
    {
        $proxiedObject = $this->getProxiedObject();
        
        $method = 'get' . $name;
        $methodIs = 'is' . $name;
        
        $methodIsExists = method_exists($proxiedObject, $methodIs);
        
        if (!method_exists($proxiedObject, $method) && !$methodIsExists) {
            throw new Tight_Model_BaseObject_Exception(Tight_Model_BaseObject_Exception::INVALID_PROPERTY . ' : ' . $name);
        }
        if ($methodIsExists) {
            return $proxiedObject->$methodIs();    
        }
        
        return $proxiedObject->$method();
    } 
    
    // __set aussi
}

Code:

abstract class Tight_Model_Db_ProxyObject extends Tight_Model_ProxyObject
{          
    protected $_mapper;        
    
    public function setMapper($mapper)
    {}
    
    public function getMapper()
    {}

    public function populate()
    {
        // ICI APPEL AU MAPPER POUR PEUPLER L'OBJET EN FONCTION
        // DE JE NE SAIS QUOI
    
        $this->_populated = true;
    }
}

Donc que faire de ces classes après ? En fait c'est assez simple.
Dans l'objet, je définis un tableau de relations ($_relations) qui donne les relations entre l'objet et ses dépendances (OneToOne, OneToMany, ManyToOne, etc.).
Lorsque je peuple l'objet depuis son mapper, si des propriétés sont dans $_relations j'injecte dedans le proxy de l'objet concerné et c'est tout.

Ensuite, l'idée serait que lorsqu'on demande pour la premiere fois l'accès à un objet, le proxy interroge le mapper de l'objet qui le peuple.
Ensuite tous les getters et setters sont redirigés par le proxy via __get et __set.

Avec l'exemple ci-dessus (User et Profil) ça donnerait un truc du genre:

Code:

class Model_User extends Tight_Model_BaseObject
{
    protected $_relations = array(
        'profil' => 'OneToMany'        
    )

    protected $_profil;
        
    // tout le bordel: proprietés, getters, setters, etc.
    
    // getId(), setId() et clearId(), toArray() sont dans BaseObject
    // les méthodes magiques sont dans BaseObject
}

class Model_Profil extends Tight_Model_BaseObject
{}
class Model_Db_Proxy_Profil extends Tight_Model_Db_ProxyObject
{
    protected $_proxiedObject = 'Model_Profil';
}

Là où j'ai besoin de reflexion c'est comment faire savoir à l'objet proxy pour qui et comment (relation) il doit charger l'objet.
En partant du principe que dans les mapper des objets en relation je suis sensé avoir des méthodes avec un pattern particulier, du genre:

Code:

// exécute une requête SQL et peuple $profil
public function getProfilForUser($userId, Model_Profil $profil)
{}

Je n'ai donc plus qu'à trouver un moyen de savoir dans l'objet proxy dépendant de quel manière je dois le peupler et avec quels paramètres depuis son mapper.
L'exemple est spécifiquement pour du OneToOne, après pour les autres relations je ne devrais pas avoir de pb pour adapter.

Des idées ?


A+ benjamin.


http://www.anonymation.com/ - anonymation - Studio de création.
http://code.anonymation.com/ - anonymation - blog - développement et architecture web

Hors ligne

 

#2 31-08-2009 14:23:26

Delprog
Administrateur
Date d'inscription: 29-09-2008
Messages: 670

Re: [Réflexion]Model: Lazy load Mapper/Proxy/Objet métier

Tout ce qui est au dessus part à la poubelle.
J'avais vu le problème à l'envers, j'ai bien réfléchi et fait autrement.

Je suis parti d'un fichier XML, ex:

Code:

<?xml version="1.0" encoding="UTF-8"?>
<model xmlns:zf="http://framework.zend.com/xml/zend-config-xml/1.0/">
    <config>
        <path>
            <root><zf:const zf:name="APPLICATION_PATH" /></root>
            <folder>models</folder>
        </path>
        <prefixes model="Model_" domain="Domain_" table="Db_Table_" mapper="Db_Mapper_" proxy="" />        
    </config>
    <tables>
        <!-- user -->
        <table name="user" primary="id" override="Tight_Model_Db_Table" />
        <table name="gallery" primary="id" override="Tight_Model_Db_Table" />
        <!-- <table name="gallery" primary="id" override="Tight_Model_Db_Table" implements="" rowclass="" rowsetclass="" /> -->
    </tables>
    <objects>
        <!-- User Object -->
        <object name="User" override="Tight_Model_BaseObject">
            <properties>
                <!-- id obligatoire pour tous les objets -->
                <property name="id" type="int" refcol="id" extra="pk" />
                <property name="login" type="string" refcol="login" />
                <property name="name" type="string" refcol="name" />
                <property name="password" type="string" refcol="password" />
                <property name="email" type="string" refcol="email" />
                <property name="roleId" type="int" refcol="role" />                
                <property name="created" type="string" refcol="created" oninsert="getCurrentTimeStamp" />
                <property name="updated" type="string" refcol="updated" />                
            </properties>
            <mapper table="user" />
            <proxy implements="Tight_Model_ProxyObject" />
            <relations>
                <relation name="Gallery" type="one-to-many" property="galleries" key="id" refcol="user_id" />
            </relations>
        </object>
        
        <!-- Gallery Object -->
        <object name="Gallery" override="Tight_Model_BaseObject">
            <properties>
                <property name="id" type="int" refcol="id" extra="pk" />
                <property name="title" type="string" refcol="titre" />
                <property name="description" type="string" refcol="description" />
                <property name="userId" type="string" refcol="user_id" />                        
            </properties>
            <mapper table="gallery" />
            <proxy implements="Tight_Model_ProxyObject" />
            <relations>
                <relation name="User" type="many-to-one" property="user" key="userId" refcol="id"/>
            </relations>
        </object>
            
    </objects>
</model>

A partir de ça, j'ai écris un générateur de classes qui va me générer les tables, les objets métiers, les proxy vers ces objets métiers et les mappers.

Le script génère tout d'abord les fichiers php des tables, avec name, primary, rowclass, etc.

Ensuite il les génère les fichiers des objets métiers (Domain). Toutes les propriétés, également celles qui attendent un objet dépendant ou une collection d'objets dépendants, les getters/setters.

Ensuite il génère les fichiers des objets proxy vers les objets métier (ce qui seront utilisés dans l'appli). Dans ces proxy sont générés une surchage des getters pour les objets dépendants qui font appel aux méthodes get<Object>For<Object>() des mappers appropriés (Lazy Load).

Enfin, il génère les mappers, avec tout un fieldMap, les méthode find() et delete(), et les méthodes nécessaires au mapper pour peupler les dépendances, donc les méthodes get<Object>For<Object>() ou get<Objects>for<Object>() (Lazy Load). Les méthodes buildObject(), save() sont déjà assurées par Tight_Model_Db_Mapper.

Pour le XML ci-dessus ça va donc générer:

Dossier models

Code:

models/
    Db/
        Mapper/
            Gallery.php
            User.php            
        Table/
            Gallery.php
            User.php
    Domain
        Gallery.php
        User.php
    Gallery.php
    User.php

Tables:

Model_Db_Table_User
(models/db/table/User.php)

Code:

<?php 
class Model_Db_Table_User extends Tight_Model_Db_Table  
{ 
    protected $_name = 'user';
    protected $_primary = 'id';
}

Model_Db_Table_Gallery
(models/db/table/Gallery.php)

Code:

<?php 
class Model_Db_Table_Gallery extends Tight_Model_Db_Table  
{ 
    protected $_name = 'gallery';
    protected $_primary = 'id';
}

Objets métiers, donc dans Domain:

Model_Domain_User
(models/domain/User.php)

Code:

<?php 
class Model_Domain_User extends Tight_Model_BaseObject 
{ 

    /**
     * @var int
     */
    protected $_id;

    /**
     * @var string
     */
    protected $_login;

    /**
     * @var string
     */
    protected $_name;

    /**
     * @var string
     */
    protected $_password;

    /**
     * @var string
     */
    protected $_email;    

    /**
     * @var int
     */
    protected $_roleId;
   
    /**
     * @var string
     */
    protected $_created;

    /**
     * @var string
     */
    protected $_updated;

    /**
     * @var ArrayObject|Model_Gallery
     */
    protected $_galleries;

    /**
     * @return int
     */
    public function getId()
    {
        return $this->_id;
    }

    /**
     * @param int $id
     * @return Model_Domain_User
     */
    public function setId($id)
    {
        $this->_id = $id;
        return $this;
    }

    /**
     * @return string
     */
    public function getLogin()
    {
        return $this->_login;
    }

    /**
     * @param string $login
     * @return Model_Domain_User
     */
    public function setLogin($login)
    {
        $this->_login = $login;
        return $this;
    }

    /**
     * @return string
     */
    public function getName()
    {
        return $this->_name;
    }

    /**
     * @param string $name
     * @return Model_Domain_User
     */
    public function setName($name)
    {
        $this->_name = $name;
        return $this;
    }

    /**
     * @return string
     */
    public function getPassword()
    {
        return $this->_password;
    }

    /**
     * @param string $password
     * @return Model_Domain_User
     */
    public function setPassword($password)
    {
        $this->_password = $password;
        return $this;
    }

    /**
     * @return string
     */
    public function getEmail()
    {
        return $this->_email;
    }

    /**
     * @param string $email
     * @return Model_Domain_User
     */
    public function setEmail($email)
    {
        $this->_email = $email;
        return $this;
    }   

    /**
     * @return int
     */
    public function getRoleId()
    {
        return $this->_roleId;
    }

    /**
     * @param int $roleId
     * @return Model_Domain_User
     */
    public function setRoleId($roleId)
    {
        $this->_roleId = $roleId;
        return $this;
    }  

    /**
     * @return string
     */
    public function getCreated()
    {
        return $this->_created;
    }

    /**
     * @param string $created
     * @return Model_Domain_User
     */
    public function setCreated($created)
    {
        $this->_created = $created;
        return $this;
    }

    /**
     * @return string
     */
    public function getUpdated()
    {
        return $this->_updated;
    }

    /**
     * @param string $updated
     * @return Model_Domain_User
     */
    public function setUpdated($updated)
    {
        $this->_updated = $updated;
        return $this;
    }

    /**
     * @return ArrayObject|Model_Gallery
     */
    public function getGalleries()
    {
        return $this->_galleries;
    }

    /**
     * @param ArrayObject|Model_Gallery $galleries
     * @return Model_Domain_User
     */
    public function setGalleries($galleries)
    {
        $this->_galleries = $galleries;
        return $this;
    }
}

Model_Domain_Gallery
(models/domain/Gallery.php)

Code:

<?php 
class Model_Domain_Gallery extends Tight_Model_BaseObject 
{ 

    /**
     * @var int
     */
    protected $_id;

    /**
     * @var string
     */
    protected $_title;

    /**
     * @var string
     */
    protected $_description;

    /**
     * @var string
     */
    protected $_userId;

    /**
     * @var Model_User
     */
    protected $_user;

    /**
     * @return int
     */
    public function getId()
    {
        return $this->_id;
    }

    /**
     * @param int $id
     * @return Model_Domain_Gallery
     */
    public function setId($id)
    {
        $this->_id = $id;
        return $this;
    }

    /**
     * @return string
     */
    public function getTitle()
    {
        return $this->_title;
    }

    /**
     * @param string $title
     * @return Model_Domain_Gallery
     */
    public function setTitle($title)
    {
        $this->_title = $title;
        return $this;
    }

    /**
     * @return string
     */
    public function getDescription()
    {
        return $this->_description;
    }

    /**
     * @param string $description
     * @return Model_Domain_Gallery
     */
    public function setDescription($description)
    {
        $this->_description = $description;
        return $this;
    }

    /**
     * @return string
     */
    public function getUserId()
    {
        return $this->_userId;
    }

    /**
     * @param string $userId
     * @return Model_Domain_Gallery
     */
    public function setUserId($userId)
    {
        $this->_userId = $userId;
        return $this;
    }

    /**
     * @return Model_User
     */
    public function getUser()
    {
        return $this->_user;
    }

    /**
     * @param Model_User $user
     * @return Model_Domain_Gallery
     */
    public function setUser($user)
    {
        $this->_user = $user;
        return $this;
    }
}

Objets proxy:

Model_User
(models/User.php)

Code:

<?php 
class Model_User extends Model_Domain_User implements Tight_Model_ProxyObject 
{ 

    public function getGalleries() 
    { 
        if (is_null(parent::getGalleries())) { 
            $mapper = new Model_Db_Mapper_Gallery(); 
            $objSet = $mapper->getGalleriesForUser(parent::getId()); 
            parent::setGalleries($objSet); 
        } 
        return parent::getGalleries(); 
    } 
}

Model_Gallery
(models/Gallery.php)

Code:

<?php 
class Model_Gallery extends Model_Domain_Gallery implements Tight_Model_ProxyObject 
{ 

    public function getUser() 
    { 
        if (is_null(parent::getUser())) { 
            $obj = new Model_User(); 
            $mapper = new Model_Db_Mapper_User(); 
            $mapper->getUserForGallery(parent::getUserId(), $obj); 
            parent::setUser($obj); 
        } 
        return parent::getUser(); 
    } 
}

Les Mappers:

Model_Db_Mapper_User
(models/db/mapper/User.php)

Code:

<?php 
class Model_Db_Mapper_User extends Tight_Model_Db_Mapper 
{ 
    protected $_dbTable = 'Model_Db_Table_User';

    protected $_fieldMap = array (
        'id' => 'id',
        'login' => 'login',
        'name' => 'name',
        'password' => 'password',
        'email' => 'email',        
        'roleId' => 'role',        
        'activated' => 'activated',
        'created' => 'created',
        'updated' => 'updated',
    );

    protected $_onInsertFunctions = array (
        'created' => 'getCurrentTimeStamp',
    );

    protected $_onUpdateFunctions = array (
    );

    /**
     * Find User with id $id
     * 
     * @param int        $id
     * @param Model_User $user
     * @return void|bool not success
     */
    public function find($id, Model_User $user)
    {
        $result = $this->getDbTable()->find($id);
        if (0 == count($result)) {
            return false;
        }
        $row = $result->current();
        
        $this->buildObject($row, $user);
    }

    /**
     * Delete User with id $id
     * 
     * @param  int  $id
     * @return bool success
     */
    public function delete($id)
    {
        try {
            $where = $this->getDbTable()->getAdapter()->quoteInto('id = ?', $id);
            $this->getDbTable()->delete($where);
        }
        catch (Zend_Db_Table_Exception $e) {
            throw new Zend_Db_Table_Exception($e->getCode() . ' : ' . $e->getMessage());
        }
        return true;
    }

    public function getUserForGallery($userId, Model_User $user) 
    { 
        $tableName = $this->getDbTable()->info('name'); 
        
        $select = $this->getDbTable()->select()->setIntegrityCheck(false); 
        $select->from(array('t1' => $tableName))                
               ->where('t1.id = ?', $userId);
        try {
            $row = $this->getDbTable()->fetchRow($select);
        }
        catch(Exception $e) {
            throw new Exception($e->getMessage(), $e->getCode());
        }

        if (0 == count($row)) {
            return false;
        }
        $this->buildObject($row, $user);
    } 
}

Model_Db_Mapper_Gallery
(models/db/mapper/Gallery.php)

Code:

<?php 
class Model_Db_Mapper_Gallery extends Tight_Model_Db_Mapper 
{ 
    protected $_dbTable = 'Model_Db_Table_Gallery';

    protected $_fieldMap = array (
        'id' => 'id',
        'title' => 'titre',
        'description' => 'description',
        'userId' => 'user_id',
    );

    protected $_onInsertFunctions = array (
    );

    protected $_onUpdateFunctions = array (
    );

    /**
     * Find Gallery with id $id
     * 
     * @param int        $id
     * @param Model_Gallery $gallery
     * @return void|bool not success
     */
    public function find($id, Model_Gallery $gallery)
    {
        $result = $this->getDbTable()->find($id);
        if (0 == count($result)) {
            return false;
        }
        $row = $result->current();
        
        $this->buildObject($row, $gallery);
    }

    /**
     * Delete Gallery with id $id
     * 
     * @param  int  $id
     * @return bool success
     */
    public function delete($id)
    {
        try {
            $where = $this->getDbTable()->getAdapter()->quoteInto('id = ?', $id);
            $this->getDbTable()->delete($where);
        }
        catch (Zend_Db_Table_Exception $e) {
            throw new Zend_Db_Table_Exception($e->getCode() . ' : ' . $e->getMessage());
        }
        return true;
    }

    public function getGalleriesForUser($id) 
    { 
        $tableName = $this->getDbTable()->info('name'); 
        
        $select = $this->getDbTable()->select()->setIntegrityCheck(false); 
        $select->from(array('t1' => $tableName))               
               ->where('t1.user_id = ?', $id);
        try {
            $resultSet = $this->getDbTable()->fetchAll($select);
            $entries  = new ArrayObject();
            foreach ($resultSet as $row) {
                $entry = new Model_Gallery();
                $this->buildObject($row, $entry);
                $entries[] = $entry;
            }
        }
        catch(Exception $e) {
            throw new Exception($e->getMessage(), $e->getCode());
        }

        return $entries;
    } 
}

Et j'ai donc du véritable Lazy Load, il va peupler "implicitement" les objets dépendants dès qu'ils sont sollicités.
Exemple:

Code:

        $gallery = $this->_galleryService->findGalleryById(1);        
        echo 'Gallery : ' . $gallery->title;
        echo '<br/>';
        echo '-- User : ' . $gallery->user->login . '<br />';
        echo '---- Galleries : <br />';        
        
        foreach ($gallery->user->galleries as $gallery) {
             echo '------ ' . $gallery->title . '<br />';
             echo '-------- User : ' . $gallery->user->login . '<br />';
        }
        
        echo '<br /><br />';
        
        $user = $this->_userService->findUserById(1);

        echo 'User : ' . $user->login;
        echo '<br/>';
        echo '-- Galleries : <br />';
        foreach ($user->galleries as $gallery) { 
            echo '---- ' . $gallery->title . '<br />';
        }
        die();

Va afficher:

Code:

Gallery : Gallery 1
-- User : Ben
---- Galleries :
------ Gallery 1
-------- User : Ben
------ Gallery 2
-------- User : Ben
------ Gallery 3
-------- User : Ben


User : Ben
-- Galleries :
---- Gallery 1
---- Gallery 2
---- Gallery 3

Voilà, je suis content du fonctionnement, me reste à compléter le générateur de code pour le many-to-many et à envisager de gérer le save.

Les méthodes get<Object>For<Object>() sont volontairement générées dans les mappers pour pouvoir garder la main dessus.

Une bonne chose à faire serait de faire des mappers des singleton aussi.

Quand j'aurai géré le many-to-many, n'ayant pas de blog, je pourrai mettre le code à disposition pour ceux que ça intéresse. En attendant, biensûr, le but de ce post est de m'ouvrir aux critiques, donc s'il vous plait, n'hésitez pas smile


A+ benjamin.

Edit: petite modif, les jointures étaient inutiles.

Dernière modification par Delprog (01-09-2009 18:58:30)


http://www.anonymation.com/ - anonymation - Studio de création.
http://code.anonymation.com/ - anonymation - blog - développement et architecture web

Hors ligne

 

#3 31-08-2009 18:08:33

Eureka
Membre
Date d'inscription: 18-07-2009
Messages: 81

Re: [Réflexion]Model: Lazy load Mapper/Proxy/Objet métier

Je viens tout juste de lire, sans avoir de retours dans l'instant autre que celui : la notion de relations OneTo*, ManyTo*, ..., n'est-elle pas celle qui est déjà implémentée via la propriété $_referenceMap d'un objet étendant Zend_Db_Table_Abstract ?
En tout cas la génération fait saliver smile

Hors ligne

 

#4 01-09-2009 10:36:50

Delprog
Administrateur
Date d'inscription: 29-09-2008
Messages: 670

Re: [Réflexion]Model: Lazy load Mapper/Proxy/Objet métier

Salut,

Ça ne m'intéresse pas d'utiliser les méthodes de récupération en cascade proposées par Zend. Pour deux raisons:

La première est que mon Lazy Load va se faire au travers d'objets métiers qui ignorent tout de la persistance et de l'organisation de la base de données.
Avec Zend et les referenceMap, le chargement des dépendances se fait à traver un row et est en plus à la charge du développeur. On pourrait envisager pour ceux qui n'ont pas besoin de toutes ces couches métiers de considérer un Zend_Db_Table_Row comme un objet métier, d'étendre Zend_Db_Table_Abstract pour que les méthodes findParent, findDependentRowset etc. se déclenchent implicitement sur les row. Mais cette solution ne me convient pas, elle lie très fortement le front avec le back et a une persistance type BDD.

De mon côté, le Lazy Load se fait sur l'objet métier au travers de proxy Db, si demain la persistance change je n'ai qu'à modifier le générateur et re-générer mes classes proxy et mapper appropriées.

La deuxième raison est une question de performances. Je veux garder la main sur les requêtes qui sont effectuées pour le Lazy. C'est pour ça que je les génère explicitement dans chaque Mapper. Si demain je veux utiliser une vue (au sens SQL) pour rendre la même chose, je pourrai le faire.


A+ benjamin.


http://www.anonymation.com/ - anonymation - Studio de création.
http://code.anonymation.com/ - anonymation - blog - développement et architecture web

Hors ligne

 

#5 01-09-2009 14:51:02

Eureka
Membre
Date d'inscription: 18-07-2009
Messages: 81

Re: [Réflexion]Model: Lazy load Mapper/Proxy/Objet métier

Tout simplement puissant !

Hors ligne

 

#6 30-11-2009 23:18:51

citronbleu-v
Membre
Lieu: Béziers ou Arles
Date d'inscription: 03-02-2009
Messages: 79
Site web

Re: [Réflexion]Model: Lazy load Mapper/Proxy/Objet métier

Très intéressant tout ceci smile.

J'aurais des questions avec extends Tight_Model_Db_Mapper.
Est ce que c'est toi qui a créé cet objet ou c'est un module téléchargé quelque par ?

Comment tout ce qui suit se gère automatiquement, c'est toi qui a programmé les modules ou c'est un design pattern ?

Code:

    protected $_dbTable = 'Model_Db_Table_Gallery';

    protected $_fieldMap = array (
        'id' => 'id',
        'title' => 'titre',
        'description' => 'description',
        'userId' => 'user_id',
    );

    protected $_onInsertFunctions = array (
    );

    protected $_onUpdateFunctions = array (
    );

Dernière modification par citronbleu-v (30-11-2009 23:27:56)

Hors ligne

 

#7 01-12-2009 09:11:46

Blount
Membre
Date d'inscription: 23-06-2009
Messages: 98
Site web

Re: [Réflexion]Model: Lazy load Mapper/Proxy/Objet métier

C'est intéressant effectivement.
Mais je me pose les questions suivantes :
- dans ce système, tu charges automatiquement toute les valeurs de la table. Par exemple, si je veux une liste d'utilisateur, il va falloir utiliser touts les champs (id, emails, created, etc.), alors que seul l'email, l'id et le nom (name) m'intéresse. Sur 10 utilisateurs, ça va, mais sur 1000 ?
- ensuite, lorsque je liste les utilisateurs, je souhaite afficher le titre des galeries de celui ci, pour chaque utilisateur une requête sera effectuée. Imaginons que mes 1000 utilisateurs aient au moins 1 galerie, alors au mieux nous aurons 1000 requêtes nécessaires à l'affichage des galeries ?

J'espère que j'ai été assez clair dans mes propos.
Je ne veux pas jouer le programmeur fou dingue de la moindre performance pouvant être gagnée, mais dans ces deux cas, ceci peut-être problématique à forte charge.

J'ai lu dans un autre sujet que tu étais finalement passé à Doctrine. Celui-ci gère t-il ces deux questions ?

Hors ligne

 

#8 01-12-2009 19:28:19

Delprog
Administrateur
Date d'inscription: 29-09-2008
Messages: 670

Re: [Réflexion]Model: Lazy load Mapper/Proxy/Objet métier

Salut,

@citronbleu-v

Tight_Model_Db_Mapper est une classe abstraite que j'ai écris. Je colle le code, attention c'est brut, elle n'a pas été finie, notamment dans la gestion des erreurs et des exceptions. Aussi, elle est expérimentale, comme tout ce que vous lisez ici d'ailleurs, elle fait son rôle mais il y aurait sans doute des outils plus adaptés à utiliser à certains endroits pour l'optimiser ou tout simplement optimiser le système. Ou encore pousser un peu plus loin la programmation par contrats (interfaces).

l'interface

Code:

<?php
interface Tight_Model_IMapper {

    /**
     * Fetch Array of entities found with criteria     
     * 
     * @param string $entity The Entity type to populate and return
     * @param mixed $criteria
     * @return ArrayObject|Tight_Model_Entity
     */
    public function fetchByCriteria($entity, $criteria, $fetchOne = false);
    
    /**
     * Fetch all entities     
     * 
     * @param string $entity The Entity type to populate and return
     * @return ArrayObject|Tight_Model_Entity
     */
    public function fetchAll($entity);
    
    /**     
     * Find an entity by id
     * 
     * @param string $entity The Entity type to populate and return
     * @param array|string $primary primary keys array
     * @return Tight_Model_Entity
     */
    public function find($entity, $primary);
    
    /**
     * Create (insert) entity into persistence
     * 
     * @param  Tight_Model_Entity $entity
     * @return void
     */
    public function create(Tight_Model_Entity $entity);
    
    /**
     * Save (update) entity into persistence
     * 
     * @param Tight_Model_Entity $entity
     * @return void
     */
    public function save(Tight_Model_Entity $entity);
    
    /**
     * Delete entity from persistence
     * 
     * @param Tight_Model_Entity $entity
     * @return void
     */
    public function delete(Tight_Model_Entity $entity);
    
    /**
     * Build an entity from an object
     * 
     * @param stdClass $row
     * @param string $entity The Entity type to populate and return
     * @return Tight_Model_Entity
     */
    public function buildEntity($row, $entity);
}

L'implémentation :

Code:

<?php
/**
 * Tight_Model_Db_Mapper
 * Db Table Mapper
 * 
 * @category   Tight
 * @package    Tight_Model
 * @subpackage Db
 * @author     Benjamin Dulau
 */
abstract class Tight_Model_Db_Mapper implements Tight_Model_IMapper
{        
    /**     
     * @var Zend_Db_Adapter_Abstract
     */
    protected $_db;
    
    
    /**     
     * @var Tight_Model_Template
     */
    protected $_template;
    
    /**
     * Constructor
     *      
     */
    public function __construct()
    {

    }
    
    /**     
     * @param Tight_Model_Template $template
     * @return Tight_Model_Db_Mapper
     */
    public function setTemplate(Tight_Model_Template $template)
    {
        $this->_template = $template;
        return $this;
    }
    
    /**          
     * @return Tight_Model_Template
     */
    public function getTemplate()
    {
        return $this->_template;
    }
    
    /**
     * Set Db adapter to use
     * 
     * @param Zend_Db_Adapter_Abstract $db
     * @return Tight_Model_Db_Table_Mapper Provides a fluent interface
     * @throws Tight_Model_Db_Mapper_Exception 
     */
    public function setDb($db)
    {
        if (!$db instanceof Zend_Db_Adapter_Abstract) {
            throw new Tight_Model_Db_Mapper_Exception(Tight_Model_Db_Mapper_Exception::INVALID_TABLE_GATEWAY);
        }        
        $this->_db = $db;
        $this->_db->setFetchMode(Zend_Db::FETCH_OBJ);
        
        return $this;
    }

    /**     
     * @return Zend_Db_Adapter_Abstract
     * @throws Tight_Model_Db_Mapper_Exception
     */
    public function getDb()
    {
        if (null === $this->_db) {
            throw new Tight_Model_Db_Mapper_Exception(Tight_Model_Db_Mapper_Exception::TABLE_GATEWAY_NOT_FOUND);
        }
        
        return $this->_db;
    }    

    /**     
     * @see library/Tight/Model/Tight_Model_IMapper#fetchByCriteria($entity, $criteria)
     */
    public function fetchByCriteria($entity, $select, $fetchOne = false)
    {                
        try {
            if (true === $fetchOne) {
                $row = $this->getDb()->fetchRow($select);
                if (is_null($row) || empty($row)) {
                    return null;
                }
                $entries = $this->buildEntity($row, $entity);                
            }
            else {
                $rows = $this->getDb()->fetchAll($select);
                $entries = new ArrayObject();                
                foreach ($rows as $row) {
                    $entries[] = $this->buildEntity($row, $entity);
                }
            }            
        }
        catch(Exception $e) {
            throw new Exception ($e->getMessage());
        }
        
        return $entries;
    }
    
    /**     
     * @see library/Tight/Model/Tight_Model_IMapper#fetchAll($entity)
     */
    public function fetchAll($entity)
    {
        $map = $this->getTemplate()->getEntityMapping($entity);
        try { 
            $table = new Zend_Db_Table($map['table']);
            $rows = $table->fetchAll();
            $entries = new ArrayObject();
            foreach ($rows as $row) {
                $entries[] = $this->buildEntity($row, $entity);
            }
        }
        catch(Exception $e) {
            throw new Exception ($e->getMessage());
        }
        
        return $entries;
    }
    
    /**     
     * @see library/Tight/Model/Tight_Model_IMapper#find($entity, $primary)
     */
    public function find($entity, $primary)
    {                        
        $map = $this->getTemplate()->getEntityMapping($entity);        
        try {                                 
            $table = new Zend_Db_Table($map['table']);
            $table->setOptions(array('primary' => $map['id']));                        
            
            $row = $table->find($primary)->current();                       
            if (is_null($row) || empty($row)) {
                return null;
            }                        
            
            $entry = $this->buildEntity($row, $entity);      
        }
        catch(Exception $e) {
            throw new Exception ($e->getMessage());
        }
        
        return $entry;
    }

    /**     
     * @see library/Tight/Model/Tight_Model_IMapper#create($entity)
     */
    public function create(Tight_Model_Entity $entity)
    {        
        // entité = parent du proxy
        $entityStr = get_class($entity);
        
        $map = $this->getTemplate()->getEntityMapping($entityStr);
        
        try {                                 
            $table = new Zend_Db_Table($map['table']);
            $table->setOptions(array('primary' => $map['id']));
            
            $row = $table->createRow();
            foreach ($map['fieldMap'] as $property => $field) {
                $row->$field = $entity->$property;
            }            
            $id = $row->save();
            
            $entity->setId($id);
        }
        catch(Exception $e) {
            throw new Exception ($e->getMessage());
        }
    }
    
    /**     
     * @see library/Tight/Model/Tight_Model_IMapper#save($entity)
     */
    public function save(Tight_Model_Entity $entity)
    {        
        // entité = parent du proxy
        $entityStr = get_class($entity);
        
        $map = $this->getTemplate()->getEntityMapping($entityStr);
        
        try {                                 
            $table = new Zend_Db_Table($map['table']);
            $table->setOptions(array('primary' => $map['id']));
            
            $row = $table->find($entity->getId())->current();
            foreach ($map['fieldMap'] as $property => $field) {
                $row->$field = $entity->$property;
            }            
            $row->save();
        }
        catch(Exception $e) {
            throw new Exception ($e->getMessage());
        }
    } 
    
    /**     
     * @see library/Tight/Model/Tight_Model_IMapper#delete($entity)
     */
       public function delete(Tight_Model_Entity $entity) {
           // entité = parent du proxy
        $entityStr = get_class($entity);
        
        $map = $this->getTemplate()->getEntityMapping($entityStr);
        
        try {                                 
            $table = new Zend_Db_Table($map['table']);            
            $primary = $map['id'];
            $table->setOptions(array('primary' => $primary));
            $row = $table->find($entity->getId())->current();
            $row->delete();
            return true;    
        }
        catch(Exception $e) {
            throw new Exception ($e->getMessage());
            return false;
        }
       }

    /**     
     * @see library/Tight/Model/Tight_Model_IMapper#buildEntity($row, $entity)
     */    
    public function buildEntity($row, $entity)
    {
        $map = $this->getTemplate()->getEntityMapping($entity);
    
        try { 
            $entry = new $entity();
            
            $id = null;            
            if (is_array($map['id'])) {                
                foreach($map['id'] as $k => $v) {
                    if (isset($row->$v)) {
                        if (!is_array($id)) {
                            $id = array();
                        }                                               
                        array_push($id, $row->$v);
                    }
                }                    
            }
            else {                
                $idField = $map['id'];
                if (isset($row->$idField)) {
                    $id = $row->$idField;
                }
            }
            
            $entry->id = $id;
            
            foreach ($map['fieldMap'] as $k => $v) {
                if (isset($row->$v)) {
                    $entry->$k = $row->$v;
                }                                      
            }
        }
        catch(Exception $e) {
            throw new Exception ($e->getCode() . ' : ' .$e->getMessage());
        } 

        return $entry;
    }
}

citronbleu-v a écrit:

Comment tout ce qui suit se gère automatiquement, c'est toi qui a programmé les modules ou c'est un design pattern ?

Il me faudrait un peu plus de précisions dans la question, mais si tu parles du mapping, je ne l'ai pas spécialement conçu autour d'un pattern particulier smile


@Blound

Blount a écrit:

- dans ce système, tu charges automatiquement toute les valeurs de la table. Par exemple, si je veux une liste d'utilisateur, il va falloir utiliser touts les champs (id, emails, created, etc.), alors que seul l'email, l'id et le nom (name) m'intéresse. Sur 10 utilisateurs, ça va, mais sur 1000 ?

Effectivement, le fetchAll() remonte toutes les colonnes, il faudrait passer par la méthode "fetchByCriteria" du mapper et construire ton select avec seulement les champs qui t'intéressent, à ce moment là, l'entité ne sera "mappée" qu'avec ces données là.

Sinon il faudrait modifier l'interface du Mapper pour faire évoluer les méthodes en ajoutant des possibilités.

Blount a écrit:

- ensuite, lorsque je liste les utilisateurs, je souhaite afficher le titre des galeries de celui ci, pour chaque utilisateur une requête sera effectuée. Imaginons que mes 1000 utilisateurs aient au moins 1 galerie, alors au mieux nous aurons 1000 requêtes nécessaires à l'affichage des galeries ?

Oui c'est le fonctionnement, comme la plupart des ORM. Tu utilises finalement peu lazy, sauf pour des requêtes qui ne sont pas gourmandes. Même avec un ORM pro, la plupart du temps, pour des raisons de performances, le lazy ne sera pas utilisé, mais plus une requête complexe sur la base.
Bien que je doute que tu affiches 1000 utilisateurs à la fois dans une ui utilisateur par ex., tu auras une pagination. Ce serait une évolution à apporter.

Alors, petit avertissement tout de même, comme tu le dis je suis passé à Doctrine, et surtout en vue de la version 2. Mais aussi parce qu'une équipe de dev pro est bien plus performante qu'un pauvre développeur solo qui pars dans le délire d'écrire son petit ORM maison.

Doctrine est très bien fourni, et conviendra à des environnements pro et nécessitant de bonnes performances.
Il règle la première question, pour la 2ème, tu seras confronté au même problème avec tous les ORM. Dans Doctrine avec le DQL (leur langage de requêtes inspiré du HQL de Hibernate en JAVA) permet de requêter directement sur les modèles et remonte directement des entités, que tu pourras décider ou non, selon une config, de charger en full (donc avec toutes les dépendances peuplées) ou en lazy, mais je ne sais pas quelle est la requête qui est exécutée derrière et si elle est suffisamment optimisée, j'ai pas regardé la source :p

Il manque ici beaucoup beaucoup de fonctionnalités. Il faut garder en tête que mes propositions sont des bases de réflexions et les codes que je donne en exemple ne sont surtout pas à utiliser tels quels en production :p

De les avoir implémentés permet de bien voir et bien comprendre les patterns et les mécanismes derrière nos ORM.


A+ benjamin.


http://www.anonymation.com/ - anonymation - Studio de création.
http://code.anonymation.com/ - anonymation - blog - développement et architecture web

Hors ligne

 

#9 01-12-2009 21:12:10

citronbleu-v
Membre
Lieu: Béziers ou Arles
Date d'inscription: 03-02-2009
Messages: 79
Site web

Re: [Réflexion]Model: Lazy load Mapper/Proxy/Objet métier

Si je comprends bien les objets métier ne possèdent aucune méthode du genre save delete etc.. qui utiliserai le Mapper ? Est ce que tu aurai un exemple d'une méthode qu'il pourrait comporter car je ne vois pas vraiment comment bien l'utiliser.

Doctrine interviendrai à quel niveau dans ton code si on compte l'intégrer car je ne le connais pas du tout et j'ai vu qu'il sera implémenter dans Zend par la suite ?

J'aurais une autre petite question pourquoi tu fais une méthode save (update) et create (insert). Moi j'ai une méthode save qui fait update ou insert.

Hors ligne

 

#10 03-12-2009 09:45:29

Delprog
Administrateur
Date d'inscription: 29-09-2008
Messages: 670

Re: [Réflexion]Model: Lazy load Mapper/Proxy/Objet métier

Salut,

Les objets métiers ne portent pas de méthodes liées à la persistance (save, delete, etc.), ils sont "persistent ignorant" et ne portent que du métier.
Par ex. je gère une banque, j'ai des clients et des comptes, j'aurai une entité "Client" et une entité "Account", je veux virer de l'argent d'un compte à un autre, dans le métier ça se traduira simplement par quelque chose du genre (en gros vite fait :p) :

Code:

$account1 = $this->_accountMapper->find($aid);
$account2 = new Model_Account();
// ....
$account1.transferMoneyTo(1000, $account2);

Le modèle métier (Domain Model) est le coeur de l'application. Cette couche ne doit dépendre d'aucune autre.

Le save est déclenché via le mapper en passant l'entité à persister.
Ex.

Code:

$user = new Model_User();
$user->name = 'mon joli nom';
$this->_userMapper->save($user);

Doctrine, n'intervient pas dans mon cote, il le remplace tout bonnement smile (et en mieux)
Doctrine génère lui même les entités et ensuite tout s'articule autour de ça. Je t'invite à lire la doc Doctrine dans l'ordre, tu y trouveras beaucoup de réponses.

Attention cependant, la version actuelle de Doctrine et celle qui sera intégrée dans Zend ne fonctionnent pas du tout de la même manière smile
Je suppose que Zend 2.0, tout comme Symfony 2, intégrera directement la version 2 de Doctrine qui introduit les patterns Entity, EntityManager, et UnitOfWork (plus d'infos toujours dans la doc, de la 2.0 cette fois).
En gros ces patterns permettent de vraiment rendre les entités métiers totalement indépendantes de la persistance et que tout se passe par magie, ce qui permet de se concentrer sur son modèle métier et de l'enrichir sans être bloqué par l'ORM.


A+ benjamin.


http://www.anonymation.com/ - anonymation - Studio de création.
http://code.anonymation.com/ - anonymation - blog - développement et architecture web

Hors ligne

 

#11 03-12-2009 20:52:17

citronbleu-v
Membre
Lieu: Béziers ou Arles
Date d'inscription: 03-02-2009
Messages: 79
Site web

Re: [Réflexion]Model: Lazy load Mapper/Proxy/Objet métier

Si je comprends bien ni Objets proxy et Objets métier ne peuvent contenir de méthode save, insert etc.. mais peuvent telle contenir des setter du genre :

Code:

function setPath($path)
{
$this->_path = $path
$this->_pathInfo = pathinfo($path);
return $this;
}
// Ou 
function setName($name)
{
$this->_name = uppercase($name);
}
// Ou encore
function getNameEnMajuscule()
{
return uppercase($this->getName());
}

Ou ce n'est pas du tout le rôle de ces objets mais de celui qui peuple ?

Dernière modification par citronbleu-v (03-12-2009 20:59:29)

Hors ligne

 

#12 03-12-2009 21:33:11

citronbleu-v
Membre
Lieu: Béziers ou Arles
Date d'inscription: 03-02-2009
Messages: 79
Site web

Re: [Réflexion]Model: Lazy load Mapper/Proxy/Objet métier

Je prends un exemple simple, je veux enregistrer un fichier :

Code:

// dans ma classe Service_File

function save(Model_File $file)
{
$newPath = ......;
rename($file->path, $newPath);
.......
$file->setPath($newPath); 
$file->setName( $this->functionPourTrouverLeNomDuFichier($newPath) ); 
/** 
Doit on changer le nom obligatoirement ici ou avons nous le droit de le faire directement dans dans la méthode setPath de l'objet Model_File. 
Je suppose que non car si on ne veut pas que la méthode setPath() change la propriété name automatiquement dans une autre partie de l'application on est foutu. 
Cependant si on veut que le nom soit d'abord traité par la functionPourTrouverLeNomDuFichier dans la majorité des cas, comment faire ? où doit on mettre cette fonction et comment se souvenir dans quelques années qu'il faut l'appliquer dans la majorité des cas ?
*/

$this->getMapper()->save($file) //ou DAO
}

Dernière modification par citronbleu-v (03-12-2009 21:43:38)

Hors ligne

 

#13 04-12-2009 09:20:59

Delprog
Administrateur
Date d'inscription: 29-09-2008
Messages: 670

Re: [Réflexion]Model: Lazy load Mapper/Proxy/Objet métier

Salut,

citronbleu-v a écrit:

Si je comprends bien ni Objets proxy et Objets métier ne peuvent contenir de méthode save, insert etc.. mais peuvent telle contenir des setter du genre :

L'objet métier ne doit rien savoir d'autre que le métier. Il ne doit effectivement donc pas savoir comment se faire persister. Peu importe la persistance dans ces objets, ils ne portent que des opérations.

Le proxy est un pattern qui permet d'ajouter et d'altérer des fonctionnalités sans toucher aux objets d'origines. Ils sont utilisés notamment pour le lazy-load mais pourraient très bien implémenter une méthode save. Dans ce cas, comme ils devraient avoir accès à la connexion DB, il faudrait leur injecter automatiquement au runtime les composants nécessaire pour requêter la base.

Je préfère quand même passer par le mapper dans ce cas là, puisque c'est son rôle.

Avec Doctrine, j'utilise le save() sur les objets, mais dans la couche de persistance uniquement. Par ex. dans mon service je vais demander à mon DAO de sauvegarder mon objet et dans le DAO je ferai monObjet.save().

Pour ton exemple, j'aurais plutôt tendance à factoriser le changement de path + nom dans une 3ème méthode de l'objet métier.


A+ benjamin.


http://www.anonymation.com/ - anonymation - Studio de création.
http://code.anonymation.com/ - anonymation - blog - développement et architecture web

Hors ligne

 

#14 04-12-2009 10:42:44

philippe
Administrateur
Lieu: Grenoble
Date d'inscription: 01-03-2007
Messages: 1624

Re: [Réflexion]Model: Lazy load Mapper/Proxy/Objet métier

@delprog :
Sur ce point de la méthode "save" dans l'objet métier, même si d'un point de vue théorique, je vois bien les arguments (et j'y adhère plutôt), je suis resté plusieurs fois sur un cas pratique à problème :

Suite à une requête, je peux récupérer une liste d'objets dont je ne connais pas le type. Je ne connais qu'une interface commune, mais pas la classe elle même. Dans ce cas précis, avoir à disposition la méthode save (ou la méthode delete) sans faire de instanceof pour aller chercher le mapper, c'est quand même bien pratique.

Etant un grand utilisateur d'interface c'est un cas que je rencontre régulièrement.

A+, Philippe


twitter : @plv ; kitpages.fr : Création de sites internet à Grenoble et Paris

Hors ligne

 

#15 04-12-2009 21:48:30

citronbleu-v
Membre
Lieu: Béziers ou Arles
Date d'inscription: 03-02-2009
Messages: 79
Site web

Re: [Réflexion]Model: Lazy load Mapper/Proxy/Objet métier

Pour reprendre l'exemple de la méthode save().

J'ai 1 endroit du site où je veux faire une sauvegarde d'un objet File avec le nom du fichier respectant la convention 'typefile_nomfile.jpg' et un autre endroit du site où le nom des fichiers seront 'id_typefile_nomfile.jpg'.

Je dois obligatoirement ne pas mettre la méthode save() dans l'objet File ou du moins faire aucun traitement particulier dedans ? car si on met dans une éventuelle méthode save() de l'objet File $this->setNameFile($this->name .'.jpg'); du coup le traitement du changement de nom effectué dans le Service sera écrasé.

Donc si je comprends bien l'objet métier c'est vraiment restreint. Plus on en met des traitements et plus on est bloqué si on nous demandes de faire un changement particulier dans l'avenir. A moins de faire des objets métiers différents avec des héritages en pagaille du Domain_File. 

--------------

Je me pose une autre question qui m'a déjà bloqué et qui sort un peut du contexte.

Considérons que j'ai mon objet métier File (proxy) + 2 objets métier Son et Vidéo qui l'étende.

// $son > objet Son
$son->setType('video'); // et pouf ça doit devenir un objet vidéo. Je ne vois pas comment faire la transformation, je suppose que c'est pas possible, il faut recréer un objet Vidéo que l'on peuple de l'objet Son ?

--------------

Encore une autre pitite chose... smile.  Tu as un exemple de traitement particulier qu'il faut faire dans un objet Service et dans un objet Dao j'ai du mal à voir la différence de rôles.

Merci encore en tout cas pour cette vision du monde ^^.

Hors ligne

 

#16 04-12-2009 22:52:14

Blount
Membre
Date d'inscription: 23-06-2009
Messages: 98
Site web

Re: [Réflexion]Model: Lazy load Mapper/Proxy/Objet métier

citronbleu-v a écrit:

Je me pose une autre question qui m'a déjà bloqué et qui sort un peut du contexte.

Considérons que j'ai mon objet métier File (proxy) + 2 objets métier Son et Vidéo qui l'étende.

// $son > objet Son
$son->setType('video'); // et pouf ça doit devenir un objet vidéo. Je ne vois pas comment faire la transformation, je suppose que c'est pas possible, il faut recréer un objet Vidéo que l'on peuple de l'objet Son ?

Pour ce cas là, ne dois-tu pas créer un objet "Transformation" (on va dire ça comma ça) qui serait chargé de transformer un objet en un autre, puisque seul lui saurait comment convertir un objet Son en objet Video.
Les deux objet Son et Video, d'après ce que je comprend ne doivent pas savoir comment se transformer, mais un convertisseur le sait.
Par exemple, tu prends un fichier MP3, tu veux le convertir en vidéo (pourquoi ? ^^), sur ton disque dur, le fichier ne sais pas comment se convertis en vidéo, mais un logiciel externe (ou même, le système d'exploitation), peut le savoir.
Donc oui, le convertisseur génère un fichier vidéo vide, et le "peuple" à partir de l'objet son.

Hors ligne

 

#17 05-12-2009 01:25:18

citronbleu-v
Membre
Lieu: Béziers ou Arles
Date d'inscription: 03-02-2009
Messages: 79
Site web

Re: [Réflexion]Model: Lazy load Mapper/Proxy/Objet métier

Delprog a écrit:

public function getGalleries()
    {
        if (is_null(parent::getGalleries())) {
            $mapper = new Model_Db_Mapper_Gallery();
            $objSet = $mapper->getGalleriesForUser(parent::getId());
            parent::setGalleries($objSet);
        }
        return parent::getGalleries();
    }

Pourquoi as tu fais parent::setGalleries($objSet); ce n'est pas le mapper quoi doit appeler cette méthode en faisant $mapper->getGalleriesForUser($this);  ?

Hors ligne

 

#18 07-12-2009 14:44:43

Delprog
Administrateur
Date d'inscription: 29-09-2008
Messages: 670

Re: [Réflexion]Model: Lazy load Mapper/Proxy/Objet métier

Salut,

@philippe :

J'essaie de voir dans quel cas tu es embêté mais je ne vois pas. Comme j'encapsule mes traitements dans mes services, je ne répond qu'à des cas d'utilisation et je n'ai pas ce genre de problème. Après tout dépend sans doute des traitements que l'on doit faire. Tu as un exemple ?

@citronbleu-v :

citronbleu-v a écrit:

Donc si je comprends bien l'objet métier c'est vraiment restreint. Plus on en met des traitements et plus on est bloqué si on nous demandes de faire un changement particulier dans l'avenir. A moins de faire des objets métiers différents avec des héritages en pagaille du Domain_File.

Non c'est le contraire, il faut faire porter le plus de traitement possible par nos objets métiers, mais que des opérations qui concernent le métier pour lequel on développe. Il faut simplement designer le modèle sans se soucier de la persistance, en considérant qu'elle sera magique. Une fois que le modèle métier tient la route, là il faut ensuite réfléchir aux requêtes et à la persistance.

Quand je dis que du métier c'est, pour prendre un exemple, je fais un site communautaire de photos, ajouter un album photos c'est un traitement métier d'un propriétaire d'albums (AlbumOwner => user), je vais avoir une méthode makeAlbum() dans mon AlbumOwner qui ajoute un objet Album à sa propriété Albums (qui est une collection) sans se soucier de comment ce sera sauvegardé. Par contre, s'authentifier par ex. ça ne sera pas du métier.


citronbleu-v a écrit:

Considérons que j'ai mon objet métier File (proxy) + 2 objets métier Son et Vidéo qui l'étende.

Cette remarque me laisse penser que tu n'as pas saisi le rôle du proxy smile
Le proxy n'est là que pour ajouter la "magie" dont je parle, aucun autre objet métier ne doit l'étendre, surtout pas.


citronbleu-v a écrit:

Encore une autre pitite chose... .  Tu as un exemple de traitement particulier qu'il faut faire dans un objet Service et dans un objet Dao j'ai du mal à voir la différence de rôles.

Hum, je vais essayer de faire un truc simple :p
Le service encapsule des opérations métiers pour répondre à des cas d'utilisation. Leur granulité ne doit pas être fine. Le service ne connait rien de la persistance, il sait par contre comment déclencher les opérations dans la persistance par le biais des DAO.
Le DAO fait partie de la couche d'accès aux données et c'est lui qui cause dans le langage de l'ORM.

Si la BDD, l'ORM, ou autre changent, ce sont les DAO qui sont refactorisés et seulement eux.

Ex. toujours avec mes albums. Je design un site dans lequel un utilisateur pourra ajouter et gérer des albums photos.

Si je pense UML, je vais écrire un package "Album" qui contiendra des cas d'utilisations (que peut faire un acteur sur le système "album" ?). L'un d'eux sera "créer un album", je traduis mon package en service :

AlbumService:

Code:

public function createAlbum($albumData, $userId)
{
    $user = $this->_userRepository->find($userId); // repository ou DAO
    if (!$data = $this->_albumValidator->validate($albumData)) {
        // ....
    }
    $album = $user->makeAlbum($data);
    $this->_userRepository->save($user);
        
    return $album;
}

UserRepository (ou DAO) :

Code:

public function find($id)
{
    try {
        return Doctrine::getTable('Model_User')->find($id);
    }
    catch (Exception $e) {
        throw new DAOException($e->getMessage());            
    }
}

//....

public function save(Model_User $user)
{
    try {
        $user->save();
    return true;
    }
    catch (Exception $e) {
        throw new DAOException($e->getMessage());                
    }
}

citronbleu-v a écrit:

Pourquoi as tu fais parent::setGalleries($objSet); ce n'est pas le mapper quoi doit appeler cette méthode en faisant $mapper->getGalleriesForUser($this);  ?

J'ai utilisé le pattern proxy pour pouvoir faire par ex. (attention ce n'est pas performant de faire ça :p) :

Code:

foreach($user->getGalleries() as $gallery):
    // echo $gallery->title;
endforeach;

De cette manière la dépendance est chargée automatiquement lorsqu'on appelle pour la première fois getGalleries() sur l'user, c'est le principe du lazy loading.

Ce n'est pas du tout obligatoire, et il est possible de passer par le mapper. C'est juste pratique et ça permet d'avoir un modèle métier consistant dans lequel on récupère naturellement les relations. Dans le cas des galleries c'est pas très optimisé, mais si par ex. je veux le profil d'un user, c'est bien plus pratique de faire directement $user->getProfile()->name, $user étant un proxy, les requêtes pour peupler la dépendance "profile" se feront automatiquement et seulement si nécessaire.


A+ benjamin.


http://www.anonymation.com/ - anonymation - Studio de création.
http://code.anonymation.com/ - anonymation - blog - développement et architecture web

Hors ligne

 

#19 07-12-2009 17:03:54

philippe
Administrateur
Lieu: Grenoble
Date d'inscription: 01-03-2007
Messages: 1624

Re: [Réflexion]Model: Lazy load Mapper/Proxy/Objet métier

@delprog :
Dans une base de données, j'ai des restaus, des vidéos touristiques, des pompes à essence (dans 3 tables séparées, avec des données différentes).
J'ai une interface Item_Interface qui est implémentée par ces trois types d'objets.

J'ai en interne de mon site un "moteur de requêtage" qui suivant certains critères me renvoie une liste d'objets de type Item_Interface. Si je veux faire des traitements sur cette liste (par exemple effacer tous les objets récupérés par ma requête), c'est pratique d'avoir la méthode "delete()" accessible sur l'objet. Si je ne l'ai pas, je suis obligée de faire

Code:

if ($objet instanceof Garage) GarageMapper->delete($objet);
if ($objet instanceof Restaurant) RestaurantMapper->delete($objet);
...

Si j'ai accès à ma méthode delete(), il me suffit de faire

Code:

$object->delete();

Donc dans ce cas, avoir la méthode delete() dans l'objet permet de mieux décorréler mon code applicatif de mon modèle (dans la mesure où ajouter un type de données supplémentaire dans mon modèle ne modifie pas le code de mon appli ou de mon service).

A+, Philippe
PS : je fais l'avocat du diable... cette pratique me chiffonne un peu d'un point de vue théorique, notamment à cause des objets zombies qui se baladent après, mais ça m'arrive cependant assez souvent d'y recourir pour la raison ci-dessus (la décorrélation...)


twitter : @plv ; kitpages.fr : Création de sites internet à Grenoble et Paris

Hors ligne

 

#20 08-12-2009 09:13:01

nORKy
Membre
Date d'inscription: 06-03-2008
Messages: 1098

Re: [Réflexion]Model: Lazy load Mapper/Proxy/Objet métier

Faut pas non plus jurer que par les interfaces et oublié l'héritage de classe !
Sous Doctrine, c'est gérer ca. On déclare qu'un "garage" est un "lieu", une "pompe à essence" est "un lieu" et on peut requêter sur "lieu" et obtenir une collection de pompes et/ou garages selon les cas, et faire des delete sur "lieu" ou des save dans une collection de "lieu"


----
Gruiiik !

Hors ligne

 

#21 08-12-2009 09:31:40

philippe
Administrateur
Lieu: Grenoble
Date d'inscription: 01-03-2007
Messages: 1624

Re: [Réflexion]Model: Lazy load Mapper/Proxy/Objet métier

Oui mais mon garage hérite déjà de quelque chose et mon restaurant de quelque chose d'autre.

Mon interface ici sert à simuler de l'héritage multiple. Dans un projet complexe et assez hétérogène on a souvent ce genre de problème.
Prenons un exemple concret : si on a un système d'information existant contenant des produits variés (avec déjà une hiérarchie de classes existante) et qu'on veut ajouter une boutique en ligne, ça me paraît être la solution la plus directe d'ajouter une interface "Buyable" sur les classes des objets vendables pour dire qu'un produit peut entrer dans le panier de la boutique. On va fatalement ajouter également dans le modèle une méthode qui récupère tous les objets "Buyable" qui coûtent moins de 15€, entre 15 et 30€,... Cette fonction renvoie donc une liste de "Buyable" dont on ne connaît pas le type sous-jacent.

Comme ça la boutique se fout complètement des types des produits qu'elle vend et on n'a pas eu à casser tout le système préexistant. Là on revient exactement au cas que j'évoquais plutôt (j'avais un peu simplifié le problème pour le rendre plus lisible).

Bref je n'utilise pas les interfaces pour la beauté du geste, c'est véritablement un besoin quand on branche des fonctionnalités "transverses" sur un système déjà existant (ou si on veut décorréler des fonctionnalités des objets manipulés).

A+, Philippe


twitter : @plv ; kitpages.fr : Création de sites internet à Grenoble et Paris

Hors ligne

 

#22 04-01-2010 17:16:51

citronbleu-v
Membre
Lieu: Béziers ou Arles
Date d'inscription: 03-02-2009
Messages: 79
Site web

Re: [Réflexion]Model: Lazy load Mapper/Proxy/Objet métier

Delprog a écrit:

Cette remarque me laisse penser que tu n'as pas saisi le rôle du proxy smile
Le proxy n'est là que pour ajouter la "magie" dont je parle, aucun autre objet métier ne doit l'étendre, surtout pas.

Est ce qu'il y a une aide en ligne qui explique bien les Proxy et les Domain Object car j'ai toujours pas bien cerner certaine chose comme :

Si un objet Domain_Video (extends Domain_Document) et Domain_Musique (extends Domain_Document) possèdent tous 2 le même Proxy (Document).

Comment faire ? on ne va pas faire Video extends Domaine_Video et Musique extends Domaine_Musique sachant que les 2 classes possèdent les mêmes méthodes exceptés 1 histoire de compliquer ?

Je me pose peut être des questions non existentielle ^^ ?

Hors ligne

 

#23 05-01-2010 09:45:29

Delprog
Administrateur
Date d'inscription: 29-09-2008
Messages: 670

Re: [Réflexion]Model: Lazy load Mapper/Proxy/Objet métier

Salut,

En fait tu pointes du doigt une mauvaise implémentation du proxy de ma part.

Le proxy ne devrait pas étendre l'objet métier mais devrait être indépendant et recevoir une instance de l'objet métier.

Exemple (très simpliste) :

Code:

class Model_Album extends BaseObject implements Model_AlbumInterface
{
    /** @var string */
    protected $_title;

    /** @var ArrayObject|Model_Photo */
    protected $_photos;    
    
    public function addPhoto($title)
    {
        $this->_photos[] = new Model_Photo($title);
        return $this;
    }
}

Le proxy serait :

Code:

class Model_AlbumProxy implements Model_AlbumInterface
{
    /** @var Model_Album */
    protected $_album;

    /**
     * @return Model_Album
     */
    protected function getAlbum()
    {
        if (null === $this->_album) {
            $this->_album = new Album();
        }
        return $this->_album;
    }

    public function addPhoto($title)
    {
         // traitement propre au proxy
         return $this->getAlbum()->addPhoto($title);
    }
}

Ensuite c'est le proxy qui est utilisé en lieu est place de l'objet.

Ce pattern prend tout son sens avec l'injection de dépendance et la programmation par contrat (interfaces). Je m'explique :

Mon exemple montre l'utilisation du pattern pour un objet métier, c'est ce qui permet d'implémenter le lazy load sans toucher à l'objet lui même. Maintenant admettons que j'utilise le pattern proxy pour tout autre chose, un service par ex. J'en parle ici : http://www.z-f.fr/forum/viewtopic.php?id=4027 bien que depuis ma réflexion a évolué.

Supposons le service suivant :

Code:

class Service_Impl_Album implements Service_Album

Dans le controlleur qui consommera le service, je vais écrire un setter pour l'injection de dépendance qui attend non pas un Service_Impl_Album mais l'interface Service_Album.

Code:

public function setAlbumService(Service_Album $albumService)
{
    $this->_albumService = $albumService;
    return $this;
}

Je détermine ensuite dans le fichier de config, quelle classe sera injectée, j'ai choisi pour ma part le format xml :

Code:

<component id="IndexController" class="Default_IndexController">
    <property name="albumService" ref="albumService" />
</component>
<component id="albumService" class="Service_Impl_Album">   <!-- C'est ici que je détermine la classe à injecter au runtime -->
    <property name="albumRepository" ref="albumRepository" />
</component>

J'espère que ça va jusque là smile

Maintenant je veux ajouter la gestion du cache à mon service sans toucher mon service d'origine. Et bien, grâce au pattern proxy (ou decorator qui est assez proche), je peux le faire facilement à condition d'implémenter la même interface :

Code:

class Service_Impl_AlbumCache implements Service_Album
{}

Et dans ma config je n'ai qu'à changer la classe qui doit être injectée :

Code:

<component id="albumService" class="Service_Impl_AlbumCache">   <!-- C'est ici que je détermine la classe à injecter au runtime -->
    <property name="albumRepository" ref="albumRepository" />
</component>

Mon exemple implique bien sûr l'implémentation de l'injection de dépendance via un conteneur, et des services, mais c'est pour illustrer l'intérêt des patterns et la robustesse que ça donne lorsqu'ils sont utilisés de concert.

Je rappelle encore une fois que tout ceci reste de l'expérimentation, la preuve en est que depuis ce post j'ai encore progressé et il faudrait déjà revoir pas mal de choses. Il s'agit juste d'aborder certains concept et de bien les comprendre.


A+ benjamin.


http://www.anonymation.com/ - anonymation - Studio de création.
http://code.anonymation.com/ - anonymation - blog - développement et architecture web

Hors ligne

 

#24 05-01-2010 09:54:26

philippe
Administrateur
Lieu: Grenoble
Date d'inscription: 01-03-2007
Messages: 1624

Re: [Réflexion]Model: Lazy load Mapper/Proxy/Objet métier

@delprog : avec le principe du proxy, quand on fait un find avec le mapper, ça renvoie l'objet métier ou le proxy ?

A+, Philippe


twitter : @plv ; kitpages.fr : Création de sites internet à Grenoble et Paris

Hors ligne

 

#25 05-01-2010 10:24:03

Delprog
Administrateur
Date d'inscription: 29-09-2008
Messages: 670

Re: [Réflexion]Model: Lazy load Mapper/Proxy/Objet métier

@philippe :

Pour répondre au lazy ça doit être un proxy.


A+ benjamin.

Dernière modification par Delprog (05-01-2010 10:25:14)


http://www.anonymation.com/ - anonymation - Studio de création.
http://code.anonymation.com/ - anonymation - blog - développement et architecture web

Hors ligne

 

Pied de page des forums

Propulsé par PunBB
© Copyright 2002–2005 Rickard Andersson
Traduction par punbb.fr

Graphisme réalisé par l'agence Rodolphe Eveilleau
Développement par Kitpages