Zend FR

Consultez la FAQ sur le ZF avant de poster une question

Vous n'êtes pas identifié.

#1 14-09-2009 16:48:14

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

[Partage/Reflexion]MVC, Services, Mapper et Persistance

Bonjour,

Quand nous connaissons certains frameworks sur d'autres langages et les comparons avec ZF, il peut y avoir une petite frustration.
Pour moi, c'est le cas par exemple avec JAVA, et les framework Spring MVC et Hibernate.

Dans le dev web, il est souvent dit qu'il faut savoir ce que nous voulons, soit faire du script et du PHP, soit faire du lourd et du JAVA.
C'est en partie vrai, car puisque JAVA est compilé une seule fois et n'est pas re-compilé à chaque requête, il permet bien plus de concessions en terme de performances (full loading, injections de toutes les dépendances dès l'initialisation de l'appli, etc.).
Avec JAVA viennent des frameworks très avancés qui proposent des implémentations matures de patterns éprouvés et qui dispensent le développeur de ces reflexions.

La POO avec PHP ne s'est démocratisée que très tard, avec l'arrivée de PHP5 qui proposait un véritable environnement objet.
Zend avec ZF tente de tirer ces possibilités vers le haut, d'imposer une rigueur dans le développement et de faire de PHP en langage PRO.
Ils rattrapent peu à peu le retard de PHP sur ces autres langages, mais certaines implémentations concrètent manquent encore à l'appel.

Mais c'est aussi ce qui rend le développement avec PHP et ZF passionnant, puisque le développeur peut laisser aller son imagination.

De mon côté, puisque PHP et ZF ne me proposaient pas les outils qui rendent si heureux dans JAVA, j'ai tenté de les implémenter moi-même et de les adapter à un environnement script.
Certains diront que je ré-invente la roue, certaines librairies bien plus poussées proposent déjà plus ou moins tout ceci, mais le fait de l'implémenter soit-même, quitte à finalement tout jeter pour adopter une solution plus sûre, permet de bien comprendre tous les rouages et les concepts de POO avancée.
Le but ici est donc de vous faire partager ce que j'en ai tiré, et par la même de faire réagir les vieux roublards que j'invite à critiquer.

Zend a introduit, je trouve un peu trop légèrement, la notion de Mapper dans son dernier Quickstart. Tout ceci a soulevé pas mal de questions et oblige beaucoup de développeurs "junior" à s'intéresser un peu plus aux concepts avancés; Tous ces fameux patterns qui sont souvent cités au milieu d'une phrase smile

Tout d'abord quelques éclaircissements.

Qu'est-ce que le "modèle" ?

Le modèle est une réprésentation formalisée du contexte métier. Il représente un ensemble d'entités et d'acteurs, et leurs intéractions dans le métier.
La démarche dans cette formalisation est d'identifier les différentes entités, leurs rôles dans le contexte et les relations entre elles.
Grâce à ça, nous obtenons des objets métiers. Un objet métier est la représentation concrète d'une entité (réelle ou virtuelle).

Par exemple, mon métier est de vendre des bananes par correspondance à mon client.
Nous avons deux entités matériels, la banane et le client. Mais nous pouvons aussi avoir des entités immatériels et/ou virtuelles, un compte en ligne pour le client, un stock, une commande, une facture, etc...

Le modèle, les entités et leurs relations sont représentés sous forme de diagrammes, et le plus souvent avec un diagramme de classes UML.


L'application ?

Pour concrétiser cette organisation métier, nous avons besoin de mettre en place un système d'information.
Ce système d'information inclue toute la dynamique pour répondre au métier.
Une interface client pour la collecte d'informations (formulaires par ex.), le contrôle et le traitement des informations, le stockage de ces informations.

C'est ici qu'entre tout d'abord en jeu le pattern MVC. Nous constatons déjà que nous séparons naturellement ces différentes "couches", et que le pattern MVC n'est qu'une "formalisation" de ce que nous faisons instinctivement.

Le Modèle, organisation du métier (entités), stockage de l'information.

La Vue, interface avec le client, qui affichera les informations à destination du client. Des listes de bananes avec leurs prix, un espace-clients avec un historique, des formulaires de commande, etc. etc.

Le Controlleur, qui dans un sens, va réclamer l'information au Modèle et transmettre ce qui doit-être affiché à la Vue, et dans l'autre sens va vérifier et filtrer l'information provenant du client (depuis la Vue) avant de la transmettre au Modèle.

C'est tout ceci que va englober notre "application".


Indépendance des couches

La tendance et les bonnes pratiques poussent les développeurs, aux travers de patterns, à rendre indépendante chacune des couches du MVC.
Le but de la manoeuvre est tout d'abord de favoriser la maintenance d'un tel système, mais également de permettre un développement indépendant de chacune des couches.

Entre les vues et les controlleurs, ça ne pose pas vraiment de problème. Tout devient délicat quand il s'agit du modèle.

Qui dit modèle veut dire objets métiers, mais également gestion de persistance des données. Il s'agit du système responsable de la sauvegarde et de la restauration des données. Très souvent une base de données relationnelle, mais pas toujours.
Le controlleur a pour rôle de contrôler, non pas de répondre à une problématique métier et encore moins de gérer la persistance des données. Dans le cas d'une bdd par exemple, il ignore totalement l'organisation de la base de données et ne communiquera jamais directement avec elle. L'intérêt d'une telle séparation est de ne pas avoir à refactoriser les controlleurs lorsque des changements sont opérés dans la persistance.
Autre point, avec le web 2.0 et les nouveaux systèmes sociaux, les sites web tendent à s'ouvrir vers l'extérieur. Ils doivent donc proposer une plateforme capable de reçevoir ou de distribuer l'information autre part que dans l'application elle même. Seulement ces services doivent forcément porter de la logique métier pour répondre à une telle demande. Il n'est donc encore une fois pas question que les controlleurs assurent un tel travail. Il serait dommage de faire porter ces traitements par les controlleurs et de devoir tout revoir le jour où il faut s'ouvrir vers l'extérieur.

Il y a donc deux problématiques, d'une part isoler la logique métier et d'autre part détacher les controlleurs de la persistance.

Le ZF actuel propose un ORM. Un ORM ou Mapping Objet-Relationnel permet de manipuler une base de données relationnelle comme s'il s'agissait d'une base de données orientée objet.
Nous manipulons donc au travers de Zend_Db_Table et Zend_Db_Table_Row des objets qui représentent des collections de données et des éléments dans cette collection (lire une table ou une ligne dans la table).
Zend a choisi d'implémenter le pattern Table Data Gateway qui permet de ne pas manipuler directement le langage de la base de données dans le code et le pattern Row Data Gateway qui permet de manipuler un enregistrement dans la base de données comme un objet et de conserver cette objet en mémoire. L'objet reflète dans ce cas exactement l'enregistrement dans la table.

Dans des applications de moindre envergure, les développeurs se contentent la plupart du temps de considérér les Zend_Db_Table_Row comme des objets métiers et les manipulent directement dans les controlleurs. Ce sont donc ces derniers, avec les Zend_Db_Table qui portent les traitements métiers.

Dans des applications plus importantes, ça peut être problématique à plusieurs niveaux.

Premièrement, il est impossible de cette manière de proposer des services vers l'extérieur.

Ensuite, tout ceci lie très fortement le métier à une persistance type base de données. Si plus tard, la gestion de persistance change et qu'il faut consommer par exemple des webservices (le plus souvent du XML) ou interroger des annuaires, il faudra refactoriser l'ensemble de l'application.

Pour répondre à la première problématique, il existe un pattern très souvent utilisé. Il s'agit du Service Layer.


Service Layer

Le Service Layer permet de découper la logique métier en services et de coordonner les intéractions entre les différentes opérations. Cette couche permet de favoriser dans le futur la mise en place de webservices.
Les controlleurs consomment des méthodes des services qui leur sont proposés. Les objets métiers, que nous avons décrit plus haut, sont les seuls objets qui sont connus par toutes les couches de l'application.
Si nous reprenons l'exemple d'une entité Utilisateur à qui correspondrait un Profil. Un service userService qui permettrait de manipuler un utilisateur et son profil (authentification, modifications profil, etc.) serait naturellement mis en place. Par exemple, lors de l'inscription d'un utilisateur, le controlleur récupèrerait, filtrerait et validerait les informations saisis dans un formulaire. Il sait construire un objet Utilisateur et le peupler avec ces informations et sait qu'il peut consommer une méthode createUser() du userService et éventuellement une méthode d'un service messageService pour envoie d'un mail de confirmation à l'utilisateur. Il n'a pas besoin d'en savoir plus, il passe le relais aux services.

