• Ce blog — désormais archivé — est en lecture seule.

Implémentation en PHP 5.3 des concepts ORM, DAL, DAO, CRUD

Ceci est une implémentation du pattern Active Record.

Dans la lignée de cet article http://blog.mazenod.fr/2010/01/design-pattern-mvc-zoom-sur-la-couche-modele-dal-dao-orm-crud/ et dans l’optique de réécrire une partie de ce site, j’ai réécrit les bases d’un ORM. Pour les explications verbeuses, l’article ci-dessus reprend parfaitement tout ce qui sera décrit plus loin, je n’y reviendrais donc pas. L’ORM présenté ci-dessous est écrit en PHP 5.3 et reprend quelques une des nouveautés de cette version.

L’architecture de cet ORM

Schéma par v. Mazenod (www.mazenod.fr)

Reprenant l’agencement décrit dans le schéma ci-dessus, wOrm (c’est son nom) est structuré en « dossier » que l’on nommera ici « package » puisque c’est ce qu’ils représentent.­

On a donc 4 packages :

  • db : contient les interfaces (dans adapters/) qui décrivent les besoins pour la DAL et la DAO. Il y a aussi la DAO représentée par la classe wDb.

  • orm : contient l’ORM.

  • pdo : contient l’implémentation de la DAL et comme son nom l’indique, la DAL est écrite avec PDO.

  • sql : contient l’implémentation de la DAL SQL. C’est-à-dire que nous allons utiliser SQL pour dialoguer avec notre base de données.

Maintenant que nous avons décrit les différents composants, voyons la composition de chacun.

PDO à la DAL

L’interface wDbAdapter décrit la DAL. Une bonne DAL a besoin de pouvoir interroger une base de données, la requêter et ici de pouvoir récupérer des informations essentielles : les descriptions des tables.

<?php

namespace core\model\db\adapters;

/**
* Interface wDbAdapter
* Décrit les méthodes nécessaires à la communication avec une DataBase.
* Cette interface définit la DAL.
*
* @author William DURAND
*/

interface wDbAdapter
{
  /**
  * Exécute une requête.
  * @param string $query
  */

  public function exec($query);
  /**
  * Interroge la base de données.
  */

  public function query($query);
  /**
  * Retourne une description de la table de base de données spécifiée.
  * @param string $table_name
  * @return array
  */

  public function describeTable($table_name);
}

<

p style= »text-align: justify; »>

La classe wPdo est l’implémentation de cette interface. Comme son nom l’indique, wPdo va étendre la classe PDO, une extension PHP standard, fiable et performante.

<?php
namespace core\model\pdo;

use core\model\pdo\wStatement;
use core\model\db\adapters\wDbAdapter;

/**
* Classe wPdo
* Code original de Julien PAULI, modifié pour wMVC.
*
* Cette classe étend la classe PDO et implémente l'interface wDbAdapter.
* C'est l'implémentation de la DAL.
*
* @author Julien PAULI
* @author William DURAND
*/

class wPdo extends \PDO implements wDbAdapter
{
  /**
  * 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('core\model\pdo\wStatement'));
    wStatement::setPDOInstance($this);
  }

  /**
  * Exécute une requête.
  * @param string $query
  */

  public function exec($query)
  {
    return parent::exec($query);
  }

  /**
  * Interroge la base de données.
  */

  public function query($query)
  {
    return parent::query($query);
  }

  /**
  * Retourne une description de la table de base de données spécifiée.
  * @param string $table_name
  * @return array
  */

  public function describeTable($table_name)
  {
    $sql = sprintf('DESCRIBE %s', $table_name);
    $statment = $this->prepare($sql);

    $statment->execute();

    return $statment->fetchAll(\PDO::FETCH_ASSOC);
  }
}

Le constructeur paramètre PDO pour utiliser une classe Statement personnalisée. Les méthodes query() et exec() passent directement le relais à PDO. Enfin la méthode describeTable() est des plus simples.

Comme nous avons pu le voir dans le constructeur, la classe wStatement est une redéfinition de PDOStatement. Cette classe permet de définir comment nous voulons exploiter nos données récupérées en base.

<?php

namespace core\model\pdo;

/**
* Replacement of PDOStatement
* Allow fetchObjectOfClass() and fetchAllObjectOfClass()
*
* @author Julien PAULI
* @author William DURAND
*
* Code original de Julien PAULI, modifié pour wMVC
* Cette classe fait partie de la DAL.
*/

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('\core\model\orm\wOrm'))
    {
      throw new \PDOException('Class '.$className.' should extend \core\model\orm\wOrm');
    }

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

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

Plus de DAL avec SQL

La DAL permet de dialoguer avec la base de données, nous l’avons déjà dit, ce qui est important c’est de savoir comment. La plupart du temps, nous utilisons le langage SQL et c’est ce qui est fait ici.

Le code ci-dessous permet la création de requêtes SQL comme si nous manipulions des objets. C’est très intéressant pour construire une requête en plusieurs fois.

<?php

namespace core\model\sql;

/**
* Classe wSql.
* Cette classe représente la couche SQL de la DAL.
*
* @author William DURAND
*/

abstract class wSql
{
  /**
  * @return wSqlSelect
  */

  public function sqlSelect()
  {
    return new wSqlSelect();
  }

  /**
  * @return wSqlUpdate
  */

  public function sqlUpdate()
  {
    return new wSqlUpdate();
  }

  /**
  * @return wSqlDelete
  */

  public function sqlDelete()
  {
    return new wSqlDelete();
  }

  /**
  * @return wSqlInsert
  */

  public function sqlInsert()
  {
    return new wSqlInsert();
  }
}

<

p style= »text-align: justify; »>

Rien de complexe ici. Détaillons alors chaque type de requête, il y en a 4 principales :

  • insert

  • update

  • delete

  • select

Chaque type de requête est dérivée de la classe wSqlAbstract qui définit ce qui est commun à ces requêtes : une méthode de construction à redéfinir, une méthode de récupération des paramètres et une méthode de récupération de la requête en elle-même.

<?php

namespace core\model\sql;

/**
* Classe wSqlAbstract.
*
* @author William DURAND
*/

abstract class wSqlAbstract
{
  /**
  * @var string
  */

  protected $request = null;
  /**
  * @var array
  */

  protected $parameters = array();

  /**
  * @return string
  */

