Home arrow MySQL arrow Page 2 - Building an ORM in PHP

Building ORM Data Mappers - MySQL

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

TABLE OF CONTENTS:
  1. Building an ORM in PHP
  2. Building ORM Data Mappers
By: Alejandro Gervasio
Rating: starstarstarstarstar / 0
November 18, 2011

print this article
SEARCH DEV SHED

TOOLS YOU CAN USE

advertisement

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;

abstract class AbstractMapper implements MapperInterface
{
    protected $_adapter;
    protected $_entityTable;
    protected $_entityClass;
   
    /**
     * 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;

class EntryMapper extends AbstractMapper
{
    protected $_commentMapper;
    protected $_entityTable = 'entries';
    protected $_entityClass = 'Blog\Model\Entry';

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

class CommentMapper extends AbstractMapper
{
    protected $_authorMapper;
    protected $_entityTable = 'comments';
    protected $_entityClass = 'Blog\Model\Comment';

    /**
     * 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;
    }
}

 

(Blog/Model/Mapper/AuthorMapper.php)

<?php

namespace Blog\Model\Mapper;
use Blog\Model;

class AuthorMapper extends AbstractMapper
{
    protected $_entityTable = 'authors';
    protected $_entityClass = 'Blog\Model\Author';

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



 
 
>>> More MySQL Articles          >>> More By Alejandro Gervasio
 

blog comments powered by Disqus
escort Bursa Bursa escort Antalya eskort
   

MYSQL ARTICLES

- Oracle Unveils MySQL 5.6
- MySQL Vulnerabilities Threaten Databases
- MySQL Cloud Options Expand with Google Cloud...
- MySQL 5.6 Prepped to Handle Demanding Web Use
- ScaleBase Service Virtualizes MySQL Databases
- Oracle Unveils MySQL Conversion Tools
- Akiban Opens Database Software for MySQL Use...
- Oracle Fixes MySQL Bug
- MySQL Databases Vulnerable to Password Hack
- MySQL: Overview of the ALTER TABLE Statement
- MySQL: How to Use the GRANT Statement
- MySQL: Creating, Listing, and Removing Datab...
- MySQL: Create, Show, and Describe Database T...
- MySQL Data and Table Types
- McAfee Releases Audit Plugin for MySQL Users

Developer Shed Affiliates

 


Dev Shed Tutorial Topics: