Building an ORM in PHP

In this first part of the tutorial, I implement the base functionality of a custom ORM (object relational mapper) using PHP.

There’s no shortage of options when it comes to picking a PHP-based ORM. The current variety of options satisfies a broad number of tastes and palates, even the pickiest ones. Similar to some other utility packages available, the prolific nature of ORMs brings with it a healthy effect: whether you use a popular third-party MVC framework like CodeIgniter Reactor, CakePHP or Kohana (only to name a few) or employ your own MVC masterpiece, chances are that you can add the functionality of an ORM to your applications with only minor hassles.

Don’t let the large number of ORMs fool you, though; the development of such packages is very often  challenging and complex. That’s especially true if you’re planning to couple the management of the most common types of relationships (you know, one-to-one, one-to-many and many-to-many) to the functionality provided by enterprise-level design patterns, like Data Mappers, Identity Maps and a Unit of Work.

At this moment, Doctrine 2.x is undeniably the most powerful and full-featured ORM on PHP terrain. It not only implements the aforementioned patterns behind its API; it also provides a solid database abstraction layer. However, other libraries, like RedBeanPHP, dORM and Propel are solid contenders that yield quite impressive results too, so feel free to give them a shot.

Obviously, with so many ORMs at one’s disposal for free, it seems pretty pointless to develop a custom one; are we trying to reinvent the wheel? No, of course not. But if you need to create a simple application that performs a few CRUD operations on some related domain objects and don’t want to climb the learning curve of a third-party library, then implementing a custom ORM might make sense. There’s alos the educational aspect of the process (yes, learning one or two things never hurts).

If this topic has caught your attention, over the course of this tutorial I’ll be building from scratch a small ORM. You’ll be able to customize and extend it at will to fit your personal requirements.

With that said, it’s time to start showing you some concrete and functional code. Let’s get going!

Building a Simple Data Persistence Layer

As you know, in many cases the functionality of an ORM is encapsulated behind a set of classes responsible for handling the relationships between different types of objects, which are persisted in a RDBMS via an Active Record implementation, or in more modern libraries, via the Data Mapper pattern (ala Doctrine 2.x, where domain objects are persistence agnostic).

In this case, for obvious reasons, I’m going to take a much simpler approach: the workhorse of my ORM will be a mapping layer, which will not only move data between a domain model and the storage, but will be capable of reconstituting entities having a one-to-one and one-to-many relationship with other entities (the so-called aggregate roots in Domain-Driven Design).

The first step is to create the layer tasked with interacting with the underlying persistence mechanism. For the sake of simplicity, this will rest on a MySQL database. This layer is simple; its implementation is as follows:

(Blog/Library/Database/DatabaseAdapterInterface.php)

<?php

namespace BlogLibraryDatabase;

interface DatabaseAdapterInterface
{
    function connect();
   
    function disconnect(); 
   
    function query($query);
   
    function fetch(); 
   
    function select($table, $conditions = ”, $fields = ‘*’, $order = ”, $limit = null, $offset = null);
   
    function insert($table, array $data);
   
    function update($table, array $data, $conditions);
   
    function delete($table, $conditions);
   
    function getInsertId();
   
    function countRows();
   
    function getAffectedRows();
}