Dans l'autre sens, un controlleur a besoin d'un objet Utilisateur pour envoyer des informations à la vue, il accède à des méthodes du service qui lui renvoient ce qu'il attend.

Donc, nous avons répondu à une partie du problème, détacher la logique métier des controlleurs. Mais quand est-il de la gestion de persistance des données ? Nous n'avons fait que déplacer le problème et rendre les services dépendants de la persistance.

Nous pourrions utiliser, comme proposé par Zend dans le dernier Quickstart, les mappers. Mais tels que Zend les introduit, il nous faudrait un mapper pour chaque entité (objet métier) pour faire correspondre les champs dans la base avec les propriétés de l'objet. De plus chaque mapper devrait porter les méthodes pour accéder à la persistance. Et enfin, l'implémentation proposée par Zend réclame en plus un objet Zend_Db_Table pour chaque table dans la base de données.

L'idéal serait d'avoir un unique mapper, capable de construire n'importe quel objet métier. Mais à ce moment là, comment découper logiquement l'accès à la persistance ?


DAO

J'ai décidé d'utiliser le bon vieux pattern DAO (Data-Access Object). Les DAO sont des objets qui cachent la façon dont les données sont stockées dans la persistance. Ils sont découpés dans la même logique que les services. Par exemple un service userService utilisera un DAO userDAO. Ils sont des interfaces qui proposent des méthodes pour accéder à la persistance. Ils ne construisent jamais d'objets métiers. Ils ne font qu'exécuter les ordres des services et renvoyer la réponse attendue.

Ce sont dans ces DAO que nous retrouveront par exemple des méthodes find(), findByTruc(), delete(), save(), etc. Ces méthodes construisent les requêtes pour la persistance et passe le relais à notre Mapper.


Mapper