  public function getRequest()
  {
    $this->_build();

    return $this->request;
  }

  /**
  * @return array
  */

  public function getParameters()
  {
    $this->_build();

    return $this->parameters;
  }

  /**
  * Termine la construction de la requête et la retourne.
  * @return string
  */

  protected abstract function _build();
}

Maintenant que la base est connue, voilà les 4 classes :

<?php

namespace core\model\sql;

use core\model\sql\wSqlAbstract;

/**
* Classe wSqlDelete
* Construit une requête SQL du type DELETE.
*
* @author William DURAND
*/

class wSqlDelete extends wSqlAbstract
{
  /**
  * @var string
  */

  private $from;
  /**
  * @var array
  */

  private $where = array();

  /**
  * @return wSqlDelete
  */

  public function from($table_name)
  {
    $this->from = $table_name;

    return $this;
  }

  /**
  * @return wSqlDelete
  */

  public function where($attr, $value)
  {
    if(!empty($value))
    {
      $this->where[$attr] = $value;
    }

    return $this;
  }

  /**
  * Termine la construction de la requête et la retourne.
  * @return string
  */

  protected function _build()
  {
    if(is_null($this->request))
    {
      $str = 'DELETE FROM ';
      $str .= $this->from;

      if(count($this->where) > 0)
      {
        $str .= ' WHERE ';

        $where = '';
        foreach($this->where as $attr => $value)
        {
          if(!empty($value))
          {
            if(!empty($where))
            {
              $where .= ' AND ';
            }

            $where .= $attr . ' = ?';
            $this->parameters[] = $value;
          }
        }

        $str .= $where;
      }

      $this->request = $str;
    }
  }
}
<?php

namespace core\model\sql;

use core\model\sql\wSqlAbstract;

/**
* Classe wSqlSelect
* Construit une requête SQL du type SELECT.
*
* @author William DURAND
*/

class wSqlSelect extends wSqlAbstract
{
  /**
  * @var string
  */

  private $from = '';
  /**
  * @var string
  */

  private $alias = '';
  /**
  * @var string
  */

  private $select = '';
  /**
  * @var array
  */

  private $where = array();
  /**
  * @var array
  */

  private $order = array();
  /**
  * @var int
  */

  private $limit;
  /**
  * @var int
  */

  private $offset;

  /**
  * @return wSqlSelect
  */

  public function from($from, $alias = null)
  {
    if(!is_null($alias))
    {
      $alias = $this->alias . '.';
      $this->alias = $alias;
    }

    if(is_array($from))
    {
      $this->from = $alias . $from['table_name'];

      foreach($from['attr'] as $attr)
      {
        $this->select .= $alias . $attr;
      }
    }
    else
    {
      $this->from = $from;
    }

    $this->select = empty($this->select) ? '*' : $this->select;

    return $this;
  }

  /**
  * @return wSqlSelect
  */

  public function where($attr, $value)
  {
    if(!empty($value))
    {
      $this->where[$attr] = $value;
    }

    return $this;
  }

  /**
  * @return wSqlSelect
  */

  public function orderBy($attr, $sens)
  {
    $this->order[$attr] = $sens;

    return $this;
  }

  /**
  * @return wSqlSelect
  */

  public function limit($limit, $offset)
  {
    $this->limit = $limit;
    $this->offset = $offset;

    return $this;
  }

  /**
  * Termine la construction de la requête et la retourne.
  * @return string
  */

  protected function _build()
  {
    if(is_null($this->request))
    {
      $str = 'SELECT ';
      $str .= $this->select;
      $str .= ' FROM ';
      $str .= $this->from;
      $str .= ' ';

      $alias = '';
      if(!empty($this->alias))
      {
        $str .= ' AS ';
        $str .= $this->alias;

        $alias .= $this->alias . '.';
      }

      if(count($this->where) > 0)
      {
        $str .= ' WHERE ';

        $where = '';
        foreach($this->where as $attr => $value)
        {
          if(!empty($value))
          {
            if($where != '')
            {
              $where .= ' AND ';
            }

            $where .= $alias . $attr . '?';
            $this->parameters[] = $value;
          }
        }

        $str .= $where;
      }

      if(count($this->order) > 0)
      {
        $str .= ' ORDER BY ';

        $order = '';
        foreach($this->order as $attr => $sens)
        {
          if($order != '')
          {
            $order .= ',';
          }

          $order .= $alias . $attr . ' ' . $sens;
        }

        $str .= $order;
      }

      if(isset($this->limit))
      {
        $str .= ' LIMIT ' . $this->limit . ', ' . $this->offset;
      }

      $this->request = $str;
    }
  }
}
<?php

namespace core\model\sql;

use core\model\sql\wSqlAbstract;

/**
* Classe wSqlUpdate
* Construit une requête SQL du type UPDATE.
*
* @author William DURAND
*/

class wSqlUpdate extends wSqlAbstract
{
  /**
  * @var string
  */

  private $from;
  /**
  * @var array
  */

  private $set = array();
  /**
  * @var array
  */

  private $where = array();

  /**
  * @return wSqlUpdate
  */

  public function from($table_name)
  {
    $this->from = $table_name;

    return $this;
  }

  /**
  * @return wSqlUpdate
  */

  public function where($attr, $value)
  {
    if(!empty($value))
    {
      $this->where[$attr] = $value;
    }

    return $this;
  }

  /**
  * @return wSqlUpdate
  */

  public function addSet($attr, $value)
  {
    if(!empty($value))
    {
      $this->set[$attr] = $value;
    }

    return $this;
  }

  /**
  * Termine la construction de la requête et la retourne.
  * @return string
  */

  protected function _build()
  {
    if(is_null($this->request))
    {
      $str = 'UPDATE ';
      $str .= $this->from;
      $str .= ' SET ';

      $set = array();
      foreach($this->set as $attr => $value)
      {
        if(!empty($value))
        {
          $set[] = $attr . ' = ?';

          $this->parameters[] = $value;
        }
      }

      $str .= implode(',', $set);

      if(count($this->where) > 0)
      {
        $str .= ' WHERE ';

        $where = '';
        foreach($this->where as $attr => $value)
        {
          if(!empty($value))
          {
            if(!empty($where))
            {
              $where .= ' AND ';
            }

            $where .= $attr . ' = ?';

            $this->parameters[] = $value;
          }
        }

        $str .= $where;
      }

      $this->request = $str;
    }
  }
}