((Blog/Library/Database/MysqlAdapter.php)

<?php

namespace BlogLibraryDatabase;

class MysqlAdapter implements DatabaseAdapterInterface
{
    protected $_config = array();
    protected $_link;
    protected $_result;
   
    /**
     * Constructor
     */
    public function __construct(array $config)
    {
        if (count($config) !== 4) {
            throw new InvalidArgumentException(‘Invalid number of connection parameters.’);  
        }
        $this->_config = $config;
    }
   
    /**
     * Connect to MySQL
     */
    public function connect()
    {
        // connect only once
        if ($this->_link === null) {
            list($host, $user, $password, $database) = $this->_config;
            if (!$this->_link = @mysqli_connect($host, $user, $password, $database)) {
                throw new RuntimeException(‘Error connecting to the server : ‘ . mysqli_connect_error());
            }
            unset($host, $user, $password, $database);
        }
        return $this->_link;
    }

    /**
     * Execute the specified query
     */
    public function query($query)
    {
        if (!is_string($query) || empty($query)) {
            throw new InvalidArgumentException(‘The specified query is not valid.’);
        }
        // lazy connect to MySQL
        $this->connect();
        if (!$this->_result = mysqli_query($this->_link, $query)) {
            throw new RuntimeException(‘Error executing the specified query ‘ . $query . mysqli_error($this->_link));   
        }
        return $this->_result;
    }
   
    /**
     * Perform a SELECT statement
     */
    public function select($table, $where = ”, $fields = ‘*’, $order = ”, $limit = null, $offset = null)
    {
        $query = ‘SELECT ‘ . $fields . ‘ FROM ‘ . $table
               . (($where) ? ‘ WHERE ‘ . $where : ”)
               . (($limit) ? ‘ LIMIT ‘ . $limit : ”)
               . (($offset && $limit) ? ‘ OFFSET ‘ . $offset : ”)
               . (($order) ? ‘ ORDER BY ‘ . $order : ”);
        $this->query($query);
        return $this->countRows();
    }
   
    /**
     * Perform an INSERT statement
     */ 
    public function insert($table, array $data)
    {
        $fields = implode(‘,’, array_keys($data));
        $values = implode(‘,’, array_map(array($this, ‘quoteValue’), array_values($data)));
        $query = ‘INSERT INTO ‘ . $table . ‘ (‘ . $fields . ‘) ‘ . ‘ VALUES (‘ . $values . ‘)’;
        $this->query($query);
        return $this->getInsertId();
    }
   
    /**
     * Perform an UPDATE statement
     */
    public function update($table, array $data, $where = ”)
    {
        $set = array();
        foreach ($data as $field => $value) {
            $set[] = $field . ‘=’ . $this->quoteValue($value);
        }
        $set = implode(‘,’, $set);
        $query = ‘UPDATE ‘ . $table . ‘ SET ‘ . $set
               . (($where) ? ‘ WHERE ‘ . $where : ”);
        $this->query($query);
        return $this->getAffectedRows(); 
    }
   
    /**
     * Perform a DELETE statement
     */
    public function delete($table, $where = ”)
    {
        $query = ‘DELETE FROM ‘ . $table
               . (($where) ? ‘ WHERE ‘ . $where : ”);
        $this->query($query);
        return $this->getAffectedRows();
    }
   
    /**
     * Escape the specified value
     */
    public function quoteValue($value)
    {
        $this->connect();
        if ($value === null) {
            $value = ‘NULL’;
        }
        else if (!is_numeric($value)) {
            $value = "’" . mysqli_real_escape_string($this->_link, $value) . "’";
        }
        return $value;
    }
   
    /**
     * Fetch a single row from the current result set (as an associative array)
     */
    public function fetch()
    {
        if ($this->_result !== null) {
            if (($row = mysqli_fetch_array($this->_result, MYSQLI_ASSOC)) === false) {
                $this->freeResult();
            }
            return $row;
        }
        return false;
    }

    /**
     * Get the insertion ID
     */
    public function getInsertId()
    {
        return $this->_link !== null
            ? mysqli_insert_id($this->_link) : null; 
    }
   
    /**
     * Get the number of rows returned by the current result set
     */ 
    public function countRows()
    {
        return $this->_result !== null
            ? mysqli_num_rows($this->_result) : 0;
    }
   
    /**
     * Get the number of affected rows
     */
    public function getAffectedRows()
    {
        return $this->_link !== null
            ? mysqli_affected_rows($this->_link) : 0;
    }
   
    /**
     * Free up the current result set
     */
    public function freeResult()
    {
        if ($this->_result === null) {
            return false;
        }
        mysqli_free_result($this->_result);
        return true;
    }
   
    /**
     * Close explicitly the database connection
     */
    public function disconnect()
    {
        if ($this->_link === null) {
            return false;
        }
        mysqli_close($this->_link);
        $this->_link = null;
        return true;
    }
   
    /**
     * Close automatically the database connection when the instance of the class is destroyed
     */
    public function __destruct()
    {
        $this->disconnect();
    }
}
    
If you’ve ever come across some of my PHP articles here at Developer Shed, you may already be familiar with the inner workings of the above “MysqlAdapter” class and the interface that it implements. In either case, this adapter acts like a simple wrapper for the “mysqli” PHP extension, which among other things, runs queries in a specified database, fetches rows and retrieves insertion IDs. It’s nothing really overcomplicated, to be honest.

Nevertheless, I’d like you to notice one subtle detail: the class’s top-level namespace is called “Blog.” I  did this because I plan to demonstrate the functionality of my humble ORM by deploying a blog application. Due to the flexibility of the project, however, the domain model will be easily substitutable, so feel free to switch to the top-level namespace that suits your needs best.

Having clarified that, it’s safe to say that the ORM’s persistence layer is ready to go. Therefore, it’s time to develop a few more components. As I said a moment ago, the main functionality of the ORM will be built around the data mapper pattern. Therefore, in the coming section I’ll be implementing a simple hierarchy of mappers, which will be tasked with fetching, saving to and deleting entities from the aforementioned MySQL database.

To see how these brand new components will be created, click on the link that appears below and keep reading.

{mospagebreak title=Building ORM Data Mappers}

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 BlogModelMapper;

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 BlogModelMapper;
use BlogLibraryDatabase,
    BlogModelCollection;

abstract class AbstractMapper implements MapperInterface
{
    protected $_adapter;
    protected $_entityTable;
    protected $_entityClass;
   
    /**
     * Constructor
     */
    public function __construct(DatabaseDatabaseAdapterInterface $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, ‘BlogModelAbstractEntity’)) {
            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 CollectionEntityCollection;
        $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 BlogModelMapper;
use BlogLibraryDatabase,
    BlogModelProxy;

class EntryMapper extends AbstractMapper
{
    protected $_commentMapper;
    protected $_entityTable = ‘entries’;
    protected $_entityClass = ‘BlogModelEntry’;

    /**
     * Constructor
     */
    public function __construct(DatabaseDatabaseAdapterInterface $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 ProxyCollectionProxy($this->_commentMapper, "entry_id = {$data['id']}")
        ));
        return $entry;
    }
}

 

(Blog/Model/Mapper/CommentMapper.php)

<?php

namespace BlogModelMapper;
use BlogLibraryDatabase,
    BlogModelProxy;

class CommentMapper extends AbstractMapper
{
    protected $_authorMapper;
    protected $_entityTable = ‘comments’;
    protected $_entityClass = ‘BlogModelComment’;

    /**
     * Constructor
     */
    public function __construct(DatabaseDatabaseAdapterInterface $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 ProxyEntityProxy($this->_authorMapper, $data['author_id'])
        ));
        return $comment;
    }
}

 

(Blog/Model/Mapper/AuthorMapper.php)

<?php

namespace BlogModelMapper;
use BlogModel;

class AuthorMapper extends AbstractMapper
{
    protected $_entityTable = ‘authors’;
    protected $_entityClass = ‘BlogModelAuthor’;

    /**
     * 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!

[gp-comments width="770" linklove="off" ]
antalya escort bayan antalya escort bayan