Ici, j'ai donc choisi d'implémenter un Mapper unique, dépendant du système de gestion de persistance, qui saura, nous verrons comment plus tard, construire n'importe quel objet métier. Ce Mapper communiquera directement avec la base de données au travers de l'adapter Db de Zend_Db. Donc exit les Zend_Db_Table et Zend_Db_Table_Row (bien que parfois pour des raisons pratiques j'utilise directement Zend_Db_Table).

Cela donne donc au final :

Controlleur --> Service --> DAO --> Mapper --> Db

Si le type de base de données change, aucun problème, c'est Zend qui gère. Si la persistance n'est plus une base de données, il faudra écrire un mapper dédié au système et refactoriser uniquement les DAO. Les controlleurs et les services restent intactes.


Objets métiers

Les objets métiers eux, ignorent tout, ils sont construits par le développeur pour répondre au métier et rien d'autre. Ils ne doivent rien savoir, ni des services, ni des DAO, ni du Mapper et de la persistance.
Par contre, les objets ont des relations entre eux, et il serait pratique de récupérer un objet dépendant directement par l'objet lui même en faisant par exemple : $user->profil->age.

Il serait relativement simple lors de la construction d'un objet dans le mapper de peupler directement tous les objets dépendants. C'est ce qu'on appelle le full loading. En PHP, comme les objets sont détruits et reconstruits dans chaque requête, cette pratique n'est pas conseillée. D'autant plus que beaucoup d'objets métiers peuvent être manipulés dans une seule requête. Il est donc plus approprié que les dépendances soient "Lazy-loaded", c'est à dire qu'elles ne soient chargées que lorsqu'elles sont demandées pour la première fois.

Mais comment mettre en place le "Lazy-load" si les objets ne doivent rien connaitre de la persistance ? J'ai décidé d'utiliser le pattern Proxy.

Proxy

Le pattern proxy est très simple. Les objets proxy sont des objets qui étendent les objets d'origine et permettent donc d'en surcharger certaines méthodes. Ils sont souvent utilisés pour agrémenter des objets métiers sans modifier leur conception d'origine. Dans notre cas, c'est le pattern idéal.

Grâce au proxy, nous pouvons surcharger les getter des objets dépendants d'un objet, et dans ces méthodes communiquer directement avec le mapper pour remonter les objets depuis la persistance.

Exemple :

Nous avons une entité Entity_User auquel est rattachée une entité Entity_Profil (et inversement). L'entité Entity_User possèdera donc une propriété _profil, réceptacle d'un objet Entity_Profil, avec biensûr le getter et le setter appropriés.
Lors de la construction de l'objet Entity_User, rien n'est fait au niveau de la propriété _profil, elle est par défaut à null.

Un objet proxy User est créé, il étend Entity_User et surcharge la méthode getProfil() dans laquelle il demande au mapper, si _profil n'est pas null, de lui retourner un objet Entity_Profil peuplé. Nous pouvons donc faire, lorsque nous en avons besoin : $user->getProfil()->getAge() (ou $user->profil->age avec la méthode magique __get de PHP).

Mieux encore, si au lieu de placer dans la propriété _profil un objet Entity_Profil je place dedans un proxy Profil, je pourrais faire : $user->profil->user->name.
Et tout est naturel.

Dans l'application, le développeur n'utilise que les proxy. Dans les tests unitaires, le développeur utilise les objets "entités" et les peuple manuellement.


Injection de dépendances

Dans notre conception, beaucoup de dépendances (entre objets) sont générées. Un controller a besoin d'un service, un service d'un DAO, le DAO du mapper, le Mapper de l'adaptateur Db, etc.
Mais il existe aussi d'autres dépendances, les controlleurs auront besoin d'autres objets, comme des Zend_Translate, des Zend_Acl, Zend_Session, etc. etc.

Lors des tests unitaires, il est parfois nécessaire de remplacer les objets d'origine par des objets "de substitution" qui sont construits à la mains. Si dans les controlleurs, services, dao etc. les objets sont créés en "dur", c'est à dire que des new Objet(); pour ses dépendances sont implémentés dans l'objet lui même, les tests seront impossibles.

La notion d'injection de dépendances ou IOC (Inversion Of Controls) introduit dans JAVA permet de pallier à ce problème. En JAVA, les dépendances sont créées via le constructeur ou via des setters. Il n'y a donc plus qu'à créer les propriétés et leurs setters et de laisser faire le framework qui sait, au travers d'une définition des relations dans des fichiers de configuration (ou aujourd'hui des annotations), qu'elles sont les dépendances à injecter et les injecte par le constructeur ou par appel du setter.

Dans la théorie c'est simple. Une propriété, un setter, c'est propre, c'est bien. Mais, un problème se pose, à quel moment instancier les dépendances et les injecter ? En JAVA avec Spring MVC, par défaut, toutes les dépendances sont instanciées dès l'initialisation de l'application, facile.

En PHP, encore une fois, les objets étant détruits et reconstruits à chaque requête, ce n'est pas envisageable. Diverses solutions, parfois simples, d'autres très complexes, proposent l'implémentation d'un conteneur. Zend_Application lui même stocke toutes ses ressources dans un conteneur (par défaut Zend_Registry).
Dans le code, les dépendances sont accessibles via le conteneur ($container->getUserService()), et ne sont pas injectées directement. Un appel au conteneur est obligatoire. Cette solution, bien qu'efficace, ne me convient pas car nous créons une forte dépendance au conteneur.
J'ai donc pris la décision d'implémenter un conteneur qui remplace Zend_Registry, dans lequel je définis toutes les dépendances, de créer systématiquement toutes les instances et de les injecter récursivement à la méthode de Spring MVC en JAVA à un détail près. Contrairement à Spring, j'ai mis en place un helper d'action qui n'injecte que les dépendances à partir du controlleur demandé.
Ce n'est pas encore idéal en terme de performances, mais ça a le mérite d'être flexible. J'ai des propriétés et des setters, c'est propre, pas d'appels à un conteneur. Mais j'ai quand même un véritable conteneur de dépendances.


Mais...mais....tout ce code à écrire ?!

Tout cette représentation nécessite beaucoup de code à implémenter. C'est pourquoi, j'ai également décidé de créer un générateur de classes, qui, à partir d'un fichier XML de définition du modèle, génère les objets métiers, les objets proxy, et les DAO (vierges).
Le Mapper étant une classe unique, celà ne pose pas de problème, ce n'est à faire qu'une seule fois. Si ce n'est écrire un mapper en fonction du système de gestion de persistance.


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

Hors ligne

 

#2 14-09-2009 16:49:15

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

Re: [Partage/Reflexion]MVC, Services, Mapper et Persistance

C'est parti pour du code. A prendre avec des pincettes, c'est en cours de dev.


Injection de dépendances

Pour commencer, le conteneur pour l'injection de dépendances. Il est possible de spécifier à Zend_Application le conteneur qui sera utilisé pour stocker les ressources (lors du return dans les Zend_Application_Resource_*). Le conteneur est très facile à changer, la seule condition est d'implémenter les méthodes magiques __set, __get et __isset qui seront utilisées par Zend.

Code:

<?php
/**
 * Tight_Di - Contains all dependencies references
 *  
 * @category Tight
 * @package Tight_Di
 * @author Benjamin Dulau
 */
class Tight_Di  {
    
    /**
     * Array of instances     
     * @var array
     */
    protected $_instances = array();
    
    /**
     * Array of registered dependencies     
     * @var array
     */
    protected $_components = array();

    
    /**
     * Constructor - calls load method for each file
     * 
     * @param array $componentFiles
     * @return void
     */
    public function __construct(array $componentFiles = array())
    {
        foreach($componentFiles as $file) {
            $this->load($file);
        }
    }

    /**
     * Loads file and builds components definitions
     * 
     * @param string $componentFile
     * @return bool success
     * @throws Tight_Di_Exception if unable to load file as XML
     */
    public function load($componentFile)
    {        
        if (false === file_exists($componentFile) || false === is_file($componentFile)) {
            Throw new Tight_Di_Exception(Tight_Di_Exception::NO_SUCH_FILE . ': ' . $componentFile);
            return false;                
        } 
        
        $xml = null;
        if (false === ($xml = simplexml_load_file($componentFile))) {
            Throw new Tight_Di_Exception(Tight_Di_Exception::FILE_LOAD_FAILED . ': ' . $componentFile);
            return false;            
        };    

        foreach ($xml->component as $XMLElement) {
            $component = array();
        
            // id
            $id = (string)$XMLElement->attributes()->id;
            $component['class'] = (string)$XMLElement->attributes()->class;        

            // dependencies
            $component['dependencies'] = array();
            foreach($XMLElement->property as $property) { 
                $name = (string)$property->attributes()->name;
                $refComponent = (string)$property->attributes()->ref;
                                
                $component['dependencies'][$name] = $refComponent;                                             
            }
            
            $this->addComponent($id, $component);
        }
        
        return true;
    }

    /**
     * Adds a dependency reference to container     
     *
     * @param  array $dependency config array for provided dependency     
     * @return Tight_Di
     */
    public function addComponent($key, array $component) {                
        $this->_components[$key] = $component;
        return $this;        
    }
    
    /**
     * Returns component definition
     * 
     * @param string $key
     * @return array
     */
    public function getComponent($key) {        
        return $this->_components[$key];        
    }
    
    /**
     * Returns all components definitions array
     * 
     * @return array
     */
    public function getComponents()
    {
        return $this->_components;
    }
    
    /**
     * Retrieves component instance.
     * Creates instance if none exists yet
     * 
     * @param $key
     * @return mixed
     */
    public function getInstance($key)
    {
        $component = $this->getComponent($key);
        if (false === array_key_exists($key, $this->_instances)) {
            $this->_instances[$key] = new $component['class']();            
        }
        
        return $this->_instances[$key];
    }
    
    /**
     * Injects recursively dependencies starting by $key
     *  
     * @param $key
     * @param $target
     * @return void
     */
    public function inject($key, $target)
    {        
        $component = $this->getComponent($key); 
        foreach($component['dependencies'] as $property => $ref) {            
            $var = '_' . $property;
            $setter = 'set' . ucfirst($property);
            
            $instance = $this->getInstance($ref);        
            $target->$setter($instance);
            
            // recursive injection for dependency dependencies
            $this->inject($ref, $instance);
        }
    }
    
    /**
     * Sets directly an instance into instances.
     * Used by Zend_Application on resource plugin return.     
     *
     * @param string $key
     * @param mixed $instance
     * @return Tight_Di
     */
    public function __set($key, $instance)
    {
        /*$component = array();                               
        $component['class'] = get_class($instance);        
        
        // dependencies
        $component['dependencies'] = array();                        
        $this->addComponent($key, $component);*/

        $this->_instances[$key] = $instance;
        
        return $this;    
    }

    /**
     * Gets the given instance
     * Used by Zend_Application on getInvokArg()
     *
     * @param string $key
     * @return mixed instance
     */
    public function __get($key)
    {        
        return $this->getInstance($key);
    }
    
    /**
     * Tests if an object exists into instances
     * Used by Zend_Application
     *
     * @param string $key
     * @return bool
     */
    public function __isset($key)
    {        
        return array_key_exists($key, $this->_instances);
    }
}

Pour indiquer à Zend_Application d'utiliser ce conteneur c'est très facile smile
index.php

Code:

/** Tight_Di */
require_once 'Tight/Di.php';

$container = new Tight_Di(array(
    APPLICATION_PATH . '/config/cpt-resources.xml',
    APPLICATION_PATH . '/config/cpt-controllers.xml',
    APPLICATION_PATH . '/config/cpt-services.xml',
    APPLICATION_PATH . '/config/cpt-daos.xml'    
));

/** Zend_Application */
require_once 'Zend/Application.php';

// Creates application, bootstrap, and run
$application = new Zend_Application(
    APPLICATION_ENV, 
    APPLICATION_PATH . '/config/application.ini'
);

$application->getBootstrap()->setContainer($container);
$application->bootstrap()
            ->run();

Les conteneurs pour l'IOC deviennent très vite très complexes quand il s'agit de gérer le passage de paramètres lors de l'instanciation de la dépendance pour la première fois. De mon côté j'ai pas cherché bien loin. Toutes les dépendances qui nécessitent une configuration sont pour moi des Zend_Application_Resource et les paramètres sont placés dans application.ini. Elles sont systématiquement chargées au départ, mais je trouve que la concession vaut le coup.

Pour ce qui est du XML, c'est du pure Spring MVC sauf que les nœuds ne se nomment pas "bean". J'ai découpé les XML par type de ressources, mais évidemment ce n'est pas obligatoire.

Pour les ressources Zend_Application_Resource, dont les instances existent dès le départ, il y a deux choix. Soit toutes les re-déclarer dans le XML pour que le conteneur les récupère lors de sa construction, soit le faire dans la méthode magique __set du conteneur (voir code commenté).

Donc, exemples de XML

Code:

// cpt-resources.xml
// les classes ici n'ont pas vraiment d'importance
<?xml version="1.0" encoding="UTF-8"?>
<components>
    <component id="db" class="Zend_Db" />        
    <component id="exception" class="Zend_Exception" />
    <component id="modules" class="" />
    <component id="exception" class="Zend_Exception" />
    <component id="Navigation" class="Zend_Navigation" />
    <component id="Routeur" class="Zend_Router" />
    <component id="frontcontroller" class="Zend_Controller_Front" />
    <component id="layout" class="Zend_Layout" />
    <component id="view" class="Tight_View" />
    <component id="locale" class="Zend_Locale" />
    <component id="translate" class="Zend_Translate" />    
    <component id="session" class="Zend_Session" />
    <component id="config" class="Zend_Config" />
    <component id="modeltemplate" class="Tight_Model_Template" />
    <component id="mailer" class="Tight_Mail" />
    <component id="acl" class="Tight_Acl" />
</components>

// cpt-controllers.xml
<?xml version="1.0" encoding="UTF-8"?>
<components>
    <!-- Default module -->
    <component id="indexController" class="Default_IndexController">
        <property name="tr" ref="translate" />
    </component>
    <component id="errorController" class="Default_ErrorController">
        <property name="tr" ref="translate" />
    </component>    
    <!-- Sign module -->
    <component id="signIndexController" class="Sign_IndexController">
        <property name="tr" ref="translate" />
        <property name="userService" ref="userService" />
        <property name="mailService" ref="mailService" />
    </component>
</components>

// cpt-services.xml
<?xml version="1.0" encoding="UTF-8"?>
<components>
    <component id="userService" class="Service_User">
        <property name="userDAO" ref="userDAO" />
        <property name="acl" ref="acl" />
    </component>
    <component id="mailService" class="Service_Mail">
        <property name="mailer" ref="mailer" />
    </component>
</components>

// cpt-daos.xml (avec mapper)
<?xml version="1.0" encoding="UTF-8"?>
<components>
    <component id="userDAO" class="Model_Dao_User">
        <property name="mapper" ref="mapper" />
    </component>
    <component id="mapper" class="Model_Db_Mapper">
        <property name="db" ref="db" />
        <property name="template" ref="modeltemplate" />
    </component>
</components>

Je pense que le XML est assez simpliste et clair pour comprendre ce qu'il se passe smile

Pour ce qui est de l'injection en elle même, c'est un helper, un peu moins propre lui, peut-être une partie à revoir.

Code:

<?php
/**
 * Provide IOC
 *
 * @uses       Zend_Controller_Action_Helper_Abstract
 * @category   Tight
 * @package    Tight_Controller
 * @author       Benjamin Dulau
 */
class Tight_Controller_Action_Helper_Di extends Zend_Controller_Action_Helper_Abstract 
{          
    /**
     * @var bool
     */
    protected $_enabled = true;
    
    /**
     * If not disabled by the action controller:
     * Browses registered components from Tight_Di
     * and injects instances from controller to all dependencies     
     */
    public function init()
    {                
        if (false === $this->_enabled) {
            return;
        }
        
        $bootstrap = $this->_actionController->getInvokeArg('bootstrap');
        $container = $bootstrap->getContainer();        
        $components = $container->getComponents();                
        
        $module = $this->_actionController->getRequest()->getModuleName();        
        $currentControllerClass = ucfirst($module) . '_' . ucfirst($this->_actionController->getRequest()->getControllerName()) . ucfirst($this->_actionController->getRequest()->getControllerKey());
        
        foreach($components as $k => $v) {            
            if ($v['class'] == $currentControllerClass) {
                $container->inject($k, $this->_actionController);
                break;
            }                    
        }    
    }        
    
    /**
     * Enable IOC
     * To be called from action controllers
     */
    public function enable()
    {        
        $this->_enabled = true;
    }
    
    /**
     * Disable IOC
     * To be called from action controllers
     */
    public function disable()
    {        
        $this->_disable = true;
    }
}

Un exemple avec un controlleur parent et un véritable controlleur

Common_Controller_AclAction

Code:

<?php
/**
 *  Example
 */
class Common_Controller_Action extends Tight_Controller_Action
{
    /**     
     * @var Zend_Translate
     */
    protected $_tr;
        
    
    public function setTr(Zend_Translate $tr)
    {
        $this->_tr = $tr;
        return $this;
    }
}

Code:

<?php
/**
 * Example
 */
class Sign_IndexController extends Common_Controller_Action
{    
    /**     
     * @var Service_User
     */
    private $_userService;
    
    /**     
     * @var Service_Mail
     */
    private $_mailService;
                

    public function setUserService(Service_User $userService)
    {
        $this->_userService = $userService;
        return $this;
    }
    
    public function setMailService(Service_Mail $mailService)
    {
        $this->_mailService = $mailService;
        return $this;
    }
    
    
    public function indexAction()
    {
        // test à la con
        $user = $this->_userService->getUserById(1);
    }
}

Service Layer

Tous les services, Dao & Co doivent implémenter une interface qui permet de définir leur activité et de donner une rigueur au développeur (mais ce n'est pas obligatoire)

Sinon le code est basique.

Interface IUser.php :

Code:

<?php 
interface Service_Generic_IUser 
{ 
    public function authenticate($login, $password);
       
    public function createUser(Model_User $user);    
        
    public function saveUser(Model_User $user);
        
    public function getUserById ($id);    
}

Service User

Code:

?php
class Service_User extends Tight_Service_Service implements Service_Generic_IUser
{  
    /**     
     * @var Model_Dao_User
     */
    private $_userDAO;
    
    /**     
     * @var Tight_Acl
     */
    private $_acl;
       
    public function setUserDAO(Model_Dao_User $userDAO)
    {
        $this->_userDAO = $userDAO;
        return $this;
    }
    
    public function setAcl(Tight_Acl $acl)
    {
        $this->_acl = $acl;
        return $this;
    }
    
    public function __construct ()
    {}
       
    /**
     * Authenticate an user
     * 
     * @param string  $login
     * @param string  $password
     * @return bool success
     */
    public function authenticate($login, $password)
    {        
        return $this->_userDAO->authenticate($login, $password);        
    }

    /**
     * Create new user     
     * 
     * @param Model_User $user
     * @return bool
     */
    public function createUser(Model_User $user)
    {
        $user->setCreated(Zend_Date::getTimestamp());
        return $this->_userDAO->create($user);
    }
    
    /**
     * Save user
     * @param Model_User $user
     * @return boolean
     */
    public function saveUser(Model_User $user)
    {
        return $this->_userDAO->save($user);       
    }

    /**
     * Find and populate User with id $id
     * 
     * @param  string $id 
     * @return Model_User
     */
    public function getUserById ($id)
    {
        return $this->_userDAO->find($id);    
    }

    // etc. etc.
}

Je suis en train de réfléchir pour intégrer la gestion de transactions (générique) au sein des services.


DAO

Idem, une interface, un dao.

Interface

Code:

<?php 
interface Model_Dao_Generic_IUser 
{ 
    public function create(Model_User $user);
    
    public function save(Model_User $user);
    
    public function authenticate($login, $password);
    
    public function find($id);
    
    public function findByLogin($login);    
}

Dao

Code:

<?php 
class Model_Dao_User extends Tight_Model_Dao implements Model_Dao_Generic_IUser
{ 
    public function create(Model_User $user)
    {
        try {
            $this->getMapper()->create($user);
            return true;
        }
        catch (Exception $e) {            
            throw new Exception($e->getMessage());
            return false;
        }        
    }
    
    public function save(Model_User $user)
    {
        try {
            $this->getMapper()->save($user);
            return true;
        }
        catch (Exception $e) {            
            throw new Exception($e->getMessage());
            return false;
        }
    }
    
    
    public function authenticate($login, $password)
    {                
        $authAdapter = new Zend_Auth_Adapter_DbTable(Zend_Db_Table::getDefaultAdapter());
        $authAdapter->setTableName('user')
                    ->setIdentityColumn('login')
                    ->setCredentialColumn('password');
        // Définit les valeurs avec lesquels authentifier l'utilisateur (ici venant d'un formulaire)
        $authAdapter->setIdentity($login)
                    ->setCredential($password)
                    ->setCredentialTreatment('MD5(?)');

        $auth = Zend_Auth::getInstance();
        $result = $auth->authenticate($authAdapter);
        
        if ($result->isValid()) {                        
            $data = $authAdapter->getResultRowObject(null, 'password');                        
            $user = $this->getMapper()->buildEntity($data, 'Model_User');

            $auth->getStorage()->write($user);

            return true;
        }
        else {
            
            return false;
        }
    }
    
    /**     
     * @return Model_User
     */
    public function find($id)
    {
        try {            
            return $this->getMapper()->find('Model_User', $id);                        
        }
        catch (Exception $e) {            
            throw new Exception($e->getMessage());
            return false;
        }
    }
    
    /**     
     * @return Model_User
     */
    public function findByLogin($login)
    {
        $select = $this->getMapper()->getDb()->select()
                       ->from('user')                       
                       ->where('login = ?', $login);                       
        
        return $user = $this->getMapper()->fetchByCriteria('Model_User', $select, true);
    }
}

La propriété et le setter pour le mapper dépendant se trouve dans Tight_Model_Dao

Code:

<?php 
abstract class Tight_Model_Dao
{
    /**
     * @var Tight_Model_Mapper
     */
    private $_mapper;
    
    /**
     * @param Tight_Model_Mapper $mapper
     * @return Tight_Model_Dao
     */
    public function setMapper(Tight_Model_Mapper $mapper)
    {
        $this->_mapper = $mapper;
        return $this;
    }
    
    /**
     * @return Tight_Model_Mapper
     */
    public function getMapper()
    {
        return $this->_mapper;
    }

}

Mapper

Donc, nous voyons que pour accéder au mapper c'est très simple :

Code:

$this->getMapper()->find('Model_User', $id);

Il suffit de passer le type d'entité que nous souhaitons récupérer et les autres paramètres.

Le mapper implémente une interface obligatoire pour les mappers :

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);
}

Ici j'ai commencé donc par implémenter un Mapper orienté BDD.

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 extends Tight_Model_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)
    {
        try { 
            $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#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;
    }
}