La partie DAL est terminée. Voyons maintenant la DAO.

DAO

C’est la classe wDb qui se charge des fonctionnalités propres à une DAO. Elle implémente trois interfaces décrivant ces besoins :

  • wTransactionAdapter : décrit les méthodes nécessaires pour gérer les transactions de base de données.

  • wDataSourceAdapter : méthodes utiles pour l’accès aux données.

  • wCrudAdapter : méthodes CRUD.

<?php
namespace core\model\db\adapters;

/**
* Interface wTransactionAdapter
* Décrit les méthodes nécessaires à l'utilisation des Transactions.
*
* @author William DURAND
*/

interface wTransactionAdapter
{
  /**
  * Commencer une transaction.
  */

  public function beginTransaction();
  /**
  * Enregistrer les modifications.
  */

  public function commit();
  /**
  * Annuler les modifications.
  */

  public function rollBack();
}
<?php
namespace core\model\db\adapters;

/**
* Interface wDataSourceAdapter
* Décrit les méthodes nécessaires à une DataSource.
*
* @author William DURAND
*/

interface wDataSourceAdapter
{
  /**
  * Retourne le nombre d'objets comptés.
  * @param object $object
  * @param array $criteria
  * @return integer
  */

  public function count($object, $criteria = array());
  /**
  * Retourne un tableau d'objets.
  * @param object $object
  * @param array $criteria
  * @param array $order
  * @param integer $limit
  * @param integer $offset
  * @return array
  */

  public function find($object, $criteria = array(), $order = null, $limit = null, $offset = null);
}
<?php

namespace core\model\db\adapters;

/**
* Interface wCrudAdapter
* Description du besoin CRUD :
* - Create
* - Read
* - Update
* - Delete
*
* @author William DURAND
*/

interface wCrudAdapter
{
  /**
  * Create a new object
  * @param object $object
  */

  public function create($object);
  /**
  * Read an object
  * @param object $object
  */

  public function read($object);
  /**
  * Update an object
  * @param object $object
  */

  public function update($object);
  /**
  * Delete an object
  * @param object $object
  */

  public function delete($object);
}

Voici ensuite l’implémentation directe :

<?php

namespace core\model\db;

use core\model\sql\wSql;
use core\model\db\adapters\wDbAdapter;
use core\model\db\adapters\wCrudAdapter;
use core\model\db\adapters\wDataSourceAdapter;
use core\model\db\adapters\wTransactionAdapter;

/**
* Classe wDb
* Etend la classe abstraite wSql.
* Implémente les interfaces wTransactionAdapter, wDataSourceAdapter et wCrudAdapter.
* Permet de gérer la persistence des données contenues dans des objets.
* Cette classe représente la DAO.
*
* @author William DURAND
*/

class wDb extends wSql implements wTransactionAdapter, wDataSourceAdapter, wCrudAdapter
{
  /**
  * Objet DAL
  * @param wDbAdapter
  */

  private $_dataBase;

  public function __construct(wDbAdapter $db)
  {
  $this->_dataBase = $db;
  }

  /**
  * @return wDbAdapter
  */

  private function _getDb()
  {
  return $this->_dataBase;
  }

  /**
  * @param array $criteria
  * @return string
  */

  private function _getWhere(array $criteria)
  {
  $result = array();

  foreach($criteria as $column => $value)
  {
  if(strpos($value, '*') !== false)
  {
  $result["{$column} LIKE "] = str_replace('*', '%', $value);
  }
  else
  {
  $result["{$column} = "] = $value;
  }
  }

  return $result;
  }

  /**
  * @param string $table_name
  * @return string
  */

  private function _getPrimaryKeyColumn($table_name)
  {
  $ret = null;
  $columns = $this->_getDb()->describeTable($table_name);

  foreach($columns as $column)
  {
  if($column['Key'] === 'PRI')
  {
  if(is_null($ret))
  {
  $ret = $column['Field'];
  }
  else
  {
  $ret = null;
  break;
  }
  }
  }

  return $ret;
  }

  /**
  * @param object $object
  * @return array
  */

  protected function _getMembers($object)
  {
  $prop = array();

  $reflect = new \ReflectionObject($object);

  foreach($reflect->getProperties(\ReflectionProperty::IS_PROTECTED) as $var)
  {
  if(!$var->isStatic() &amp;&amp; $var->getDeclaringClass()->getName() == get_class($object))
  {
  $prop[] = $var->getName();
  }
  }

  return $prop;
  }

  /**
  * @param object $object
  * @return array
  */

  protected function _getValues($object)
  {
  $values = array();

  foreach($this->_getMembers($object) as $attr)
  {
  $values[$attr] = $object->$attr;
  }

  return $values;
  }

  /**
  * Retourne un tableau d'objets.
  * @param object $object
  * @param array $criteria
  * @param array $order
  * @param integer $limit
  * @param integer $offset
  * @return array
  */

  public function find($object, $criteria = array(), $order = null, $limit = null, $offset = null)
  {
  $table_name = $object::getTableName();

  $select = $this->sqlSelect()->from($table_name);

  foreach($this->_getWhere($criteria) as $where => $value)
  {
  $select->where($where, $value);
  }

  if(count($order) > 0)
  {
  foreach($order as $col => $sens)
  {
  $select->orderBy($col, $sens);
  }
  }

  if ($limit !== null)
  {
  $select->limit($limit, $offset);
  }

  $stmt = $this->_getDb()->prepare($select->getRequest());
  $stmt->execute($select->getParameters());
  $result = $stmt->fetchAllObjectOfClass($object);

  return $result;
  }

  /**
  * Retourne le nombre d'objets comptés.
  * @param object $object
  * @param array $criteria
  * @return integer
  */

  public function count($object, $criteria = array())
  {
  $table_name = $object::getTableName();

  $select = $this->sqlSelect()->from(array('table_name' => $table_name, 'attr' => array('COUNT(*)')));

  foreach($this->_getWhere($criteria) as $where => $value)
  {
  $select->where($where, $value);
  }

  $stmt = $this->_getDb()->prepare($select->getRequest());
  $stmt->execute($select->getParameters());
  $result = $stmt->fetchAll(\PDO::FETCH_ASSOC);

  return (integer)$result[0]['COUNT(*)'];
  }

  /**
  * Commencer une transaction.
  */

