Zend FR

Consultez la FAQ sur le ZF avant de poster une question

Vous n'êtes pas identifié.

#1 23-12-2010 13:47:25

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

Architecture des modèles

Je précise que j'ai déjà mis ce sujet sur Dvlp.com, je me permet de le mettre aussi ici pour obtenir des avis.

Je suis en train de créer la base pour un projet, et l'élément qui m'a toujours posé un problème de compréhension est le modèle. J'ai lu énormément de tutoriaux, d'explications... sur les différentes méthodes, des méthodes les plus simples aux plus évoluées à base de services (notamment décrites par Matthew O'Phinney).

Concernant ces dernières, j'ai beau eu relire de nombreuses fois les codes, je trouve cette méthode particulièrement complexe par rapport à la méthode des mappers, pour finalement peu d'avantages, en tout cas dans une utilisation basique.

J'ai donc tenté, en m'inspirant de plusieurs articles de blogs, le quickstart et des forums, de faire un petit système avec les modèles. Je souhaiterais donc avoir vos avis avisés (il y a plusieurs parties du code dont je suis pas entièrement content).

Pour ce système, j'avais ces impératifs en tête :

[LIST]
[*]Pouvoir implémenter des relations entre les modèles et modéliser aussi bien des relations 1-1 (un modèle User qui a une relation vers un modèle UserProfile) et des relations 1-n (un modèle Article qui a une relation vers plusieurs modèles Comments). Dans tous les cas, il fallait qu'il y ait un lazy loading. En gros, ne pas charger le profil d'un utilisateur tant que je n'en ait pas besoin. Pour les relations 1-n, passer par Zend_Paginator par récupérer qu'une partie des résultats. Le système est suffisamment flexible pour faire en sorte de forcer le chargement sans lazy-loading si souhaité.
[*]Avoir à écrire le moins de code une fois tout ce système écrit. Typiquement, je me suis rendu compte que mes variables étaient par exemple écrites en camelCase en PHP mais séparé par un underscore en MySQL et par un tiret en HTML. Ce qui fait que je réécrivais souvent des fonctions ou je convertissais un nom PHP en un nom MySQL. Ici, j'ai préféré m'imposer à garder les mêmes noms (du camelCase partout), et ainsi automatiser ces opérations.
[/LIST]

Voici les différentes classes de mon système. Si vous avez besoin d'explication complémentaires n'hésitez pas :

Model_ModelAbstract :

Code:

<?php

    abstract class Application_Model_ModelAbstract
    {
        protected $_id;
        
        
        //
        // Constructeur
        //
        public function __construct($data = null)
        {
            $this->_init();
            
            if ($data !== null)
                $this->populate($data);
        }
        
        //
        // Définit une valeur du modèle
        //
        public function __set($key, $value)
        {
            $method = 'set' . ucfirst($key);
            
            if (method_exists($this, $method) === false)
                throw new Zend_Exception('Invalid property');
            
            $this->$method($value);
        }
        
        //
        // Récupère une valeur du modèle
        //
        public function __get($key)
        {
            $method = 'get' . ucfirst($key);
            
            if (method_exists($this, $method) === false)
                throw new Zend_Exception('Invalid property');
            
            return $this->$method();
        }
        
        //
        // Renvoit le nom de la classe
        //
        public function __toString()
        {
            return get_class($this);
        }
        
        //
        // Définit les données pour les propriétés du modèle
        //
        public function populate(array $data)
        {
            foreach($data as $key => $value)
                $this->$key = $value;
            
            return $this;
        }
        
        //
        // Récupère les données pour les propriétés du modèle sous forme d'un tableau
        //
        public function toArray()
        {
            $data = array();
            
            foreach($this as $key => $value)
            {
                // Les valeurs d'un modèle de type Application_Model_Relation sont chargées en "lazy-loading" à partir d'autres
                // modèles, on ne doit donc pas les enregistrer dans la base
                if (!($value instanceof Application_Model_Relation))
                    $data[substr($key, 1)] = $value;
            }
                
            return $data;
        }
        
        //
        // Définit l'identifiant du modèle (généralement généré automatiquement)
        //
        public function setId($id)
        {
            $this->_id = (int)$id;
            
            return $this;
        }
        
        //
        // Récupère l'identifiant du modèle
        //
        public function getId()
        {
            return $this->_id;
        }
        
        //
        // Initialise tout un tas de choses intéressantes
        //
        protected function _init()
        {
        }
    }

Model_Mapper_MapperAbstract :

Code:

<?php

    abstract class Application_Model_Mapper_MapperAbstract
    {
        protected $_class = '';

        protected $_dbTable;
        
        
        //
        // Constructeur
        //
        public function __construct(Zend_Db_Table_Abstract $table)
        {
            $this->setDbTable($table);
                                    
            $this->_init();
        }
        
        //
        // Définit la table dans laquelle récupérer les données
        //
        public function setDbTable(Zend_Db_Table_Abstract $table)
        {
            if ($table === null)
                throw new Zend_Exception('An invalid table was provided');
                
            $this->_dbTable = $table;
        }
        
        //
        // Récupère la table dans laquelle récupérer les données
        //
        public function getDbTable()
        {
            return $this->_dbTable;
        }
        
        //
        // Récupère un enregistrement par la clé primaire
        //
        public function getById($key)
        {
            $result = $this->_dbTable->find($key);
            
            if (count($result) == 0)
                return null;
            else
                return new $this->_class($result->current()->toArray());
        }
        
        //
        // Enregistre ou met à jour une entrée de la table
        //
        public function save(Application_Model_ModelAbstract $model)
        {
            if ($model->getId() === null)
            {
                $this->_preInsert();                
                $this->_dbTable->insert($model->toArray());                
                $this->_postInsert();
            }
            else
            {
                $this->_preUpdate();
                $this->_dbTable->update($model->toArray(), array('id = ?' => $model->getId()));
                $this->_postUpdate();
            }
        }
        
        //
        // Supprime une entrée de la table
        //
        public function delete(Application_Model_ModelAbstract $model)
        {
            if ($model->getId() === null)
                throw new Zend_Exception('Cannot delete an instance that does not exist');
            
            $this->_preDelete();
            $this->_dbTable->delete(array('id = ?' => $model->getId()));
            $this->_postUpdate();
        }
        
        //
        // Initialise tout un tas de choses intéressantes
        //
        protected function _init()
        {
        }
        
        //
        // Effectue des opérations avant l'enregistrement d'une entrée dans la table
        //
        protected function _preInsert()
        {
        }
        
        //
        // Effectue des opérations après l'enregistrement d'une entrée dans la table
        //
        protected function _postInsert()
        {
        }
        
        //
        // Effectue des opérations avant la mise à jour d'une entrée dans la table
        //
        protected function _preUpdate()
        {
        }
        
        //
        // Effectue des opérations avant la mise à jour d'une entrée dans la table
        //
        protected function _postUpdate()
        {
        }
        
        //
        // Effectue des opérations avant suppression d'une entrée dans la table
        //
        protected function _preDelete()
        {
        }
        
        //
        // Effectue des opérations après la suppression d'une entrée dans la table
        //
        protected function _postDelete()
        {
        }
    }

Model_Relation :

Code:

<?php

    class Application_Model_Relation implements IteratorAggregate
    {
        protected $_mapper;
        
        protected $_method;
        
        protected $_arguments;
        
        protected $_object = null;
        
        
        //
        // Constructeur
        //
        public function __construct(Application_Model_Mapper_MapperAbstract $mapper, $method, array $arguments = array())
        {
            $this->_mapper = $mapper;
            $this->_method = $method;
            $this->_arguments = $this->setArguments($arguments);
        }
        
        //
        // Définit les arguments à passer à la fonction puis appelle la fonction de rappel
        //
        public function setArguments(array $arguments)
        {
            if (!empty($arguments))
            {
                $this->_arguments = $arguments;
                $this->_object = call_user_func_array(array($this->_mapper, $this->_method), $this->_arguments);
            }
        }
        
        //
        // Définit une valeur du modèle
        //
        public function __set($key, $value)
        {
            if (!($this->_object instanceof Application_Model_ModelAbstract))
                throw new Zend_Exception('__set function cannot be called on collections');
                
            $this->_object->$key = $value;
        }
        
        //
        // Récupère une valeur du modèle
        //
        public function __get($key)
        {
            if (!($this->_object instanceof Application_Model_ModelAbstract))
                throw new Zend_Exception('__get function cannot be called on collections');

            return $this->_object->$key;
        }
        
        //
        // Appelle une fonction du modèle
        //
        public function __call($name, array $arguments)
        {            
            if (!method_exists(get_class($this->_object), $name))
                throw new Zend_Exception("Method $name does not exist for the object " . get_class($this->_object));
                
            return call_user_func_array(array($this->_object, $name), $arguments);            
        }
        
        //
        // Implémente l'interface IteratorAggregate
        //
        public function getIterator()
        {
            return $this->_object;
        }
    }

Model_Collection :

Code:

<?php

    class Application_Model_Collection implements Countable,
                                                  ArrayAccess,
                                                  IteratorAggregate
    {
        protected $_class = '';
        
        protected $_paginator = null;
        
        protected $_entities = array();
        
        
        //
        // Constructeur
        //
        public function __construct($class, $select = null)
        {
            $this->_class = $class;
            
            if ($select !== null)
                $this->_paginator = new Zend_Paginator_Adapter_DbTableSelect($select);
        }
        
        //
        // Récupère les modèles pour l'offset de la page courante
        //
        public function getItems($offset, $itemCountPerPage)
        {
            $result = $this->_paginator->getItems($offset, $itemCountPerPage);
            
            foreach($result as $row)
                $this->_entities[] = new $this->_class($row->toArray());
        }
        
        //
        // Implémente l'interface Countable
        //
        public function count()
        {
            return count($this->_entities);
        }
        
        //
        // Implémente l'interface ArrayAccess
        //
        public function offsetExists($offset)
        {
            return array_key_exists($offset, $this->_entities);
        }
        
        //
        // Implémente l'interface ArrayAccess
        //
        public function offsetGet($offset)
        {
            return $this->_entities[$offset];
        }
        
        //
        // Implémente l'interface ArrayAccess
        //
        public function offsetSet($offset, $value)
        {
            if (!($value instanceof $this->_class))
                throw new Zend_Exception("$value must be of type $this->_class");
            elseif ($offset === null)
                $this->_entities[] = $value;
            else
                $this->_entities[$offset] = $value;
        }

        //
        // Implémente l'interface ArrayAccess
        //
        public function offsetUnset($offset)
        {
            unset($this->_entities[$offset]);
        }
        
        //
        // Implémente l'interface IteratorAggregate
        //
        public function getIterator()
        {
            return new ArrayIterator($this->_entities);
        }
    }

Je vais prendre l'exemple des articles et des commentaires pour illustrer comment ça fonctionne au final :

DbTable_Article :

Code:

<?php    
    
    class Application_Model_DbTable_Article extends Zend_Db_Table_Abstract
    {
        protected $_name = 'Articles';
    }

DbTable_Comment :

Code:

<?php    
    
    class Application_Model_DbTable_Comment extends Zend_Db_Table_Abstract
    {
        protected $_name = 'Comments';
    }

Model_Mapper_Article

Code:

<?php

    class Application_Model_Mapper_Article extends Application_Model_Mapper_MapperAbstract
    {
        protected $_class = 'Application_Model_Article';
        
        
        //
        // Constructeur
        //
        public function __construct()
        {
            parent::__construct(new Application_Model_DbTable_Article());
        }
    }

Model_Mapper_Comment :

Code:

<?php

    class Application_Model_Mapper_Comment extends Application_Model_Mapper_MapperAbstract
    {
        protected $_class = 'Application_Model_Comment';
        
        
        //
        // Constructeur
        //
        public function __construct()
        {
            parent::__construct(new Application_Model_DbTable_Comment());
        }
        
        //
        // Récupère les commentaires par rapport à l'identifiant de l'article
        //
        public function getByArticleId($id)
        {
            $select = $this->_dbTable->select()->where('articleId = ?', $id);
            
            return new Application_Model_Collection('Application_Model_Comment', $select);
        }
    }

Model_Article :

Code:

<?php

    class Application_Model_Article extends Application_Model_ModelAbstract
    {
        protected $_title;
        
        protected $_comments;
        
        
        //
        // Constructeur
        //
        protected function _init()
        {
            $this->_comments = new Application_Model_Relation(new Application_Model_Mapper_Comment(), 'getByArticleId');
        }
        
        //
        // Surcharge de la fonction setId
        //
        public function setId($id)
        {
            $this->_comments->setArguments(array('articleId' => $id));
            
            return parent::setId($id);
        }
        
        //
        // Définit le nom d'utilisateur
        //
        public function setTitle($title)
        {
            $this->_title = (string)$title;
            
            return $this;
        }
        
        //
        // Récupère le nom d'utilisateur
        //
        public function getTitle()
        {
            return $this->_title;
        }
        
        //
        // Récupère les commentaires
        //
        public function getComments()
        {
            return $this->_comments;
        }
    }

Model_Comment :

Code:

<?php

    class Application_Model_Comment extends Application_Model_ModelAbstract
    {
        protected $_articleId;
        
        protected $_title;
        
        
        //
        // Définit le nom d'utilisateur
        //
        public function setArticleId($articleId)
        {
            $this->_articleId = (string)$articleId;
            
            return $this;
        }
        
        //
        // Récupère le nom d'utilisateur
        //
        public function getArticleId()
        {
            return $this->_articleId;
        }

        //
        // Définit le nom d'utilisateur
        //
        public function setTitle($title)
        {
            $this->_title = (string)$title;
            
            return $this;
        }
        
        //
        // Récupère le nom d'utilisateur
        //
        public function getTitle()
        {
            return $this->_title;
        }
    }

Comme on peut le voir, dans la fonction _init du modèle Article, je créé une relation sur la variable _comments :

Code:

$this->_comments = new Application_Model_Relation(new Application_Model_Mapper_Comment(), 'getByArticleId');

La fonction setId est également surchargée :

Code:

//
        // Surcharge de la fonction setId
        //
        public function setId($id)
        {
            $this->_comments->setArguments(array('articleId' => $id));
            
            return parent::setId($id);
        }

ainsi, dès que j'ai l'identifiant, je passe l'argument à la fonction setArguments de _comments (qui est, en fait un objet de type Application_Model_Relation). Cet appel va provoquer l'appel à la fonction spécifiée lors de la création de la relation (ici, "getArticleById"). getArticleById peut demander de créer soit une collection normale ou soit une collecton paginée en utilisatn Zend_Paginator.



A l'usage, tout ça donne ça :

Code:

class IndexController extends Zend_Controller_Action
    {
    
        public function init()
        {
            /* Initialize action controller here */
        }
    
        public function indexAction()
        {
            $mapperArticle = new Application_Model_Mapper_Article();
            
            $article = $mapperArticle->getById('3'); // Ici, aucun commentaire n'est chargé
            
        $article->comments->getItems(0, 2); // Par contre ici oui :)
        
        foreach($article->comments as $comment)
            echo $comment->title;
        }
    }

L'écriture :

Code:

foreach($article->comments as $comment)
            echo $comment->title;

est rendue possible par l'implémentation de l'interface Iterator_Agregate dans la classe Model_Relation.

Ici c'est une relation 1-n, avec une relation 1-1 l'écriture serait toujours aussi flexible :

Code:

public function indexAction()
        {
            $mapperUser = new Application_Model_Mapper_User();
            
            $user = $mapperUser->getById('3'); // Ici, le profile de l'utilisateur n'est pas chargé
            
        echo $user->profile->firstName; // Chargé juste là, et autorise d'accéder aux propriétés d'un autre modèle directement
    }

Désolé pour la quantité de code et les explications succintes et pas très claires, j'espère que ceratins auront le courage de lire le code smile.

Je souhaiterais avoir des retours sur ça. Si vous notez des problèmes (vérification incomplète, possibilité de faire plus simple...) n'hésitez pas.

Le point ou je suis pas très content est la classe Model_Collection, qui en fait peut être soit une collection "normale" de modèle ou soit faire office de paginateur, suivant le fait qu'on lui donne ou pas le paramètre $select. Je pense qu'il y a possibilité de faire mieux !

Merci m

Dernière modification par bakura (23-12-2010 13:47:50)

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