Since the domain model that I’ll be using in the implementation of this sample ORM will be the home and place for entities of a simple blog program, the mapping layer that I plan to construct below will be made up of three concrete mappers (aside from an abstract parent). The first one will deal with blog entries, while the second and third ones will handle comments and authors respectively. Pretty easy to follow, right?
It’s time to show the classes that bring these mappers to life. Here’s the base one, along with its associated interface:
(Blog/Model/Mapper/MapperInterface.php)
<?php
namespace Blog\Model\Mapper;
interface MapperInterface { public function findById($id);
public function find($criteria = '');
public function insert($entity);
public function update($entity);
public function delete($entity); }
(Blog/Model/Mapper/AbstractMapper.php)
<?php
namespace Blog\Model\Mapper; use Blog\Library\Database, Blog\Model\Collection;
/** * Constructor */ public function __construct(Database\DatabaseAdapterInterface $adapter, array $entityOptions = array()) { $this->_adapter = $adapter; // set the entity table is the option has been specified if (isset($entityOptions['entityTable'])) { $this->setEntityTable($entityOtions['entityTable']); } // set the entity class is the option has been specified if (isset($entityOptions['entityClass'])) { $this->setEntityClass($entityOtions['entityClass']); } // check the entity options $this->_checkEntityOptions(); }
/** * Check if the entity options have been set */ protected function _checkEntityOptions() { if (!isset($this->_entityTable)) { throw new \RuntimeException('The entity table has not been set yet.'); } if (!isset($this->_entityClass)) { throw new \RuntimeException('The entity class has been not set yet.'); } }
/** * Get the database adapter */ public function getAdapter() { return $this->_adapter; }
/** * Set the entity table */ public function setEntityTable($entityTable) { if (!is_string($table) || empty($entityTable)) { throw new \InvalidArgumentException('The entity table is invalid.'); } $this->_entityTable = $entityTable; return $this; }
/** * Get the entity table */ public function getEntityTable() { return $this->_entityTable; }
/** * Set the entity class */ public function setEntityClass($entityClass) { if (!is_subclass_of($entityClass, 'Blog\Model\AbstractEntity')) { throw new \InvalidArgumentException('The entity class is invalid.'); } $this->_entityClass = $entityClass; return $this; }
/** * Get the entity class */ public function getEntityClass() { return $this->_entityClass; }
/** * Find an entity by its ID */ public function findById($id) { $this->_adapter->select($this->_entityTable, "id = $id"); if ($data = $this->_adapter->fetch()) { return $this->_createEntity($data); } return null; }
/** * Find entities according to the given criteria (all entities will be fetched if no criteria are specified) */ public function find($conditions = '') { $collection = new Collection\EntityCollection; $this->_adapter->select($this->_entityTable, $conditions); while ($data = $this->_adapter->fetch()) { $collection[] = $this->_createEntity($data); } return $collection; }
/** * Insert a new entity in the storage */ public function insert($entity) { if (!$entity instanceof $this->_entityClass) { throw new \InvalidArgumentException('The entity to be inserted must be an instance of ' . $this->_entityClass . '.'); } return $this->_adapter->insert($this->_entityTable, $entity->toArray()); }
/** * Update an existing entity in the storage */ public function update($entity) { if (!$entity instanceof $this->_entityClass) { throw new \InvalidArgumentException('The entity to be updated must be an instance of ' . $this->_entityClass . '.'); } $id = $entity->id; $data = $entity->toArray(); unset($data['id']); return $this->_adapter->update($this->_entityTable, $data, "id = $id"); }
/** * Delete one or more entities from the storage */ public function delete($id, $col = 'id') { if ($id instanceof $this->_entityClass) { $id = $id->id; } return $this->_adapter->delete($this->_entityTable, "$col = $id"); }
/** * Reconstitute an entity with the data retrieved from the storage (implementation delegated to concrete mappers) */ abstract protected function _createEntity(array $data); }
Although the definition of the above abstract mapper looks somewhat lengthy and complicated, it really isn't. Simply put, the functionality of this base class is focused on running some CRUD operations with generic entities, which is achieved by injecting the previous database adapter into the mapper’s constructor.
With this abstract parent doing the leg work with generic domain objects, it’s fairly easy to subclass the concrete mappers that specifically handle blog entries, comments and authors. Here’s how these ones look:
(Blog/Model/Mapper/EntryMapper.php)
<?php
namespace Blog\Model\Mapper; use Blog\Library\Database, Blog\Model\Proxy;
/** * Constructor */ public function __construct(Database\DatabaseAdapterInterface $adapter, CommentMapper $commentMapper) { $this->_commentMapper = $commentMapper; parent::__construct($adapter); }
/** * Get the comment mapper */ public function getCommentMapper() { return $this->_commentMapper; }
/** * Delete an entry from the storage (deletes the related comments also) */ public function delete($id, $col = 'id') { parent::delete($id); return $this->_commentMapper->delete($id, 'entry_id'); }
/** * Create an entry entity with the supplied data * (assigns a collection proxy to the 'comments' field for lazy-loading comments) */ protected function _createEntity(array $data) { $entry = new $this->_entityClass(array( 'id' => $data['id'], 'title' => $data['title'], 'content' => $data['content'], 'comments' => new Proxy\CollectionProxy($this->_commentMapper, "entry_id = {$data['id']}") )); return $entry; } }
(Blog/Model/Mapper/CommentMapper.php)
<?php
namespace Blog\Model\Mapper; use Blog\Library\Database, Blog\Model\Proxy;
/** * Constructor */ public function __construct(Database\DatabaseAdapterInterface $adapter, AuthorMapper $authorMapper) { $this->_authorMapper = $authorMapper; parent::__construct($adapter); }
/** * Get the author mapper */ public function getAuthorMapper() { return $this->_authorMapper; }
/** * Create a comment entity with the supplied data * (assigns an entity proxy to the 'author' field for lazy-loading authors) */ protected function _createEntity(array $data) { $comment = new $this->_entityClass(array( 'id' => $data['id'], 'content' => $data['content'], 'author' => new Proxy\EntityProxy($this->_authorMapper, $data['author_id']) )); return $comment; } }
/** * Create an author entity with the supplied data */ protected function _createEntity(array $data) { $author = new $this->_entityClass(array( 'id' => $data['id'], 'name' => $data['name'], 'email' => $data['email'] )); return $author; } }
The entire picture is becoming much more interesting now. As shown above, the concrete mappers are refined implementations of the abstract parent, which can reconstitute blog entries, related comments and authors via their “_createEntity()” method. With the author mapper, the logic of the method is reduced to populating a plain domain object and returning it to client code for further use; things are quite different with the other two. If you look more closely at them, you’ll see that they inject proxy objects for lazy loading comments and authors from the database (if you’re interested in learning how to use proxies for lazy-loading data, feel free to take a peek at the article I wrote on the topic at the link).
While the functionality of proxies should be quite clear to you, even though their originating classes haven’t been shown yet, at this point you must be wondering where the portion of code that sets up the relationships between the involved domain objects is. Well, this functionality is implicitly implemented in the earlier mappers also.
Still not clear enough? Then go ahead and check them out again (don’t worry, I’ll wait). Effectively, the entry mapper defines a one-to-many relationship with the comment mapper, while the comment mapper sets up a one-to-one relationship with the author mapper. Got it now? Good.
Of course, it’s possible to define more complex relationships via metadata files (usually in XML, YAML or even plain arrays). In this case, dependency injection is the driving force that allows you to create relationships (at least the most common ones) between entities, which are neatly encapsulated behind a few straightforward mappers.
I'm not saying that this approach can scale up well in the development of large projects, but if you use to build small/mid-size applications, a set of data mappers like the ones shown before might fit the bill quite decently.
Closing Remarks
In this first part of the tutorial, I managed to implement the base functionality of my custom ORM. While the persistence and mapping layers are already up and running, there are still some additional modules that need to be implemented. These include the domain model, the corresponding proxies and some dependency injection containers.
However, part of this laborious work will be done in the upcoming installment, so don’t miss it!