  public function beginTransaction()
  {
  $this->_getDb()->beginTransaction();
  }

  /**
  * Enregistrer les modifications.
  */

  public function commit()
  {
  $this->_getDb()->commit();
  }

  /**
  * Annuler les modifications.
  */

  public function rollBack()
  {
  $this->_getDb()->rollBack();
  }

  /**
  * Create a new object
  * @param object $object
  */

  public function create($object)
  {
  $table_name = $object::getTableName();
  $primaryKeyColumn = $this->_getPrimaryKeyColumn($table_name);

  $insert = $this->sqlInsert()->into($table_name);

  foreach($this->_getMembers($object) as $attr)
  {
  $insert->addValue($attr, $object->$attr);
  }

  try
  {
  $stmt = $this->_getDb()->prepare($insert->getRequest());
  $stmt->execute($insert->getParameters());
  $object->$primaryKeyColumn = $this->_getDb()->lastInsertId();
  }
  catch(\Exception $e)
  {
  echo $e->getMessage();
  echo '';
  echo '';
  echo $insert->getRequest();
  echo '';
  echo '';
  echo $e->getTraceAsString();
  }
  }

  /**
  * Read an object
  * @param object $object
  */

  public function read($object)
  {
  throw new Exception('Not yet implemented (probably never...)');
  }

  /**
  * Update an object
  * @param object $object
  */

  public function update($object)
  {
  $table_name = $object::getTableName();
  $primaryKeyColumn = $this->_getPrimaryKeyColumn($table_name);

  $update = $this->sqlUpdate()->from($table_name);

  $data = $this->_getValues($object);

  $updateData = $data;
  unset($updateData[$primaryKeyColumn]);

  foreach($updateData as $attr => $value)
  {
  $update->addSet($attr, $value);
  }

  $update->where($primaryKeyColumn, $data[$primaryKeyColumn]);

  try
  {
  $stmt = $this->_getDb()->prepare($update->getRequest());
  $stmt->execute($update->getParameters());
  }
  catch(\Exception $e)
  {
  echo $e->getMessage();
  echo '';
  echo '';
  echo $update->getRequest();
  echo '';
  echo '';
  echo $e->getTraceAsString();
  }
  }
 
  /**
  * Delete an object
  * @param object $object
  */

  public function delete($object)
  {
  $table_name = $object::getTableName();
  $primaryKeyColumn = $this->_getPrimaryKeyColumn($table_name);

  $data = $this->_getValues($object);

  $delete = $this->sqlDelete()->from($table_name)->where($primaryKeyColumn, $data[$primaryKeyColumn]);

  try
  {
  $stmt = $this->_getDb()->prepare($delete->getRequest());
  $stmt->execute($delete->getParameters());
  }
  catch(\Exception $e)
  {
  echo $e->getMessage();
  echo '';
  echo '';
  echo $delete->getRequest();
  echo '';
  echo '';
  echo $e->getTraceAsString();
  }
  }
}

Ici aussi, tout est suffisamment explicite. Il ne nous reste plus que la classe décrivant l’ORM.

ORM

L’ORM utilise la DAO pour l’accès aux données et la manipulation directe d’objets. L’intérêt d’utiliser un ORM est de pouvoir utiliser les relations entre objets. Par exemple, un article contient des commentaires. Si nous n’utilisons pas d’ORM, il faudra probablement récupérer l’article et les articles comme suit :

Article::findOneById(999);
$comments = Comments::findByArticleId(999);

Or avec un ORM la gestion est plus naturelle :

$article = Article::findOneById(999);
$comments = $article->getComments();

Ce type de relation (1-N ici) est géré par l’ORM, il peut en gérer trois au total : 1-1, 1-N et N-N. Pour utiliser ces relations, il faudra les configurer avec la méthode addRelation() :

/**
* Adds an object relation.
* @param $type ONE_TO_ONE | ONE_TO_MANY | MANY_TO_MANY
* @param array $params
*
* Parameters :
* 'column' Generally id
* 'foreignClass' Model object's class
* 'foreignColumn' You should set xxxx_id
* 'relationClass' Model object's class used in N-N relation
*/

public function addRelation($type, array $params)
{
  switch($type)
  {
    case self::ONE_TO_ONE:
      static::_addOneToOneRelation($params);
      break;
    case self::ONE_TO_MANY:
      static::_addOneToManyRelation($params);
      break;
    case self::MANY_TO_MANY:
      static::_addManyToManyRelation($params);
      break;

    default:
  }
}

Plusieurs paramètres sont disponibles comme on peut le voir dans le commentaire de la méthode. Ensuite, toute la gestion est faite en interne dans la classe. Elle s’occupe de regarder de quel type de relation il s’agit puis elle la stocke. La récupération se fait aisément avec les méthodes magiques PHP qui vont permettre d’utiliser des méthodes non-écrites à l’avance, des getters on-the-fly en somme.

Pour les relations, par exemple la méthode getComments() vue plus haut, ce sera probablement la méthode __call() qui s’en chargera :

/**
* Usage :
* - getProperty()
* - setProperty()
* where Property is an attribute
*
* - getRelationProperty()
* - setRelationProperty() where RelationPropery
* is the name of the relation.
*
* Exemple : $object->getMembers()
*/

public function __call($method, $args)
{
  if(!preg_match('/^(get|set)(\w+)$/', $method, $matches))
  {
    throw new \Exception("Call to undefined method {$method}");
  }

  if(array_key_exists($matches[2], static::$_relations))
  {
    $toCall = '_' . $matches[1] . 'Relation';

    return $this->$toCall(static::$_relations[$matches[2]], $args);
  }
  else
  {
    $property = strtolower($matches[2]);
    $reflection = new \ReflectionObject($this);

    if($reflection->hasProperty($property))
    {
      if($matches[1] == 'get')
      {
        return $this->__get($property);
      }
      else if($matches[1] == 'set')
      {
        return $this->__set($property, $args[0]);
      }
    }
    else
    {
      throw new \Exception('Relation ' . $matches[2] .' not found');
    }
  }
}

La méthode __callStatic() elle va permettre de créer des Magic finders (cf. Doctrine, l’ORM à mon sens le plus performant en PHP). L’entête de la méthode est ici aussi suffisamment explicite :

/**
* Magic finders
* Usage :
* - findByTitle(...)
* - findOneByTitle(...)
* - countByTitle(...)
* - findByTitleAndDescription(...)
*/