Il manque encore quelques tests dans le code à faire. Ou encore implémenter une bonne gestion des clés primaires composées. De toute manière il s'agit d'un exemple, les mappers vont évoluer. Libre au développeur d'en faire ce qu'il veut.

Ce mapper est donc unique pour une persistance type BDD et tous les DAO consomment ses méthodes.

Reste donc, les objets métiers, les proxy et le générateur de classes, que je vais ajouter un peu plus tard smile


A+ benjamin.

Dernière modification par Delprog (15-09-2009 13:27:54)


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

Hors ligne

 

#3 14-09-2009 22:04:57

Julien
Membre
Date d'inscription: 16-03-2007
Messages: 501

Re: [Partage/Reflexion]MVC, Services, Mapper et Persistance

Bon résumé , rien à dire (Martin Fowler et le Gang of Four (entres autres) sont passés avant nous il y a des dizaines d'années ^^)
C'est ce qui me plait dans PHP et ZF : Tu lances tes idées dans les forges, et en général ça suit (Si c'est orienté patterns bien sûr), puis tu bosses avec plein de monde pour te faire avancer toi, et les autres, du vrai opensource bien bon :-D

Bien sûr je reste convaincu (et tu le démontres bien) qu'une expérience Java reste un plus énorme pour se frotter à ZF. Java (langage que j'aime hein et que je ne bannis nullement) est un langage dans lequel les patterns et la conception excellent. Il manque des choses encore à PHP (pas beaucoup), on pousse, on pousse, mais ça avance doucement (mais ça avance :-) ).

