Un MVC en PHP5 : Data Access Layer ou couche d'accès aux données

8

Posted by Will | Posted on 03-09-2009

 MVC  wMVC  PHP5  PDO  DAL  CRUD

Dans l'optique d'écrire quelques brèves sur ce site, j'ai décidé de présenter la réalisation d'un Modèle Vue Contrôleur en PHP5. J'expliquerai fonctionnalité par fonctionnalité et pas à pas comment réaliser un MVC type en PHP5.

Cet article présente la couche d'accès aux données (Data Access Layer). Elle utilise PDO, ce qui permet notamment d'utiliser plusieurs Systèmes de Gestion de Bases de Données (SGBD).

Je présenterai ici la classe wPdo qui permet d'intéragir avec la base de données. Suivrons les présentations respectives des classes wStatement, permettant la récupération de véritables objets depuis la base et wResultObjects, représentation abstraite d'un objet en base.

Une couche supplémentaire est en place via la classe wOrm qui implémente les méthodes Create Read Update Delete (CRUD) en se servant d'une instance de la classe wPdo. Cette classe sera détaillée dans un autre article.

 

A noter que le code original vient de Julien Pauly pour le site www.developpez.com. Je n'ai qu'intégré et adapté cette implémentation dans wMVC.

 

wPdo : connexion

<?php

/**
 * Class wPdo. Comes with other classes
 * mainly to show how to tune PDO.
 *
 * @author Julien PAULI
 * @author William DURAND <william.durand1@gmail.com>
 * 
 * Code original de Julien PAULI, modifié pour wMVC
 */
class wPdo extends PDO 
{
    /**
     * Constructor.
     * Enables PDOExceptions
     * Initiates wStatement
     *
     * @param string $dsn
     * @param string $username
     * @param string $password
     * @param array $driver_options
     */
    final public function __construct($dsn, $username = '', $password = '', $driver_options = array())
    {
        parent::__construct($dsn, $username, $password, $driver_options);        
        $this->setAttribute(self::ATTR_ERRMODE, self::ERRMODE_EXCEPTION);
        $this->setAttribute(self::ATTR_STATEMENT_CLASS, array('wStatement'));
        wStatement::setPDOInstance($this);
    }
}

?>

Cette classe permet de créer un connexion avec la base de données. Ici, on modifie quelques peu les attributs par défaut pour pouvoir utiliser notre propre classe wStatement.

 

wStatement : traitement

<?php

/**
 * Replacement of PDOStatement
 * Allow fetchObjectOfClass() and fetchAllObjectOfClass()
 * 
 * @author Julien PAULI
 * @author William DURAND <william.durand1@gmail.com>
 * 
 * Code original de Julien PAULI, modifié pour wMVC
 */
final class wStatement extends PDOStatement 
{
    /**
     * wPdo instance, passed to classes allowed by
     * fetchObjectOfClass() and fetchAllObjectOfClass()
     *
     * @var wPdo
     */
    private static $_pdo;
    
    /**
     * PDOStatement doesn't allow a public constructor
     * probably due to internal job. However, we need a way
     * to pass the wPdo instance to us.
     *
     * @param wPdo $pdo
     */
    public static function setPDOInstance(wPdo $pdo)
    {
        self::$_pdo = $pdo;
    }
    
    /**
     * Internal stuff to check for class validity and
     * discovering of table name
     *
     * @param string $className
     * @throws PDOException
     * @return array
     */
    private function _prepareFetchObject($className)
    {
        if (!preg_match("/.*FROM\s(.*?)[\s|;]/i", $this->queryString, $table)) {
            throw new PDOException('Could not find table name in query');
        }
        
        if (!class_exists($className, true)) {
            throw new PDOException('Class '.$className.' does not exist');
        }
        
        $reflection = new ReflectionClass($className);
        
        if (!$reflection->isSubclassOf('JPDO_ResultObjects')) {
            throw new PDOException('Class '.$className.' should extend wResultObjects');
        }
        
        return $table;
    }
    
    /**
     * Fetch a result as an object of a class extending
     * wResults. Those class should allow their objects
     * to be saved back to the DB.
     *
     * @param string $className
     * @return wResultObjects
     */
    public function fetchObjectOfClass($className)
    {
        $table = $this->_prepareFetchObject($className);        
        $instance = new $className(self::$_pdo, $table[1]);
        $this->setFetchMode(PDO::FETCH_INTO, $instance);
        return parent::fetch(PDO::FETCH_INTO);
    
    }
    
     /**
     * Fetch a result as an object of a class extending
     * wResults. Those class should allow their objects
     * to be saved back to the DB.
     *
     * @param string $className
     * @return wResultObjects
     */
    public function fetchAllObjectOfClass($className)
    {
        $table = $this->_prepareFetchObject($className);        
        return parent::fetchAll(PDO::FETCH_CLASS, $className, array(self::$_pdo, $table[1]));    
    }
    
    /** Cette méthode ne fonctionne pas en PHP < 5.2.6
     * A cause d'un bug PHP que j'ai reporté et qui a été corrigé
	 */
    public function __call($method, $args)
    {
        if (preg_match("/^fetchAll(\w+)$/", $method, $matches)) {
            return $this->fetchAllObjectOfClass($matches[1]);
        } elseif (preg_match("/^fetchOne(\w+)$/", $method, $matches)) {
            return $this->fetchObjectOfClass($matches[1]);
        } else {
            trigger_error('Call to undefined method '.$matche[1], E_USER_ERROR);
        }
    }
}

?>

En ajoutant deux méthodes fetchObjectOfClass() et fetchAllObjectOfClass() on est en mesure de récupérer des objets depuis la base de données.

 

wResultObjects : représentation d'un objet

<?php

/**
 * Database objects
 * Allow fetchObjectOfClass()
 * 
 * @author Julien PAULI
 * @author William DURAND <william.durand1@gmail.com>
 * 
 * Code original de Julien PAULI, modifié pour wMVC
 */

abstract class wResultObjects
{
    /**
     * wPdo Instance
     *
     * @var wPdo
     */
    protected $_pdo;
    
    /**
     * table name
     *
     * @var string
     */
    protected $_tableName;
    
    /**
     * primary key
     *
     * @var string
     */
    protected static $_pk;
    
    /**
     * Constructor. Should be called if extended
     *
     * @param wPdo $pdo
     * @param string $tableName
     */
    public function __construct(wPdo $pdo, $tableName)
    {
        $this->_pdo       = $pdo;
        $this->_tableName = $tableName;
    }
    
    /**
     * Returns all public attributes
     *
     * @return array
     */
    final protected function _getPublicMembers()
    {
        $reflect = new ReflectionObject($this);
        foreach ($reflect->getProperties() as $var) {
            if (!$var->isPublic()) {
                continue;
            }
            $prop[] = $this->{$var->getName()};
        }
        return $prop;
    }
    
    /**
     * Allow stringification
     *
     * @return string
     */
    public function __toString()
    {
        $prop = $this->_getPublicMembers();
        return implode(' - ', $prop);
    }
    
    /**
     * Should set a PK
     */
    public static function setPk($pk)
    {
        self::$_pk = $pk;
    }
    
    /**
     * Returns PK name
     */
    public function getPk()
    {
    	return $this->$_pk;
    }
    
    /**
     * Should allow saving the object
     */
    abstract public function save();
}

?>

Chacun de nos objets étend cette classe. Elle contient le nom de la table et la clé primaire, deux informations nécessaires au bon fonctionnement du système. Notre classe représentant l'objet concret devra fournir ces deux informations mais également définir la manière par laquelle l'objet doit être sauvegardé.

 

Utilisation

Les classes précédentes font parties du core du MVC. Ce qui nous intéresse c'est le principe d'utilisation. Prenons un exemple, un objet Page.

Une Page sur un site contient les informations suivantes :

- Les métas Titre et Description.

- Le titre de la page

- Une description de la page

- Le stripped title qui permet d'avoir une URL compréhensible (Ex: mon-premier-article.html)

- Le contenu de la page

- Les dates de création et de mise à jour

 

Ce qui donne cette classe :

<?php

class Page extends wResultObjects 
{
	private $id;
	private $metaTitle;
	private $metaDescription;
	private $title;
	private $description;
	private $strippedTitle;
	private $htmlContent;
	private $dateHeureCreation;
	private $dateHeureMiseAJour;
	
	public function __construct()
	{
		self::setPk('id');
	}
	
	public function __set($attribute, $value)
	{
		$this->$attribute = $value;
	}
	
	public function __get($attribute)
	{
		return $this->$attribute;
	}
	
	public function __toString()
	{
		return isset($this->htmlContent) ? $this->htmlContent : '';
	}
	
  	public function save()
    {
        $cols = $this->_getPublicMembers();
        foreach ($cols as $col) {
        	$set[] = $col.'='.$this->_pdo->quote($this->$col);
        }
        $query = 'UPDATE '.$this->_tableName.' SET ' .
      				implode(',', $set) .
        			' WHERE ' .
        			self::$_pk .'='. $this->{self::$_pk};
        		
        $this->_pdo->exec($query);
       	$this->{self::$_pk} = $this->_pdo->lastInsertId();
    }
}

?>

 

Pour créer une Page, on procède de cette manière :

$maPage = new Page();

 

Et pour la sauvegarder en base :

$maPage->save();

 

En toute transparence, on sauvegarde nos objets en base, sans devoir se préoccuper de la manière de sauvegarder l'objet. C'est très pratique.

Commentaires

Ajouter un commentaire

avatar

MattGNU  (04 September 2009 - 09:08:35)

PDO c'est quand même une belle invention.
Bon tu vois que tu trouves des trucs à poster ;)

avatar

eltyty  (29 October 2009 - 09:19:41)

Bonjour,

 

tu montres la création d'une instance mais pas un exemple de construction de page avec un peu de contenu. Ca serai plus simple à comprendre ;-)

avatar

Will  (29 October 2009 - 13:20:41)

Effectivement, entre temps j'ai fait de nombreux changements, une nouvelle présentation est en cours de rédaction ;-)

Merci.

avatar

Blaz  (21 January 2010 - 00:17:42)

La nouvelle version n'est toujours pas en ligne ???

;-)

avatar

Will  (21 January 2010 - 00:20:04)

Et non, celle-ci est dailleurs un peu deprecated.

La prochaine version sera en ligne bientôt mais pour le moment des petits licences ont un TD à rendre et ça colle un peu trop à ce que je compte publier :p

avatar

Nicol  (22 February 2010 - 08:43:39)

je suis Fan ! Simple et élégant !!! Merci ................

avatar

Nicol  (25 February 2010 - 14:51:40)

Ton article m'a poussé à m'interesser un peu plus en profondeur sur le pattern DAL.

Une des évolutions de ton code serait les gestion des relations entre objets... en tout cas merci pour ton partage de connaissances, c'est une excellente base de travail !!!

Par hasard, tu aurais un bout code concernant wORM ?!!! je suis tout emoustillé...

avatar

Will  (15 March 2010 - 01:07:48)

Oui je vais mettre mes sources sur GIT dans peu de temps :)