Programming and Technology
RSS icon Home icon
  • Domain Model ORM Adapters

    Posted on November 24th, 2009 brandon 1 comment

    So I’ve had a series of articles that focus on domain model programming methodologies and most of my examples have demonstrated implementations using the Doctrine ORM (Object Relational Mapper). Given the nature of domain model programming I’ve held to the assertion that models built using this method are truly ORM agnostic and it should be effortless to swap out Doctrine for the ORM of your choice. Well I like to put my money where my mouth is by showing how easily this has been done by showing off adapters that I have written, one for Doctrine, and the other for Zend_Db. The beauty of this system is that both support the same ORM agnostic query syntax demonstrated in my recent post about creating complex sql queries using associative arrays. This means that no modifications are necessary in the models, controllers or views when switching ORMs. Developer’s utopia.

    I will be building upon the foundation I have laid in past articles so you may want to catch up:
    Domain Model Programming with the Zend Framework
    Doctrine, Complex SQL Queries, and Paginators

    First we will revisit the Model Gateway base class which all model gateways will extend. This implements a gateway interface to ensure compliance for our paginator adapter from the previous articles.

    1.  
    2. <?php
    3. abstract class FP_Model_Gateway_Doctrine_Adapter extends FP_Model_Gateway_Abstract {
    4.  
    5.     protected $_dao_class = null;
    6.     protected $_model_class = null;
    7.     protected $_model_collection_class = null;
    8.  
    9.     public function create(array $pData=array(), $pAutoSave=false, $pAutoSaveValidate=true){
    10.         $model = new $this->_model_class($pData, $this);
    11.         if ($pAutoSave)
    12.             $model->save($pAutoSaveValidate);
    13.         return $model;
    14.     }
    15.  
    16.     public function save(array $pData){
    17.         if (!is_null($pData[$this->_primary_key])){
    18.             $_record = Doctrine_Query::create()
    19.             ->from("{$this->_dao_class} o")
    20.             ->where("o.{$this->_primary_key} = ?", $pData[$this->_primary_key])
    21.             ->fetchOne();
    22.         } else
    23.             $_record = new $this->_dao_class();
    24.  
    25.         if ($_record){
    26.             $_record->fromArray($pData);
    27.             $_record->save();
    28.         } else
    29.             throw new Exception(‘Invalid record id’);
    30.  
    31.         return $_record->toArray();
    32.     }
    33.  
    34.     public function delete($pId){
    35.         return Doctrine_Query::create()
    36.             ->delete("{$this->_dao_class} o")
    37.             ->where("o.{$this->_primary_key} = ?", $pId)
    38.             ->execute();
    39.     }
    40.  
    41.     public function count($pCriteria=null){
    42.         if (is_array($pCriteria)){
    43.             $q = Doctrine_Query::create()
    44.             ->select("COUNT(o.{$this->_primary_key})")
    45.             ->from("{$this->_dao_class} o");
    46.  
    47.             $q = $this->buildQuery($pCriteria, $q);
    48.             logdebug($q->getSqlQuery() . ‘ – ‘ . logobject($q->getParams()));
    49.             return $q->execute(array(), Doctrine::HYDRATE_SINGLE_SCALAR);
    50.         } else {
    51.             $q = Doctrine_Query::create()
    52.             ->select("COUNT(o.{$this->_primary_key})")
    53.             ->from("{$this->_dao_class} o");
    54.             return $q->execute(array(), Doctrine::HYDRATE_SINGLE_SCALAR);
    55.         }
    56.     }
    57.  
    58.     public function fetch($pCriteria=null, $pOffset=null, $pLimit=null, $pOrderBy=null){
    59.         if (is_numeric($pCriteria)){
    60.  
    61.             $_record = Doctrine_Query::create()
    62.             ->from("{$this->_dao_class} o")
    63.             ->where("o.{$this->_primary_key} = $pCriteria")
    64.             ->fetchOne();
    65.  
    66.             if ($_record)
    67.                 return new $this->_model_class($_record->toArray(), $this);
    68.         } else if (is_array($pCriteria)){
    69.  
    70.             $q = Doctrine_Query::create()
    71.             ->from("{$this->_dao_class} o");
    72.  
    73.             $q = $this->buildQuery($pCriteria, $q, $pOffset, $pLimit, $pOrderBy);
    74.  
    75.             logdebug($q->getSqlQuery() . ‘ – ‘ . logobject($q->getParams()));
    76.             $_records = $q->execute();
    77.             return new $this->_model_collection_class($_records->toArray(), $this);
    78.         } else {
    79.  
    80.             $q = Doctrine_Query::create()
    81.             ->from("{$this->_dao_class} o");
    82.  
    83.             $q = $this->buildQuery(null, $q, $pOffset, $pLimit, $pOrderBy);
    84.  
    85.             logdebug($q->getSqlQuery() . ‘ – ‘ . logobject($q->getParams()));
    86.             $_records = $q->execute();
    87.             return new $this->_model_collection_class($_records->toArray(), $this);
    88.         }
    89.     }
    90.  
    91.     protected function buildQuery($pCriteria, $query, $pOffset=null, $pLimit=null, $pOrderBy=null){
    92.         $joined = array();
    93.  
    94.         $first = true;
    95.         if ($pCriteria && count($pCriteria) > 0){
    96.             foreach ($pCriteria as $key => $val){
    97.                 if (preg_match(‘/^!.*/’, $key)){
    98.                     $key = preg_replace(‘/^!/’, , $key);
    99.                     $negate = true;
    100.                 } else
    101.                     $negate = false;
    102.  
    103.                 if ($first)
    104.                     $clause_func = ‘where’;
    105.                 else {
    106.                     if (preg_match(‘/^\|\|.*/’, $key)){
    107.                         $clause_func = ‘orWhere’;
    108.                         $key = preg_replace(‘/^\|\|/’, , $key);
    109.                     } else
    110.                         $clause_func = ‘andWhere’;
    111.                 }
    112.                 if (preg_match(‘/^sql:/’, $key)){ //Raw sql where condition
    113.                     $query = call_user_func(array($query, $clause_func), $val);
    114.  
    115.                 } else if (preg_match(‘/^%.*%$/’, $key)){ //LIKE
    116.                     $key = preg_replace(‘/(^%|%$)/’, , $key);
    117.  
    118.                     if (preg_match(‘/\./’, $key)){ //for joins
    119.                         $keyparts = explode(‘.’, $key);
    120.                         if (!array_key_exists($keyparts[0], $joined)){
    121.                             $alias = "j" . strval(count($joined) + 1);
    122.                             $joined[$keyparts[0]] = $alias;
    123.                             $query = $query->leftJoin("o.{$keyparts[0]} $alias");
    124.                         } else
    125.                             $alias = $joined[$keyparts[0]];
    126.  
    127.                         $key = "{$alias}.{$keyparts[1]}";
    128.                     }
    129.  
    130.                     if ($negate)
    131.                         $query = call_user_func(array($query,$clause_func), "$key NOT LIKE ?", ‘%’ . $val . ‘%’);
    132.                     else
    133.                         $query = call_user_func(array($query,$clause_func), "$key LIKE ?", ‘%’ . $val . ‘%’);
    134.  
    135.                 } else if (preg_match(‘/^\(.*\)$/’, $key)){ //IN
    136.                     $key = preg_replace(‘/(^\(|\)$)/’, , $key);
    137.  
    138.                     if (preg_match(‘/\./’, $key)){ //for joins
    139.                         $keyparts = explode(‘.’, $key);
    140.                         if (!array_key_exists($keyparts[0], $joined)){
    141.                             $alias = "j" . strval(count($joined) + 1);
    142.                             $joined[$keyparts[0]] = $alias;
    143.                             $query = $query->leftJoin("o.{$keyparts[0]} $alias");
    144.                         } else
    145.                             $alias = $joined[$keyparts[0]];
    146.  
    147.                         $key = "{$alias}.{$keyparts[1]}";
    148.                     }
    149.  
    150.                     if ($negate)
    151.                         $query = call_user_func(array($query,$clause_func), "$key NOT IN ?", array($val));
    152.                     else
    153.                         $query = call_user_func(array($query,$clause_func), "$key IN ?", array($val));
    154.  
    155.                 } else { //Comparison
    156.                     if (preg_match(‘/\./’, $key)){
    157.                         $keyparts = explode(‘.’, $key);
    158.                         if (!array_key_exists($keyparts[0], $joined)){
    159.                             $alias = "j" . strval(count($joined) + 1);
    160.                             $joined[$keyparts[0]] = $alias;
    161.                             $query = $query->leftJoin("o.{$keyparts[0]} $alias");
    162.                         } else
    163.                             $alias = $joined[$keyparts[0]];
    164.  
    165.                         $key = "{$alias}.{$keyparts[1]}";
    166.                     }
    167.  
    168.                     if ($negate){
    169.                         if (is_null($val))
    170.                             $query = call_user_func(array($query,$clause_func), "$key IS NOT NULL");
    171.                         else
    172.                             $query = call_user_func(array($query,$clause_func), "$key != ?", $val);
    173.                     } else {
    174.                         if (is_null($val))
    175.                             $query = call_user_func(array($query,$clause_func), "$key IS NULL");
    176.                         else
    177.                             $query = call_user_func(array($query,$clause_func), "$key = ?", $val);
    178.                     }
    179.                 }
    180.  
    181.                 $first = false;
    182.             }
    183.         }
    184.         if ($pOffset)
    185.             $query = $query->offset($pOffset);
    186.  
    187.         if ($pLimit)
    188.             $query = $query->limit($pLimit);
    189.  
    190.         if ($pOrderBy){
    191.             if (preg_match(‘/\./’, $pOrderBy)){
    192.                 if (preg_match(‘/\s+/’, trim($pOrderBy))){
    193.                 $parts = explode(‘ ‘, $pOrderBy);
    194.                 $field_parts = explode(‘.’, $parts[0]);
    195.                 } else{
    196.                 $parts = null;
    197.                 $field_parts = explode(‘.’, $pOrderBy);
    198.                 }
    199.  
    200.                 if (!array_key_exists($field_parts[0], $joined)){
    201.                 $alias = "j" . strval(count($joined) + 1);
    202.                 $joined[$field_parts[0]] = $alias;
    203.                 $query = $query->leftJoin("o.{$field_parts[0]} $alias");
    204.                 } else
    205.                 $alias = $joined[$field_parts[0]];
    206.                 $sort = ($parts) ? $parts[1] : ;
    207.                 $query = $query->orderBy("{$alias}.{$field_parts[1]} $sort");
    208.             } else
    209.                 $query = $query->orderBy($pOrderBy);
    210.         }
    211.  
    212.         return $query;
    213.     }
    214.  
    215.     public function getObjectId($pObject){
    216.         if (is_numeric($pObject))
    217.             return $pObject;
    218.         else if (get_class($pObject) == $this->_model_class)
    219.             return $pObject->{$this->_primary_key};
    220.         else
    221.             throw new Exception("Must specify a valid {$this->_model_class}");
    222.     }
    223.  
    224.     public function getObject($pObject){
    225.         if (is_numeric($pObject))
    226.             return $this->fetch($pObject);
    227.         else if (get_class($pObject) == $this->_model_class)
    228.             return $pObject;
    229.         else
    230.             throw new Exception("Must specify a valid {$this->_model_class}");
    231.     }
    232.  
    233.     //Accepts collection of doctrine rows and converts them to model collection
    234.     public function getCollection($pCollection){
    235.         return new $this->_model_collection_class($pCollection->toArray(), $this);
    236.     }
    237. }
    238. ?>
    239.  

    We’ve seen this class before in the previous article. The bulk of the code is centered around building queries using Doctrine’s DQL query language adapted from my associative array syntax. The algorithm has been upgraded and tweaked since so there are some minor differences but for the most part it is the same. One major change is how primary keys are referenced. Early on I used ‘id’ as the primary key by convention but as I wrote the ZendDb adapter, I ran into some roadblocks that made this an unwise assumption that everyone would use this so I made the primary key name configurable in the base class. This is the class that application model gateways will extend should the developer prefer to use Doctrine for the ORM.

    For example:

    1.  
    2. <?php
    3. class App_Users extends FP_Model_Gateway_Doctrine_Adapter {
    4.     protected $_dao_class = ‘App_Dao_User’;
    5.     protected $_model_class = ‘AppUser’;
    6.     protected $_model_collection_class = ‘AppUsers’;
    7.     protected $_primary_key = ‘user_id’;
    8. }
    9. ?>
    10.  

    And voila, you’re done with your model gateway for your AppUser model. This simple class commands the full might of CRUD for the Doctrine ORM.

    Now let’s see the ZendDb adapter.

    1.  
    2. <?php
    3.  
    4. abstract class FP_Model_Gateway_ZendDb_Adapter extends FP_Model_Gateway_Abstract {
    5.  
    6.     protected $_dao_class = null;
    7.     protected $_model_class = null;
    8.     protected $_model_collection_class = null;
    9.  
    10.     protected function getTable(){
    11.         return new $this->_dao_class();
    12.     }
    13.  
    14.     protected function getTableName(){
    15.         $table = $this->getTable();
    16.         return $table->getRealTableName();
    17.     }
    18.  
    19.     protected function getDb(){
    20.         return Zend_Db_Table_Abstract::getDefaultAdapter();
    21.     }
    22.  
    23.     public function create(array $pData=array(), $pAutoSave=false, $pAutoSaveValidate=true){
    24.         $model = new $this->_model_class($pData, $this);
    25.         if ($pAutoSave)
    26.             $model->save($pAutoSaveValidate);
    27.         return $model;
    28.     }
    29.  
    30.     public function save(array $pData){
    31.         $table = $this->getTable();
    32.        
    33.         if (!is_null($pData[$this->_primary_key])){
    34.             $_record = $table->get($pData[$this->_primary_key]);
    35.         } else
    36.             $_record = $table->createRow();
    37.  
    38.         if ($_record){
    39.             $_record->setFromArray($pData);
    40.             $_record->save();
    41.         } else
    42.             throw new Exception(‘Invalid record id’);
    43.  
    44.         return $_record->toArray();
    45.     }
    46.  
    47.     public function delete($pId){
    48.         $record_id = $this->getObjectId($pId);
    49.         $db = $this->getDb();
    50.         $db->delete($this->getTableName(), "{$this->_primary_key} = $record_id");
    51.     }
    52.  
    53.     public function count($pCriteria=null){
    54.         $db = $this->getDb();
    55.         if (is_array($pCriteria)){
    56.             $select = $this->getTable()->select()->from($this->getTableName(), ‘COUNT(*)’);
    57.             $select = $this->buildQuery($pCriteria, $select);
    58.             return $db->fetchOne($select);         
    59.         } else
    60.             return $db->fetchOne($this->getTable()->select()->from($this->getTableName(), ‘COUNT(*)’));
    61.     }
    62.  
    63.     public function fetch($pCriteria=null, $pOffset=null, $pLimit=null, $pOrderBy=null){
    64.         $table = $this->getTable();
    65.         if (is_numeric($pCriteria)){
    66.             $_record = $table->get($pCriteria);
    67.             if ($_record)
    68.                 return new $this->_model_class($_record->toArray(), $this);
    69.         } else if (is_array($pCriteria)){
    70.             $select = $table->select()->from($table->getRealTableName());
    71.             $select = $this->buildQuery($pCriteria, $select, $pOffset, $pLimit, $pOrderBy);
    72.  
    73.             $_records = $table->fetchAll($select);
    74.             return new $this->_model_collection_class($_records->toArray(), $this);
    75.         } else {
    76.             $select = $table->select();
    77.             $select = $this->buildQuery(null, $select, $pOffset, $pLimit, $pOrderBy);
    78.             $_records = $table->fetchAll($select);
    79.             return new $this->_model_collection_class($_records->toArray(), $this);
    80.         }
    81.     }
    82.  
    83.     protected function buildQuery($pCriteria, $query, $pOffset=null, $pLimit=null, $pOrderBy=null){
    84.         $joined = array();
    85.  
    86.         $query = $query->setIntegrityCheck(false);
    87.  
    88.         $first = true;
    89.         if ($pCriteria && count($pCriteria) > 0){
    90.             foreach ($pCriteria as $key => $val){
    91.                 if (preg_match(‘/^!.*/’, $key)){
    92.                     $key = preg_replace(‘/^!/’, , $key);
    93.                     $negate = true;
    94.                 } else
    95.                     $negate = false;
    96.  
    97.                 if ($first)
    98.                     $clause_func = ‘where’;
    99.                 else {
    100.                     if (preg_match(‘/^\|\|.*/’, $key)){
    101.                         $clause_func = ‘orWhere’;
    102.                         $key = preg_replace(‘/^\|\|/’, , $key);
    103.                     } else
    104.                         $clause_func = ‘where’;
    105.                 }
    106.  
    107.                 if (preg_match(‘/^sql:/’, $key)){ //Raw sql where condition
    108.                     $query = call_user_func(array($query, $clause_func), $val);
    109.                 } else if (preg_match(‘/^%.*%$/’, $key)){ //LIKE
    110.                     $key = preg_replace(‘/(^%|%$)/’, , $key);
    111.  
    112.                 if (preg_match(‘/\./’, $key)){ //for joins
    113.                     $keyparts = explode(‘.’, $key);
    114.                     if (!array_key_exists($keyparts[0], $joined)){
    115.                         $alias = "j" . strval(count($joined) + 1);
    116.                         $joined[$keyparts[0]] = $alias;
    117.                         $joinClass = $keyparts[0];
    118.                         $joinTable = new $joinClass();
    119.                         $query = $query->joinUsing($joinTable->getRealTableName(), $joinTable->getPrimaryKeyName());
    120.                     } else
    121.                         $alias = $joined[$keyparts[0]];
    122.  
    123.                     $key = $joinTable->getRealTableName() . ".{$keyparts[1]}";
    124.                 }
    125.  
    126.                     if ($negate)
    127.                         $query = call_user_func(array($query,$clause_func), "$key NOT LIKE ?", ‘%’ . $val . ‘%’);
    128.                     else
    129.                         $query = call_user_func(array($query,$clause_func), "$key LIKE ?", ‘%’ . $val . ‘%’);
    130.  
    131.                 } else if (preg_match(‘/^\(.*\)$/’, $key)){ //IN
    132.                     $key = preg_replace(‘/(^\(|\)$)/’, , $key);
    133.  
    134.                     if (preg_match(‘/\./’, $key)){ //for joins
    135.                     $keyparts = explode(‘.’, $key);
    136.                     if (!array_key_exists($keyparts[0], $joined)){
    137.                         $alias = "j" . strval(count($joined) + 1);
    138.                         $joined[$keyparts[0]] = $alias;
    139.                         $joinClass = $keyparts[0];
    140.                         $joinTable = new $joinClass();
    141.                         $query = $query->joinUsing($joinTable->getRealTableName(), $joinTable->getPrimaryKeyName());
    142.                     } else
    143.                         $alias = $joined[$keyparts[0]];
    144.  
    145.                     $key = $joinTable->getRealTableName() . ".{$keyparts[1]}";
    146.                 }
    147.                     if ($negate)
    148.                         $query = call_user_func(array($query,$clause_func), "$key NOT IN (" . implode(‘,’, $val) . ")");
    149.                     else
    150.                         $query = call_user_func(array($query,$clause_func), "$key IN (". implode(‘,’,$val) .")");
    151.  
    152.                 } else { //Comparison
    153.                     if (preg_match(‘/\./’, $key)){ //for joins
    154.                         $keyparts = explode(‘.’, $key);
    155.                         if (!array_key_exists($keyparts[0], $joined)){
    156.                             $alias = "j" . strval(count($joined) + 1);
    157.                             $joined[$keyparts[0]] = $alias;
    158.                             $joinClass = $keyparts[0];
    159.                             $joinTable = new $joinClass();
    160.                             $query = $query->joinUsing($joinTable->getRealTableName(), $joinTable->getPrimaryKeyName());
    161.                         } else
    162.                             $alias = $joined[$keyparts[0]];
    163.  
    164.                         $key = $joinTable->getRealTableName() . ".{$keyparts[1]}";
    165.                     }
    166.  
    167.                     if (preg_match(‘/.+(<|>|<=|>=)$/’, $key, $matches)){
    168.                         $key = preg_replace(‘/(<|>|<=|>=)$/’, , $key);
    169.                         $op = $matches[1];
    170.  
    171.                         $query = call_user_func(array($query, $clause_func), "$key $op $val");
    172.                        
    173.                     } else {
    174.                         if ($negate){
    175.                             if (is_null($val))
    176.                                 $query = call_user_func(array($query,$clause_func), "$key IS NOT NULL");
    177.                             else
    178.                                 $query = call_user_func(array($query,$clause_func), "$key != ?", $val);
    179.                         } else {
    180.                             if (is_null($val))
    181.                                 $query = call_user_func(array($query,$clause_func), "$key IS NULL");
    182.                             else
    183.                                 $query = call_user_func(array($query,$clause_func), "$key = ?", $val);
    184.                         }
    185.                     }
    186.                 }
    187.  
    188.                 $first = false;
    189.  
    190.             }
    191.         }
    192.        
    193.         if ($pOffset)
    194.             $query = $query->offset($pOffset);
    195.  
    196.         if ($pLimit)
    197.             $query = $query->limit($pLimit);
    198.  
    199.         if ($pOrderBy){
    200.             if (preg_match(‘/\./’, $pOrderBy)){
    201.                 if (preg_match(‘/\s+/’, trim($pOrderBy))){
    202.                     $parts = explode(‘ ‘, $pOrderBy);
    203.                     $field_parts = explode(‘.’, $parts[0]);
    204.                 } else{
    205.                     $parts = null;
    206.                     $field_parts = explode(‘.’, $pOrderBy);
    207.                 }
    208.  
    209.                 if (!array_key_exists($field_parts[0], $joined)){
    210.                     $alias = "j" . strval(count($joined) + 1);
    211.                     $joined[$field_parts[0]] = $alias;
    212.                     $joinClass = $field_parts[0];
    213.                     $joinTable = new $joinClass();
    214.                     $query = $query->joinUsing($joinTable->getRealTableName(), $joinTable->getPrimaryKeyName());
    215.                 } else
    216.                     $alias = $joined[$field_parts[0]];
    217.                     $sort = ($parts) ? $parts[1] : ;
    218.                     $query = $query->order($joinTable->getRealTableName() . ".{$field_parts[1]} $sort");
    219.             } else
    220.                 $query = $query->order($pOrderBy);
    221.         }
    222.  
    223.         logdebug(‘FP_Model_Gateway_ZendDb_Adapter.buildQuery’, "Query=" . $query->__toString());
    224.  
    225.         return $query;
    226.     }
    227.  
    228.     public function getObjectId($pObject){
    229.         if (is_numeric($pObject))
    230.             return $pObject;
    231.         else if (get_class($pObject) == $this->_model_class)
    232.             return $pObject->{$this->_primary_key};
    233.         else
    234.             throw new Exception("Must specify a valid {$this->_model_class}");
    235.     }
    236.  
    237.     public function getObject($pObject){
    238.         if (is_numeric($pObject))
    239.             return $this->fetch($pObject);
    240.         else if (get_class($pObject) == $this->_model_class)
    241.             return $pObject;
    242.         else
    243.             throw new Exception("Must specify a valid {$this->_model_class}");
    244.     }
    245.  
    246.     //Accepts collection of doctrine rows and converts them to model collection
    247.     public function getCollection($pCollection){
    248.         return new $this->_model_collection_class($pCollection->toArray(), $this);
    249.     }
    250.  
    251. }
    252. ?>
    253.  

    This is an identical implementation of the same query builder functionality. You can see that instead of converting our queries into DQL we are now converting them into queries through Zend_Db_Select objects. The application level gateway class is none-the-wiser and requires no modification when switching ORMs.

    I should inject a minor footnote/caveat here with the ZendDb adapter however, due to the difference in how joins are handled by ZendDb, this adapter makes one minor assumption through necessity. In order to mimic the join alias functionality that Doctrine supports, joins are done using joinUsing() which assumes the local/foreign key both share the same name. This differs from Doctrine which allows the keys to be different. Keep this in mind and you should have no trouble using join queries.

    Using ORM adapters may seem a little bit overkill from the perspective we have added an abstraction layer to another abstraction layer but it is a necessary step to acheive true separation of business logic and the persistence layer. Additionally regardless of the ORM used, data models can remain modular and re-usable across projects. Supporting new ORMs is simply a matter of writing a new adapter.

    • Share/Bookmark
     

    One response to “Domain Model ORM Adapters”

    1. [...] platform. This particular example builds gateway classes that extend my Doctrine ORM Adapter (Read about that here) so this is unlikely to be a turnkey drop in solution but it’s really easy to write your own [...]

    Leave a reply

Get Adobe Flash playerPlugin by wpburn.com wordpress themes