Ah oui j'oubliais : l'architecture logicielle est un métier à part entière ;-)

Hors ligne

 

#4 15-09-2009 09:28:24

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

Re: [Partage/Reflexion]MVC, Services, Mapper et Persistance

Julien a écrit:

Bon résumé , rien à dire (Martin Fowler et le Gang of Four (entres autres) sont passés avant nous il y a des dizaines d'années ^^)

Oui ces réflexions ont déjà été faites des dizaines de fois, mais je sais pas, dans le monde du PHP j'ai l'impression qu'il y a bien moins de monde qui se pose des questions. C'est certainement du au passé de PHP avec PHP4.

Ce qui me plait à moi dans cette communauté c'est de voir des dev PHP débutants ou confirmés s'illuminer quand ils découvrent toute la puissance des patterns et ce qu'on peut en faire big_smile


A+ benjamin.

Dernière modification par Delprog (15-09-2009 09:29:23)


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

Hors ligne

 

#5 15-09-2009 10:56:55

Julien
Membre
Date d'inscription: 16-03-2007
Messages: 501

Re: [Partage/Reflexion]MVC, Services, Mapper et Persistance

Et oui, on est bien loin de PHP4 et de la "non reflexion". Aujourd'hui les architectes logiciels passent avant les dév en bureau d'étude , ca évite pas mal de catastrophes et ca permet enfin de construire une application dans le vrai sens du terme, du moins en théorie ^^

On arrive bien tard dans le domaine du génie logiciel. Tous les problèmes auxquels on fait face ont été résolu il y a bien des années ==> les patterns. C'est pas pour rien que ça s'apprend en école d'ingé ou en technicien sup ;-)

Hors ligne

 

#6 15-09-2009 13:29:29

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

Re: [Partage/Reflexion]MVC, Services, Mapper et Persistance

J'ai mis à jour les exemples de codes. Il manque encore un bout mais ça arrive smile

http://www.z-f.fr/forum/viewtopic.php?pid=21892#p21892

A+ benjamin.


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

Hors ligne

 

#7 14-01-2010 10:52:13

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

Re: [Partage/Reflexion]MVC, Services, Mapper et Persistance

J'ai toujours du mal avec la couche Service, je trouve peut d'informations précises sur Internet.

Imaginons que je veux créer une archive Zip qui contient un ensemble de documents dont les sources sont récupérés dans une base de données.

Que doit on faire :

Code:

$serviceZip = new Service_Zip(); // Ou $proxyZip = new Proxy_Zip ?
$serviceZip->createZip();

// dans la classe Service_Zip (ou Proxy_Zip ?)
public function createZip() 
{
       // Récupération des objets Documents peuplés
       $documentMapper = Mapper_Document::getInstance();
       $documents = $documentMapper->getAllDocuments();

       // Création d'un zip et propose le download
       $zip = new Domain_Zip();
       $zip->createZip($documents);

       // Envoi d'un mail pour le plaisir
       mail(...)
}

Et si on veut créer un Zip contenant tous les documents d'un dossier particulier, pourrait-on faire directement quelque chose comme ci-dessous dans le contrôleur ? sans passer par une couche service :

Code:

$pathDocuments = 'documents/';
$zip = new Domain_Zip();
$zip->createZip($pathDocuments);

C'est peut être plus judicieux de toujours utiliser un Service dans les contrôleur et ne jamais passer directement dans des objet métiers de base (Proxy, Domain etc..). Mais en regardant la définition sur le net du pattern Proxy j'ai l'impression que le Service et le Proxy c'est un peut la même chose, donc je ne comprends pas trop les différences (je dois surement confondre certaines choses et en particulier l'utilisation du Service).

Par contre d'après ce schéma http://www.dotnetguru.org/articles/Pers … rmapp1.jpg un objet métier peut utiliser la couche d'accès aux données. Bon tu me diras qu'on doit utiliser principalement le proxy pour accéder à cette couche, mais est ce vraiment le rôle d'un proxy ? car je n'en ai pas l'impression.

C'est en regardant cet exemple que je me suis un peut mélangé les pinceaux :
http://www.phplibrairies.com/tutorial_d … _7_fr.html

A+ Anthony

Dernière modification par citronbleu-v (14-01-2010 10:56:35)

Hors ligne

 

#8 19-01-2010 21:26:40

xbugzz
Nouveau membre
Date d'inscription: 19-01-2010
Messages: 2

Re: [Partage/Reflexion]MVC, Services, Mapper et Persistance

Bonjour tout le monde !

Ceci est mon premier post dans la communauté smile

Je post car je ne suis pas entièrement d'accord avec cette vision (du moins pour le PHP) mais avant d'essayer d'expliquer pourquoi, je tente de te répondre citronbleu-v :

Effectivement, le pattern Proxy n'est pas exactement cela et est plus proche du service. Dans ton cas, tu peux appeler ton service dans le controlleur comme tu l'as fait MAIS il serait plus "prudent" de disposer d'une ressource "ServiceManager" (accompagné d'un helper) qui sait faire appel au service demandé car si un jour tu veux renommer ou déplacer ton service, tu seras obliger de modifier tout tes appels...

Pour Delprog, en quoi le découpage du modèle est il un pattern? Les test se feront de la même manière sur ton "Proxy". Cette méthode à des inconvénients non négligables puisque tu ne peut plus faire d'héritage ! je m'explique :

Ton domaine contient des produits de différents types. ex : une machine expresso et un aspirateur.

Tes deux modèles ont des données propres à elles mêmes (type de cartouche et puissance d'aspiration) mais les deux ont des données communes (prix, couleur, marque, etc...) La logique voudrait que tu créer une classe Produit qui sera hérité par les deux. Le HIC, Ta classe Produit possèdera  son entité mais ton aspi devrait aussi avoir la sienne... + les méthodes de lazy loading du Produit.. donc :

Produit extends ProduitEntity
Aspi     extends AspiEntity, ProduitEntity (ah bah non pas d'héritage multiple en PHP)

Je suis peut être à coté de la plaque mais cette structure n'est pas adaptée en PHP...

Hors ligne

 

#9 20-01-2010 09:30:54

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

Re: [Partage/Reflexion]MVC, Services, Mapper et Persistance

Salut,

Code:

Effectivement, le pattern Proxy n'est pas exactement cela et est plus proche du service. Dans ton cas, tu peux appeler ton service dans le controlleur comme tu l'as fait MAIS il serait plus "prudent" de disposer d'une ressource "ServiceManager" (accompagné d'un helper) qui sait faire appel au service demandé car si un jour tu veux renommer ou déplacer ton service, tu seras obliger de modifier tout tes appels...

Attention, dans ce cas le proxy n'est pas proche d'un service. Comme je l'expliquais je crois dans un autre post, le proxy n'est pas fait pour faire du métier, c'est simplement le pattern qui a été choisi pour rendre "magique" la persistance, et c'est tout, ça ne va pas plus loin. Pour ce qui est des dépendances, l'injection de dépendances me suffit. J'ai aussi décidé de mixer un peu les pattern façade et service layer dans le sens où mes services ne portent que des cas d'utilisations pour une ressource donnée. De fait, les controlleurs ne consomment la plupart du temps qu'un seul service. Si le service est renommé, ce n'est pas grand chose à modifier smile

Effectivement PHP montre des limitations. D'abord en objet, comme tu le fais remarquer, mais aussi du fait que ce soit un langage non compilé. En toute logique, avec cette approche, le développeur ne devrait pas lui même manipuler les proxy, ils devraient se substituer automatiquement aux objets métiers lors du runtime. Or, en php c'est difficile de mettre en place un tel fonctionnement et d'obtenir de bonnes performances.

Je ne pense pas avoir dit que le découpage du modèle est un pattern. J'expose simplement des patterns qui peuvent être utilisés pour ajouter un peu de magie dans le modèle. L'idée est de faire réfléchir et ton post est le bienvenue smile

Depuis, j'ai fait murir mon raisonnement. Par exemple, je n'utilise plus le pattern DAO mais Repository. Mes interfaces de repository dépendent du métier alors que leurs implémentations dépendent de la couche infrastructure. Mes services sont en quelque sorte un mix des patterns façade et service layer comme je le dis plus haut, etc.

Pour ce qui est de l'héritage, j'ai fait remarqué sur un autre sujet que j'avais mal implémenté le pattern proxy. Les objets métiers doivent implémenter une interface, et les proxy la même interface. Le proxy doit ensuite contenir une instance du "proxied object". Du coup, le proxy n'est qu'une extension d'un objet et permet d'ajouter des fonctionnalités sans rien toucher à l'objet d'origine qui lui étend toujours la classe voulue.
Pour le multi-héritage, c'est effectivement gênant d'un point de vue objet, mais pour la persistance ça ne devrait logiquement pas poser de problème étant donné que chaque objet possèdera un proxy.


A+ benjamin.


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

Hors ligne

 

#10 20-01-2010 10:07:54

xbugzz
Nouveau membre
Date d'inscription: 19-01-2010
Messages: 2

Re: [Partage/Reflexion]MVC, Services, Mapper et Persistance

Salut,

Je me suis relu et désolé d'avoir été un peu "pet-sec" smile Effectivement, cette fois ci je suis d'accord avec toi.

Hors ligne

 

#11 20-01-2010 11:30:28

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

Re: [Partage/Reflexion]MVC, Services, Mapper et Persistance

Delprog a écrit:

mais aussi du fait que ce soit un langage non compilé.

faux !


----
Gruiiik !

Hors ligne

 

#12 20-01-2010 12:00:32

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

Re: [Partage/Reflexion]MVC, Services, Mapper et Persistance

Abus de langage, PHP est un langage qui repose sur un interpréteur qui est lui même compilé pour s'adresser au processeur. On peut dire que le langage est compilé dans le sens où à chaque exécution il est "re-compilé". Je disais ça par opposition aux langages compilés une seule fois.

Maintenant, au lieu de pointer du doigt avec un simple "faux" tu peux aussi donner une explication constructive, sinon ton post est un peu useless.


A+ benjamin.

Dernière modification par Delprog (20-01-2010 12:01:26)


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

Hors ligne

 

#13 20-01-2010 14:56:16

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

Re: [Partage/Reflexion]MVC, Services, Mapper et Persistance

Delprog a écrit:

Abus de langage, PHP est un langage qui repose sur un interpréteur qui est lui même compilé pour s'adresser au processeur. On peut dire que le langage est compilé dans le sens où à chaque exécution il est "re-compilé". Je disais ça par opposition aux langages compilés une seule fois.

Maintenant, au lieu de pointer du doigt avec un simple "faux" tu peux aussi donner une explication constructive, sinon ton post est un peu useless.


A+ benjamin.

En quoi je dois donné une réponse constructive ?? PHP est un langage compilé, pas besoin de faire une thèse.

Tu renvois tes problèmatiques sur des soit disant défaut du langage (en parlant d'héritage multiple). Si le langage ne te convient pas, change ! Y'a assez de langages (au pif: python) pour te satisfaire.

EDIT :
D'ailleurs, PHP6 embarquera un cache, donc, ne recompilera pas le code à chaque fois (et d'ailleurs, qui de nos jours n'utilise pas un cache ??)

Dernière modification par nORKy (20-01-2010 14:57:42)


----
Gruiiik !

Hors ligne

 

#14 20-01-2010 15:10:31

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

Re: [Partage/Reflexion]MVC, Services, Mapper et Persistance

nORKy a écrit:

Tu renvois tes problèmatiques sur des soit disant défaut du langage (en parlant d'héritage multiple). Si le langage ne te convient pas, change ! Y'a assez de langages (au pif: python) pour te satisfaire.

Je vois pas à quel moment j'ai "renvoyé mes problématiques sur des soit disant défaut du langage". Je dis que l'héritage multiple manque c'est vrai, mais je dis justement que c'est pas un problème par rapport au sujet traité ici.

D'ailleurs je n'ai même pas à me justifier, j'essaie d'apporter ce qui manque à php et faire partager mon expérience. Je ne sais pas ce que tu cherches à faire, ici on parle bien de PHP, et j'ai aussi des scripts en Python et des projets dans d'autres langages, aucun rapport.
Une chose est sûre, me lâcher une remarque comme "Si le langage ne te convient pas, change" alors que je ne fais que constater un manque dans le modèle Objet de PHP (sans me plaindre qui plus est) n'est pas du tout constructif. Si le sujet ne t'intéresse pas pour autre chose que sauter à la gorge des gens avec des énormités pareilles, tu peux passer ton chemin. Et d'après ce que j'ai vu sur le forum, tu aides beaucoup de personnes certes, mais tu n'as pas forcément toujours la bonne manière de t'exprimer, je ne suis pas le seul à l'avoir remarqué.

Et comme tu ne vas pas pouvoir t'empêcher de répondre encore à ça et qu'on s'en sortira plus, le troll s'arrête ici pour moi, ça ne m'intéresse pas.


A+ benjamin.

Dernière modification par Delprog (20-01-2010 15:11:03)


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

Hors ligne

 

#15 20-01-2010 15:31:38

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

Re: [Partage/Reflexion]MVC, Services, Mapper et Persistance

Bon, vous êtes 2 habitués du forum (et à force vous devez connaître un peu les caractères de chacun smile )... je dis +1 pour la remarque "le troll s'arrête ici pour moi, ça ne m'intéresse pas"... Un peu de diplomatie SVP !

A+, Philippe


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

Hors ligne

 

#16 30-01-2010 15:10:21

bakura
Administrateur
Date d'inscription: 30-01-2010
Messages: 353

Re: [Partage/Reflexion]MVC, Services, Mapper et Persistance

Bonjour,

C'est mon premier message sur ce forum, que je trouve relativement intéressant smile. Je viens du monde C++ et je me suis mis que très récemment au PHP et à ZF. J'ai eu quelques soucis de compréhension au départ (notamment sur la persistance des données, c'est très différent du type d'application que j'écris en C++...).

Je me permets de prendre part à ce débat, en espérant ne pas dire une grosse bêtise :

Si la persistance n'est plus une base de données, il faudra écrire un mapper dédié au système et refactoriser uniquement les DAO. Les controlleurs et les services restent intactes.

Pourquoi ne pas avoir pour ceci un adapter (comme pour Zend_Auth ?). A savoir, avoir une classe Mapper abstraite avec une fonction pour définir un adapter (base de données ou n'importe quoi d'autre). Les mappers étendraient cette classe et définiraient l'adapter qui convient pour récupérer les données. Ainsi le mapper ne serait plus bloqué uniquement à une base de données.

Pour mon cas je suis en train aussi de me renseigner sur ce domaine, après avoir lu plusieurs messages ci-et là sur les blogs. Mon application étant relativement simple, j'ai décidé de me diriger vers la solution suivante :

- une classe abstraite App_Model_MapperAbstract avec des méthodes create, save et update, ainsi que des méthodes _create, _save (accepte un Model_EntityAbstract), et _update (même chose) afin d'avoir ce comportement : http://blog.tekerson.com/2008/12/17/dat … rn-in-php/

- des mappers qui héritent de cette classe (UserMapper, UserProfileMapper...), qui implémentent donc les méthodes _create, _save et _update et d'autres fonctions spécifiques (findByUsername, findById...).

- des tables qui héritent de Zend_Db_Table_Abstract (en fait, comme dans le quickstart) et qui implémentent la logique des différentes fonctions des mappers (findByUsername, findById...).

- une classe abstraite App_Model_EntityAbstract (qui définie une fonction populate qui peut prendre un Zend_Db_Table_Row_Abstract ou un tableau tout bête), fonctions __set et __get, de laquelle dérivera tous mes modèles (User, UserProfile...), qui pourront donc ajouter des fonctionnalités spécifiques.


Cette solution me paraît simple à mettre en oeuvre et relativement propre, notamment pour le site que je développe ou j'ai quasimment que des mapping one-to-one avec ma représentation en base de données. Y voyez-vous certains inconvénients ?

J'ai quand même une question vis-à-vis de ce design. Typiquement, mon App_Model_UserEntity disposera d'une fonction getProfile qui permettra de récupérer un App_Model_UserProfileEntity. Je pensais faire en sorte que tout chargement d'un utilisateur entraine le chargement de son profil mais visiblement, le premier message indique que ce n'est pas top niveau performance en PHP à cause du rechargement à chaque page.

Dans ce modèle là, la fonction getProfile va donc créer un mapper UserProfile et le récupérer à la demande ? Ou il n'est pas logique de créer un mapper dans un objet métier ?

D'autre part, j'avoue ne pas très bien comprendre le principe des Services. Dans mon cas, je ne comprends pas spécialement en quoi cela peut me servir et simplifier mon application, si ce n'est à complexifier l'architecture.

Cet article d'un blog : http://www.litfuel.net/plush/?postid=186 décrit un service UserAuth. En clair, un service serait quelque chose qui effectuerait des traitements avec un objet "entité" (mes App_Model_EntityAbstract) ? Qu'est-ce qui, d'un point de vue "logique", empêche d'intégrer ce type de service directement dans l'entité (avoir donc une fonction authenticate à l'intérieur du App_Model_UserEntity ?).

Désolé pour toutes ces questions peut-être naïves, j'ai beaucoup de mal à bien designer mes applications, cette persistance de données me pose beaucoup de soucis...

EDIT : vous vous battez sur l'héritage multiple... pour l'avoir utilisé quand je débutais en C++, c'est clairement quelque chose que j'essaye d'éviter à tout prix maintenant tant c'est une galère à maintenir et à "suivre" comme code...

Dernière modification par bakura (30-01-2010 15:11:47)

Hors ligne

 

#17 01-02-2010 14:43:56

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

Re: [Partage/Reflexion]MVC, Services, Mapper et Persistance

Pas en relation directe avec le sujet (mais un petit peu), je suis tombé sur un blog très interessant avec plusieurs posts sur différents PHP pattenrs (facade, composite, decorator, adapter, singleton, bridge, ...)

http://giorgiosironi.blogspot.com/


----
Gruiiik !

Hors ligne

 

#18 01-02-2010 15:50:50

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

Re: [Partage/Reflexion]MVC, Services, Mapper et Persistance

Salut,

bakura a écrit:

Pourquoi ne pas avoir pour ceci un adapter (comme pour Zend_Auth ?). A savoir, avoir une classe Mapper abstraite avec une fonction pour définir un adapter (base de données ou n'importe quoi d'autre). Les mappers étendraient cette classe et définiraient l'adapter qui convient pour récupérer les données. Ainsi le mapper ne serait plus bloqué uniquement à une base de données.

Oui, c'est une possibilité. Il ne faut surtout pas confondre DAO et Mapper. Le DAO est un objet qui permet d'interfacer l'accès à la persistance. Le Mapper quand à lui, comme son nom l'indique, sert à mapper tes objets par rapport à la persistance. Tu pourrais avoir par ex. un objet métier dont les données sont issues de plusieurs tables dans une base de données. En supposant que tu utilises Zend_Db, au moment de peupler ton objet, ton Mapper devra donc faire appel à toutes les Zend_Db_Table nécessaires. Son rôle est bien différent d'un DAO. Mais dans la pratique, tu pourrais très bien te passer de l'un des deux selon la dimension de ton application.

Ce que je fais aujourd'hui, c'est que je définis des interfaces de Repository dans ma couche métier, avec mes objets métiers. Par ex. je vais avoir une interface UserRepository qui va déterminer le contrat pour accéder aux données de l'user. Ensuite je fais ce que je veux dans l'implémentation à condition de respecter le contrat. Dans mon appli, je vais attendre un objet du type de l'interface, et injecter l'implémentation que je veux (polymorphisme).

En étant puriste, le mapper ne devrait que mapper (pas de fonctionnel dedans). Mais en pratique, ça peut valoir la peine d'implémenter les méthodes find* dans les mappers. Si c'est la solution qui te convient à tous les niveaux, alors c'est la bonne solution smile


Le Service peut avoir différents rôles. Son but principal est de concentrer la logique de ton application, typiquement une API, et de pouvoir ensuite la distribuer facilement (via des webservices par ex.).

Exemple : tu veux proposer une api qui permet de créer des albums photos et qui sera consommée par d'autres "front" que le tiens.
Tu concentres donc ta logique dans des services qui permettront les opérations (CRUD souvent) sur ton modèle métier : créer un album, ajouter une photo à un album, modifier, supprimer, etc.

Dans ce cas, pour des raisons de sécurités, ce sera à tes services de valider et filtrer les données et de vérifiers les droits (acls).

C'est en aucun cas une couche obligatoire. Si tu sais dès le départ que tu ne devras jamais avoir une API distribuée, alors ils peuvent te sembler totalement inutiles et c'est de toute façon pas la peine de les implémenter.

Par contre, je te conseille quand même d'extraire en partie la logique de l'application de tes controlleurs. C'est bien plus lisible est bien plus facile à maintenir. Dans ce cas là, si tu n'as pas besoin de distribuer ton API, tu peux valider/filtrer et vérifier les Acls dans tes controlleurs, puis passer le relais pour les opérations dans le modèle à une façade par ex. (une télécommande universelle permettant de déclencher les opérations dans ton modèle/persistance). En fait c 'est souvent une simple classe fonctionnelle qui propose une méthode par cas d'utilisation (au sens UML du terme). De cette manière tu organises instinctivement la logique de ton application et ce sera facile de s'y retrouver.


A+ benjamin.

Dernière modification par Delprog (01-02-2010 18:37:12)


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

Hors ligne

 

#19 01-02-2010 17:47:32

bakura
Administrateur
Date d'inscription: 30-01-2010
Messages: 353

Re: [Partage/Reflexion]MVC, Services, Mapper et Persistance

Merci Delprog pour ces précisions, j'y vois un peu plus clair wink.

Hors ligne

 

#20 02-02-2010 13:53:51

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

Re: [Partage/Reflexion]MVC, Services, Mapper et Persistance

J'ai lu par endroit, que la classe pouvait, par exemple donnée un formulaire.

Comment gérer le fait qu'un formulaire peut être lié à 2 services ?

Par exemple, 2 services, un User, et un Group (je dis bien service, pas de model/entité). 2 services car j'ai des fois des opérations complexes à réalisé pour l'un ou l'autre.
Dans, le service User, je créé une fonction pour retourné une formulaire de création d'utilisateur. Celui-ci possède un champ avec la liste des groupes (accès au service Group ? ou direct via le model ?) et un champ pour la création d'un groupe ci-celui il n'existe pas dans la liste. Le but étant de gérer la création et l'insertion dans un group pour un utilisateur.

Comment s'imbrique les services ? J'aurais tendance à dire que le service User qui va gérer le formulaire, va faire appel au service Group, mais, qu'est ce qui fait la relation entre les 2 ? (transmission du formulaire ? Acl ? ...)


----
Gruiiik !

Hors ligne

 

#21 02-02-2010 15:24:21

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

Re: [Partage/Reflexion]MVC, Services, Mapper et Persistance

Tu veux dire que tes services renvoient un formulaire Zend_Form ? Si c'est le cas je ne sais pas te répondre car mes services ne fonctionnent pas comme ça. Ils ne reçoivent et ne renvoient que des objets métiers (dans le cas d'appli. simples), ou des DTO.

Dans le cas de la création d'un utilisateur, une seule méthode qui répond au cas d'utilisation reçoit un DTO_Account (par ex.), contrôle les droits. Le valide grâce à une extension de Zend_Form qui ne me sert que de validateur, transforme le DTO en objet métier, demande à la persistance de faire son boulot, met à jour le DTO (l'id par ex.). J'ai utilisé le DTO car c'est le seul moyen que j'ai trouvé pour pouvoir balancer les erreurs de validation en gardant un objet léger, j'ai des notifications au sein même du DTO.

Code:

$monDTO->getNotification()->hasErrors();
$monDTO->getNotification()->getErrors();
$monDTO->getNotification()->addError();
$monDTO->getNotification()->setErrors();

J'ai utilisé le pattern Notification de Fowler.

La construction du formulaire dépend du front et non pas de l'API.

Au départ j'avais pensé utiliser Zend_Form, et recevoir un formulaire en paramètre, mais ça ne m'a pas semblé logique. J'ai préféré utiliser des objets simples (les DTO donc) qui n'ont que des propriétés, des accesseurs, et des méthodes de serialization (toJson, toArray, toXml). Mais si le métier est très simple, et que les objets métiers ne sont que des sacs à getters et setters générés par l'ORM, pas besoin des DTO, on peut utiliser directement les entités.

Pour ce qui est du front, je vais simplement récupérer la liste des groupes via une méthode de service pour pouvoir construire mon formulaire. D'ailleurs, plus besoin de Zend_Form et des décorateurs pour ça, un simple formulaire HTML à la mano, le controlleur derrière construit un DTO avec les données en POST et invoque le service, il n'a plus rien d'autre à faire, sauf validations qui pourraient dépendre du front (par ex. un champ "Répeter le mot de passe").

Je te donne un de code pour exemple parce qu'on a une vision surement différente et c'est pas facile d'expliquer smile

Code:

class AccountsController extends App_Controller_Action
{
    /** @var Service_Account */
    private $_accountService;

    public function newAction()
    {
        $captcha = $this->_generateCaptcha();        
        $this->view->captcha = $captcha;
        $this->view->account = new DTO_Account();
        $this->view->userGroups = $this->_accountService->getUserGroups();

        $this->view->action = array(
            'method' => 'post',
            'url'    => $this->view->url(array(
                'controller' => 'accounts',
                'action' => 'post'
            ), 'default')  
        );
        
        $this->renderScript('accounts/account-form.phtml');
    }

    public function postAction()
    {
        if (!$this->_request->isPost()) {
            throw new Zend_Controller_Action_Exception('Bad request method', 405);            
        }
        $account = $this->_getAccountFromPost();
        $this->_accountService->createAccount($account);        
        if ($account->getNotification()->hasErrors()) {
            // traitement si erreurs de validation

        }
        $this->view->account = $account;
    }

    public function editAction()
    {
        $accountId = $this->_request->getParam('aid', null);
        if (is_null($aid)) {
            throw new InvalidArgumentException('Missing account ID', 400);
        }
        
        $account = $this->_accountService->getAccount($aid);
        if (null === $account) {
            throw new Zend_Controller_Action_Exception('Account not found', 404);
        }

        $this->view->account = $account;
        $this->view->userGroups = $this->_accountService->getUserGroups();
        $this->view->action = array(
            'method' => 'put',
            'url'    => $this->view->url(array(
                'controller' => 'accounts',
                'action' => 'put'
            ), 'default')  
        );
        
        $this->renderScript('accounts/account-form.phtml');
    }

    public function putAction()
    {
         // même principe
    }

    /**
     * @return DTO_Account
     */
    private function _getAccountFromPost()
    {
         $account = new DTO_Account();
         $account->login = $this->_request->getParam('login', null);
         $account->password = $this->_request->getParam('password', null);
         $account->groupId = $this->_request->getParam('group', null);

         return $account;
    }

    public function setAccountService(Service_Account $accountService)
    {
        $this->_accountService = $accountService;
        return $this;
    }
}

Code:

class Service_Impl_Account implements Service_Account
{
    /** @var Repository_User */
    private $_userRepository;
    
    /** @var App_Auth */
    private $_auth;

    /** @var Service_Validator_Account */
    private $_accountValidator;
    
    /** @var Assembler_Account */
    private $_accountAssembler;

    /** @Transactional */
    public function createAccount(DTO_Account $accountDTO)
    {               
        // TODO: Transactional
        if (!$this->getAuth()->isAllowed($this->getAuth()->getCurrentUser(), 'account', 'create')) {
            throw new Service_Exception_Unauthorized('Unauthorized', 401);
        }
        
        // le validateur étend un validateur abstrait qui lui même étant Zend_Form
        // le validateur account ne fait que définir la méthode validate() qui contient la liste des validateurs.
        // la méthode run déclenche la validation ($this->isValid()), ajoute les erreurs au DTO,
        // et écrase les données du DTO avec les données filtrées.
        $this->_accountValidator->setData($accountDTO);
        $this->_accountValidator->run();
        if ($accountDTO->getNotification()->hasErrors()) {
            return false;
        }                 
         
        $user  = $this->_accountAssembler->dtoToDomainEntity($accountDTO);               
        $this->_userRepository->save($user);                  
        $accountDTO->setId($user->id);
        $accountDTO->setActivationCode(sha1($user->login . $user->email . 'unechainepourlehash'));
        
        return true;
    }

    public function setUserRepository(Repository_User $userRepository)
    {
        $this->_userRepository = $userRepository;
        return $this;
    }
    
    public function setAccountValidator(Service_Validator_Account $accountValidator)
    {
        $this->_accountValidator = $accountValidator;
        return $this;
    }

    public function setAccountAssembler(Assembler_Account $accountAssembler)
    {
        $this->_accountAssembler = $accountAssembler;
        return $this;        
    }
    
    public function setAuth(App_Auth $auth)
    {
        $this->_auth = $auth;
        return $this;
    }
}

Bon il manque des trucs, c'est pour l'exemple. Il faudrait ajouter la méthode getUserGroups() au service par ex.

Désolé de ne pas répondre exactement à ta question, mais ça pourra peut-être te donner 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

 

#22 02-02-2010 16:54:08

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

Re: [Partage/Reflexion]MVC, Services, Mapper et Persistance

Beh, ce que tu dis sur la notification c'est intéressant. Mais par contre, ce que toi tu en fais, je ne fais pas comme ca.
Je suis plutôt du genre

Code:

$ret = $service->action();
if ($ret === false) {
$service->getMessages();
}

Mais, la notification peut être intéressant pour la communication "inter-service"


----
Gruiiik !

Hors ligne

 

#23 02-02-2010 17:08:11

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

Re: [Partage/Reflexion]MVC, Services, Mapper et Persistance

Oui c'est aussi une solution.
En fait j'ai intégré les notifications sur les objets pour qu'en remote access je n'ai qu'un seul objet à envoyer et qui contient ses propres messages (erreurs ou autre), sinon il faudrait que j'encapsule le DTO (les données donc) + les messages dans un autre objet que je retourne au consommateur du service.

Mais ce serait aussi une idée, sans DTO, d'avoir un objet serializable de réponse (qui servirait dans toutes les réponses) et qui contiendrait un attribut data (une entité sérializable, plus besoin de DTO) et un attribut messages.

De mon côté j'ai besoin des DTO pour d'autres raisons.

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 02-02-2010 17:22:03

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

Re: [Partage/Reflexion]MVC, Services, Mapper et Persistance

Éventuellement, avec "Observer/Observable"

Dernière modification par nORKy (02-02-2010 17:28:41)


----
Gruiiik !

Hors ligne

 

#25 09-02-2010 21:31:30

bakura
Administrateur
Date d'inscription: 30-01-2010
Messages: 353

Re: [Partage/Reflexion]MVC, Services, Mapper et Persistance

Bonjour smile

Je me permet de réagir de nouveau sur ce domaine qui m'intéresse toujours autant, étant par nature très porté sur tout ce qui est architecture... mais là il y a quand même quelque chose qui me chagrine quand à cette approche, ou alors j'ai mal pigé quelque chose.

J'utilise le système mapper/entité dont j'ai parlé plus haut, avec un mapper qui fait le lien entre une classe qui hérite de Zend_Db_Table_Abstract, et une "entité" (l'objet métier) qui est créé à partir de ce mapper.

J'aime beaucoup ce système, extrêmement élégant, toutefois j'ai trouvé un petit soucis : imaginons que nous ayons donc un UserMapper, une classe UsersTable qui hérite de Zend_Db_Table_Abstract et une classe UserEntity qui hérite de rien (c'est généralement l'approche que j'ai retrouvé le plus souvent parmi les "gurus" de Zend qui se refusent à faire hériter l'objet métier de Zend_Db_Table_Row_Abstract).

Disons que je souhaite mettre à jour le mot de passe, j'aurais donc quelque chose comme ça :

Code:

$userMapper = new Model_UserMapper ();
$user = $userMapper->getByUsername ('toto');
$user->setPassword ('passTresComplique');

$userMapper->save ($user);

Le principal problème avec cette approche c'est... qu'on a deux requêtes vachement redondantes sur la base de données... Dans mon implémentation, le getByUsername du mapper va effectuer une requête pour récupérer l'utilisateur via son nom, puis le save du mapper va de nouveau récupérer cet utilisateur (dans mon cas, le save récupère l'entrée de la base, en l'occurence un Zend_Db_Table_Row_Abstract, le rempli et le sauvegarde).

Alors évidemment, ce sont des recherches relativement rapides mon nom utilisateur étant un index, mais quand même, ça me gêne énormément ça... Je suis pour perdre un peu en vitesse lorsque cela permet de rendre le programme plus évolutif et plus souple, mais là c'est vraiment du temps perdu pour absolument rien... et je me pose de plus en plus la question : pourquoi les gens tendent à ne pas faire hériter de Zend_Db_Table_Row_Abstract... quels sont les vrais soucis de cette approche, qui me paraît finalement plus judicieuse... je récupère une fois, je règle les valeurs et je met directement à jour !

Ou alors j'ai loupé un épisode...

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