public static function __callStatic($method, $params)
{
  if(!preg_match('/^(find|findOne|count)By(\w+)$/', $method, $matches))
  {
    throw new \Exception("Call to undefined method {$method}");
  }

  $criteriaKeys = explode('_And_', preg_replace('/([a-z0-9])([A-Z])/', '$1_$2', $matches[2]));
  $criteriaKeys = array_map('strtolower', $criteriaKeys);
  $criteriaValues = array_slice($params, 0, count($criteriaKeys));
  $criteria = array_combine($criteriaKeys, $criteriaValues);

  $method = $matches[1];
  return static::$method($criteria);
}

Voilà la classe dans sa totalité :

<?php

namespace core\model\orm;

use core\model\db\adapters\wCrudAdapter;
use core\model\db\adapters\wDataSourceAdapter;

/**
* Classe wORM = Object Relation Mapping (ORM) de wMVC
* Permet de faire le lien entre les objets et ses relations.
* Chaque objet métier (model) devrait étendre cette classe.
*
* @author William DURAND
*/

abstract class wOrm
{
/**
* Relation 1-1
*/

const ONE_TO_ONE = 1;
/**
* Relation 1-N
*/

const ONE_TO_MANY = 2;
/**
* Relation N-N
*/

const MANY_TO_MANY = 3;

/**
* wDataSourceAdapter
* @var wDataSourceAdapter
*/

protected static $_dataSource = null;
/**
* Object model name
* @var string
*/

protected static $_name = null;
/**
* Object table name
* @var string
*/

protected static $_tableName = null;
/**
* Defines object relations
* @var array
*/

protected static $_relations = array();
/**
* @var boolean
*/

private $_isNew;
/**
* @var wDataSourceAdapter
*/

private $_dataSrc = null;
/**
* Model objects relations' container
* @var array
*/

protected $_dataRelations = array();

/**
* Constructor
*/

public function __construct()
{
$this->_isNew = true;
$this->_dataSrc = static::getDataSource();
}

/**
* Setter
* @param string $name
* @param mixed $value
*/

public function __set($name, $value)
{
$this->$name = htmlspecialchars($value);
}

/**
* Getter
* @param string $name
*/

public function __get($name)
{
return htmlspecialchars_decode($this->$name);
}

/**
* Returns the wDataSourceAdapter object
* @return wDataSourceAdapter
*/

public static function getDataSource()
{
return static::$_dataSource;
}

/**
* @param wDataSourceAdapter dataSource
*/

public static function setDataSource(wDataSourceAdapter $dataSource)
{
static::$_dataSource = $dataSource;
}

/**
* Retourne le nom de la table en base de données qui correspond à l'objet.
* @return string
*/

protected static function _getTableName()
{
return isset(static::$_tableName) ? static::$_tableName : strtolower(preg_replace('/([a-zA-Z]+[\\])/i', '', static::_getName()));
}

/**
* Retourne le nom de la table en base de données qui correspond à l'objet.
* @return string
*/

public static function getTableName()
{
return static::_getTableName();
}

/**
* Retourne le nom de l'objet.
* @return string
*/

protected static function _getName()
{
return isset(static::$_name) ? static::$_name : get_called_class();
}

/**
* @param boolean $isNew
*/

protected function _setNew($isNew)
{
$this->_isNew = $isNew;
}

/**
* Determines if the current object is new or not.
* @return boolean
*/

public function isNew()
{
return $this->_isNew;
}

/**
* @return wDataSourceAdapter
*/

protected function _getDataSource()
{
return $this->_dataSrc;
}

/**
* Returns an objects array.
* @param array $criteria
* @param array $order
* @param integer $limit
* @param integer $offset
* @return array
*/

public static function find(array $criteria = array(), array $order = null, $limit = null, $offset = null)
{
$result = static::getDataSource()->find(static::_getName(), $criteria, $order, $limit, $offset);

$objects = array();

foreach ($result as $object)
{
$object->_setNew(false);
$objects[] = $object;
}

return $objects;
}

/**
* Returns the objects counted number.
* @param array $criteria
* @return integer
*/

public static function count(array $criteria = array())
{
return static::getDataSource()->count(static::_getName(), $criteria);
}

/**
* Returns an object.
* @param array $criteria
* @param array $order
* @return wOrm
*/

public static function findOne(array $criteria = array(), array $order = null)
{
$objects = static::find($criteria, $order, 0, 1);

return count($objects) == 1 ? $objects[0] : null;
}

/**
* Checks if the current object is read-only or not.
*/

private function _validateModifiable()
{
if(!($this->_getDataSource() instanceof wCrudAdapter))
{
throw new Exception('Object is read-only.');
}
}

/**
* Saves the current object (or updates it).
*/

public function save()
{
$this->_validateModifiable();

if($this->isNew())
{
$this->_getDataSource()->create($this);
}
else
{
$this->_getDataSource()->update($this);
}

$this->_saveRelations();
$this->_setNew(false);
}

/**
* Deletes the current object.
*/

public function delete()
{
$this->_validateModifiable();

$this->_getDataSource()->delete($this);
}

/**
* Magic finders
* Usage :
* - findByTitle(...)
* - findOneByTitle(...)
* - countByTitle(...)
* - findByTitleAndDescription(...)
*/

public static function __callStatic($method, $params)
{
if(!preg_match('/^(find|findOne|count)By(\w+)$/', $method, $matches))
{
throw new \Exception("Call to undefined method {$method}");
}

$criteriaKeys = explode('_And_', preg_replace('/([a-z0-9])([A-Z])/', '$1_$2', $matches[2]));
$criteriaKeys = array_map('strtolower', $criteriaKeys);
$criteriaValues = array_slice($params, 0, count($criteriaKeys));
$criteria = array_combine($criteriaKeys, $criteriaValues);

$method = $matches[1];
return static::$method($criteria);
}

/**
* Exécute la fonction $callback en mode transactionnel.
* @param function $callback
*/

public static function transaction($callback)
{
if(!(static::getDataSource() instanceof Transactional))
{
call_user_func($callback);
return;
}

try
{
static::getDataSource()->beginTransaction();
call_user_func($callback);
static::getDataSource()->commit();
}
catch(Exception $e)
{
static::getDataSource()->rollBack();
throw $e;
}
}

/* Here is the part of ORM that deals with object relations */

/**
* Adds an object relation.
* @param $type ONE_TO_ONE | ONE_TO_MANY | MANY_TO_MANY
* @param array $params
*
* Parameters :
* 'column' Generally id
* 'foreignClass' Model object's class
* 'foreignColumn' You should set xxxx_id
* 'relationClass' Model object's class used in N-N relation
*/

public function addRelation($type, array $params)
{
switch($type)
{
case self::ONE_TO_ONE:
static::_addOneToOneRelation($params);
break;
case self::ONE_TO_MANY:
static::_addOneToManyRelation($params);
break;
case self::MANY_TO_MANY:
static::_addManyToManyRelation($params);
break;

default:
}
}

/**
* Defines a 1-1 relation
*/

protected static function _addOneToOneRelation(array $params)
{
$name = $params['foreignClass'];

$params['type'] = self::ONE_TO_MANY;

static::$_relations[$name] = $params;
}

/**
* Defines a 1-N relation
*/

protected static function _addOneToManyRelation(array $params)
{
$name = $params['foreignClass'] . 's';

$params['type'] = self::ONE_TO_ONE;

static::$_relations[$name] = $params;
}

/**
* Defines a N-N relation
*/

protected static function _addManyToManyRelation(array $params)
{
$name = $params['foreignClass'] . 's';

$params['type'] = self::MANY_TO_MANY;

static::$_relations[$name] = $params;
}

/**
* Usage :
* - getProperty()
* - setProperty()
* where Property is an attribute
*
* - getRelationProperty()
* - setRelationProperty() where RelationPropery
* is the name of the relation.
*
* Exemple : <code>$object->getMembers()
*/

public function __call($method, $args)
{
if(!preg_match('/^(get|set)(\w+)$/', $method, $matches))
{
throw new \Exception("Call to undefined method {$method}");
}

if(array_key_exists($matches[2], static::$_relations))
{
$toCall = '_' . $matches[1] . 'Relation';

return $this->$toCall(static::$_relations[$matches[2]], $args);
}
else
{
$property = strtolower($matches[2]);
$reflection = new \ReflectionObject($this);

if($reflection->hasProperty($property))
{
if($matches[1] == 'get')
{
return $this->__get($property);
}
else if($matches[1] == 'set')
{
return $this->__set($property, $args[0]);
}
}
else
{
throw new \Exception('Relation ' . $matches[2] .' not found');
}
}
}

/**
* Saves object's relations
*/

protected function _saveRelations()
{
foreach($this->_dataRelations as $relation)
{
$this->_saveRelation($relation);
}
}

/**
* Saves a relation
*/

protected function _saveRelation(array $params)
{
$objs = $params['objs'];
$column = $params['column'];
$object = $params['foreignClass'];
$foreignColumn = $params['foreignColumn'];

$object = 'objects\' . strtolower($object) . '\' . $object;

if($params['
type'] == self::MANY_TO_MANY)
{
$relationClass = $params['
relationClass'];
$relationClass = '
objects\' . strtolower($relationClass) . '\' . $relationClass;

$left_col = static::_getTableName() . '
_' . $column;
$right_col = $object::_getTableName() . '
_' . $foreignColumn;

foreach($objs as $obj)
{
$obj->save();

$relationObject = new $relationClass();
$relationObject->$left_col = $this->$column;
$relationObject->$right_col = $obj->$foreignColumn;

$relationObject->save();
}
}
else
{
foreach($objs as $obj)
{
$obj->$foreignColumn = $this->$column;
$obj->save();
}
}
}

/**
* Adds a relation'
s data to the current object.
* @param array @params
* @param array $objects
*/
protected function _setRelation(array $params, array $objects)
{
if(is_null($objects) || count($objects) < 1)
{
throw new \Exception('Cannot set a relation without at least an object');
}

$params['objs'] = $objects;
array_push($this->_dataRelations, $params);
}

/**
* Returns an objects array.
* @param array $params
* @param array $args
* @return array
*/

protected function _getRelation(array $params, array $args = null)
{
$array = array();

$column = $params['column'];
$object = $params['foreignClass'];
$foreignColumn = $params['foreignColumn'];

$object = 'objects\' . strtolower($object) . '\' . $object;

if($params['
type'] == self::MANY_TO_MANY)
{
$relationClass = $params['
relationClass'];
$relationClass = '
objects\' . strtolower($relationClass) . '\' . $relationClass;

$left_col = static::_getTableName() . '
_' . $column;
$right_col = strtolower($object) . '
_' . $foreignColumn;

$relationObjects = $relationClass::find(array($left_col => $this->$column));

foreach($relationObjects as $obj)
{
array_push($array, $object::find(array($foreignColumn => $obj->$right_col)));
}
}
else
{
$array = $object::find(array($foreignColumn => $this->$column));
}

$array = is_null($array) ? array() : $array;

$params['
objs'] = $array;
array_merge($this->_dataRelations, $params);

return $array;
}
}

Exemple concret

Pour reprendre l’exemple des articles et des commentaires, voici un exemple concret d’utilisation.

La classe Article tout d’abord :

<?php
namespace model\article;

use core\model\orm\wOrm;

/**
* Classe Article
* @method int getId() Returns the article's id
* @method string getTitle() Returns the article's title
* @method string getHook() Returns the article's hook
* @method string getText() Returns the article's text
* @method boolean getIs_published() Returns true if the article is published, false otherwise
* @method string getCreate_date() Returns the article's creation datetime
* @method string getUpdate_date() Returns the article's update datetime
*
* @method array getCommentaires()
*/

class Article extends wOrm
{
/* Definition */
protected $id;
protected $title;
protected $hook;
protected $text;
protected $is_published;
protected $create_date;
protected $update_date;

public function __construct()
{
parent::__construct();

$this->addRelation(wOrm::ONE_TO_MANY , array('column' => 'id', 'foreignClass' => 'Commentaire', 'foreignColumn' => 'article_id'));
$this->addRelation(wOrm::MANY_TO_MANY , array('column' => 'id', 'foreignClass' => 'Tag', 'foreignColumn' => 'id', 'relationClass' => 'ArticleTag'));
}

/* Custom methods should come here */

/**
* @return boolean
*/

public function isPublished()
{
return $this->getIs_published();
}

/**
* @return int
*/

public function getNbCommentaires()
{
return count($this->getCommentaires());
}
}

On voit bien la description de deux relations de différents types.

$this->addRelation(wOrm::ONE_TO_MANY , array('column' => 'id', 'foreignClass' => 'Commentaire', 'foreignColumn' => 'article_id'));
$this->addRelation(wOrm::MANY_TO_MANY , array('column' => 'id', 'foreignClass' => 'Tag', 'foreignColumn' => 'id', 'relationClass' => 'ArticleTag'));

Les noms des attributs sont les mêmes que ceux en base. Pour utiliser au maximum les fonctionnalités de l’IDE il est important de bien documenter ces classes mais ce n’est pas la seule raison.

Les méthodes magiques n’apparaissent pas en suggestion puisque l’IDE ne peut les trouver, le fait de déclarer @method dans l’entête de la classe permet d’obtenir cette méthode dans les suggestions d’auto-complétion. Voilà tout l’intérêt de ce gros bloc de commentaires.

La classe Commentaire ne dispose pas de définition de relations. Ici, il y a une relation maître/esclave pour cette relation 1-N. C’est la classe du côté du 1 qui définit la relation.

<?php

namespace model\commentaire;

use core\model\orm\wOrm;

/**
* Classe Commentaire
* @method int getId() Returns the commentaire's id
* @method string getAuthor() Returns the commentaire's title
* @method string getEmail() Returns the commentaire's email
* @method string getMessage() Returns the commentaire's message
* @method boolean getIs_published() Returns true if the commentaire is published, false otherwise
* @method string getCreate_date() Returns the commentaire's creation datetime
* @method string getUpdate_date() Returns the commentaire's update datetime
* @method string getArticle_id() Returns the article's id
*/

class Commentaire extends wOrm
{
/* Definition */
protected $id;
protected $author;
protected $email;
protected $message;
protected $is_published;
protected $create_date;
protected $update_date;
protected $article_id;

public function __construct()
{
parent::__construct();
}

/* Custom methods should come here */

public function isPublished()
{
return $this->getIsPublished();
}

/**
* @return Article
*/

public function getArticle()
{
return Article::find(array('id' => $this->getArticle_id()));
}

/**
* @return string
*/

public function getArticleTitle()
{
return $this->getArticle()->getTitle();
}

/**
* @return string
*/

public function getHashEmail()
{
return md5($this->getEmail());
}
}

Un script d’essai est fourni sur le repository Git, le résultat de sortie est la suivante :

Database overview :

- 1 article(s)

- 0 commentaire(s)

- 0 tag(s)

L’article intitulé « Création d\’un WebService avec Talend Open Studio » comporte 0 commentaire(s).

Create a new ‘Commentaire’ :

!t took the id : 51.

Gonna retrieve it…

Hi ! How are you ? That’s pretty cool ! =P by William.

Playing with relations :

Adds two comments to our article (1-N relation) : how many comments has it ?

Before: 0

After: 2

Comment’s article :

  • William said Hi ! How are you ? That’s pretty cool ! =P
  • William (again) said Hi ! This comment will not be registered until the article be saved.

And now ?

- 1 article(s)

- 1 commentaire(s)

- 0 tag(s)

Delete the comment !

The end !

- 1 article(s)

- 0 commentaire(s)

- 0 tag(s)

C’est tout pour aujourd’hui. L’idée était surtout de présenter une implémentation claire et simple des idées dégagées par Vincent Mazenod lors de nos divers échanges et recherches.

Retrouver le code complet sur GIT : http://github.com/willdurand/Tests-ORM

Sources

http://techportal.ibuildings.com/2010/01/11/learning-php-5-3-by-writing-your-own-orm/

Doctine 1.2 pour l’inspiration.

Toutes mes erreurs sur wMVC v1.

  • Print
  • Digg
  • StumbleUpon
  • del.icio.us
  • Facebook
  • Twitter
  • Google Bookmarks
  • FriendFeed
  • LinkedIn
  • MySpace
  • Netvibes
  • PDF
  • Ping.fm
  • RSS
  • Technorati
  • viadeo FR
  • Wikio
  • Yahoo! Buzz

Related Posts

Cet article a été publié dans Ancien blog avec les mots-clefs : , , , , , , . Bookmarker le permalien. Les commentaires et les trackbacks sont fermés.

13 commentaires

  1. Kévin
    Le 28 octobre 2010 à 23 h 11 min | Permalien

    Bonsoir,

    tout d’abord merci pour cet article très complet.

    Par contre j’ai un problème avec l’ORM et plus précisément les relations entre les tables. Je m’explique :
    J’ai donc une table album (id_album, …) et une table Photo qui est relié à la table Album par l’attribut album_id.

    Dans ma classe Album j’ai donc créé la relation suivante :

    « $this->addRelation(wOrm::ONE_TO_MANY , array(‘column’ => ‘id’, ‘foreignClass’ => ‘Commentaire’, ‘foreignColumn’ => ‘article_id’)); »

    Puis j’ai créer une fonction qui devrait retourner le nombre de photo dans l’album comme ceci :

    public function getNbCommentaires()
    {
    return count($this->getCommentaires());
    }

    Mais celle ci me retourne toujours 0 et je ne comprend pas.

    Merci d’avance de me conseiller.

    • Le 14 novembre 2010 à 21 h 50 min | Permalien

      Salut, si tu as bien définis les paramètres, il ne devrait pas y avoir de soucis…

      • patryyyck
        Le 14 juin 2011 à 14 h 38 min | Permalien

        J’ai exactement le même problème que Kevin.

        D’ailleurs, j’ai ajouté une ou deux lignes dans la table Commentaire liées à l’article 1.
        Pourtant, j’ai toujours Before = 0 dans ton exemple.

        Merci pour ton aide.

        Cordialement

        • Le 11 juillet 2011 à 23 h 59 min | Permalien

          J’ai !
          Il y a un problème lors du getRelation (dans wOrm).
          Il n’injecte pas correctement les objets liés dans l’objet principal. Le « fix » n’est pas compliqué par contre il faut faire attention aux redondances. Pour ça le mieux est de se baser sur les Pks des objets.
          Ensuite il faudra voir du côté du setRelation (wOrm également).

          Voilà :)

  2. Nicolas
    Le 18 mai 2011 à 11 h 09 min | Permalien

    Bonjour,

    Je teste en ce moment ton petit ‘framework’, et j’ai un problème :
    En reprenant ton exemple (index.php fournit), j’ai voulu sauvegardé l’article après avoir ajouté les relations (setCommentaires()).

    Voilà le code :
    <?php

    namespace test;

    /* Core */
    use core\model\db\wDb;
    use core\model\pdo\wPdo;
    use core\model\orm\wOrm;

    /* Models */
    use model\tag\Tag;
    use model\article\Article;
    use model\commentaire\Commentaire;

    /* Defines the autoload function */
    spl_autoload_register(function($class) {
    $tab = explode('\\', $class);
    $path = implode(DIRECTORY_SEPARATOR, $tab) . '.class.php';

    if(file_exists($path))
    {
    include_once($path);
    return true;
    }

    throw new \Exception('Cannot load : ' . $class);
    });

    /* Sets the connection */
    $pdo = new wPdo('mysql:host=localhost;dbname=testorm', 'root', '');
    $db = new wDb($pdo);
    wOrm::setDataSource($db);

    /* Tests */
    echo 'Database overview :‘;
    echo  »;
    echo ‘- ‘ . Article::count() . ‘ article(s)’;
    echo  »;
    echo ‘- ‘ . Commentaire::count() . ‘ commentaire(s)’;
    echo  »;
    echo ‘- ‘ . Tag::count() . ‘ tag(s)’;
    echo  »;

    $article = Article::findOne(array(‘id’ => 1));

    echo  »;
    echo ‘L\’article intitulé « ‘;
    echo $article->getTitle();
    echo ‘ » comporte ‘;
    echo $article->getNbCommentaires();
    echo ‘ commentaire(s).’;

    $article->save();
    echo  »;
    echo  »;

    echo ‘Create a new \’Commentaire\’ :‘;
    echo  »;

    $comment = new Commentaire();
    $comment->author = ‘William’;
    $comment->message = ‘Hi ! How are you ? That\’s pretty cool ! =P’;
    $comment->is_published = 1;
    $comment->article_id = $article->getId();
    $comment->save();

    echo ‘!t took the id : ‘ . $comment->getId() . ‘.’;

    echo  »;
    echo  »;
    echo ‘Gonna retrieve it…‘;
    echo  »;

    $a_comment = Commentaire::findOne(array(‘id’ => $comment->getId()));
    echo $a_comment->getMessage() . ‘ by ‘ . $a_comment->getAuthor() . ‘.’;

    echo  »;
    echo  »;
    echo ‘Playing with relations :‘;
    echo  »;
    echo ‘Adds two comments to our article (1-N relation) : how many comments has it ?’;
    echo  »;

    $comment2 = new Commentaire();
    $comment2->author = ‘William (again)’;
    $comment2->message = ‘Hi ! This comment will not be registered until the article be saved.’;
    $comment2->is_published = 1;
    $comment2->article_id = $article->getId();
    $comment2->save();

    echo ‘Before: ‘ . $article->getNbCommentaires();
    echo  »;

    $article->setCommentaires(array($comment, $comment2));

    echo ‘After: ‘ . $article->getNbCommentaires();
    echo  »;
    echo  »;
    echo ‘Comment\’s article :’;
    echo  »;
    echo  »;

    foreach($article->getCommentaires() as $c)
    {
    echo  » . $c->getAuthor() . ‘ said ‘ . $c->getMessage() .  »;
    }

    echo  »;
    echo  »;
    echo ‘And now ?‘;
    echo  »;
    echo $article->getTitle();
    echo ‘- ‘ . Article::count() . ‘ article(s)’;
    echo  »;
    echo ‘- ‘ . Commentaire::count() . ‘ commentaire(s)’;
    echo  »;
    echo ‘- ‘ . Tag::count() . ‘ tag(s)’;
    echo  »;
    echo  »;

    $article->save();

    ?>

    A l’appel de save(), j’ai les erreurs suivante :
    -Warning: Attempt to assign property of non-object in C:\wamp\www\testORM\core\model\orm\wOrm.class.php on line 458
    -Fatal error: Call to a member function save() on a non-object in C:\wamp\www\testORM\core\model\orm\wOrm.class.php on line 459

    Que se passe t’il ?
    Merci d’avance

    • Le 25 mai 2011 à 23 h 49 min | Permalien

      Bonjour,

      As tu repris ce code : https://github.com/willdurand/tests-orm

      Will.

      • Nicolas
        Le 26 mai 2011 à 21 h 47 min | Permalien

        Oui j’ai pris la dernière version sur github, impossible de sauvegarder un objet auquel on a rajouté des relations.

        • Le 5 juillet 2011 à 12 h 56 min | Permalien

          Je ne vois pas. Je tenterai de regarder..

          Edit: J’ai fait quelques modifs sur le Github, j’ai eu des problèmes sous environnement Linux, due à la casse principalement :)
          Ce devrait être ok.

    • Nazih
      Le 13 juin 2011 à 19 h 05 min | Permalien

      Bonjour,
      j’utilise votre ‘framework’ pour un petit projet d’étude mais j’ai une erreur que je n’arrive pas à comprendre. j’ai un form pour uploader un fichier au demarrage du form je n’ai pas d’erreur mais une fois je fais parcourir et je click sur ulpoader j’ai l’erreur suivante:Fatal error:
      Class ‘modele\configuration\Configuration’ not found in C:\xampp\htdocs\PHProjet2\QUIZZ\qcm.php on line 29

      comme s’il ne trouve plus le chemin de mes classes.

      Comment je peux faire pour résoudre ce problème.

      D’avance je vous en remercie.

      • Le 5 juillet 2011 à 12 h 57 min | Permalien

        Bonjour,

        Il faudrait vérifier l’automoading.

        William

    • Le 5 juillet 2011 à 12 h 53 min | Permalien

      Bonjour,

      L’implémentation est en 5.3 et avec namespaces, je ne peux pas vous aider pour un projet tel que le vôtre.
      Ceci est un concept, pas du code utilisable.

      William

  3. Marcel Bichon
    Le 15 juillet 2011 à 16 h 58 min | Permalien

    Il y a un tout petit bug sur wSqlSelect. Il faut inverser le code et mettre $this->alias = $alias; puis $alias = $this->alias . ‘.’;

    • Le 20 juillet 2011 à 13 h 42 min | Permalien

      ah bin oui, merci..