Building an ORM in PHP: Domain Modeling

If you feel that the features provided by popular ORM packages like Doctrine 2.x or RedBeanPHP are rather like overkill for handling the relationships between the domain objects that comprise your applications, then take a peek at this tutorial. In a step-by-step fashion, you’ll be guided through the development of a simple, extendable ORM, which you’ll be able to tweak to suit your personal requirements and needs.

What’s more, if you’ve already read the first installment of this tutorial, you’re probably familiar with the functionality that I plan to add to this custom ORM. In that first part, I implemented the ORM’s data access and mapping layers. And as you’ll surely recall, the entire implementation process was pretty straightforward and easy to follow.

Of course, in its current state the ORM is still far from a fully-functional structure. We need to add some additional components to it, such as a domain model and the classes responsible for handling collections of entities (remember that the ORM relies heavily on the data mapper pattern to do its business properly).

To fit this requirement, in the lines to come I’ll be showing you the implementation of these domain classes, thus further extending the ORM’s existing functionality.

Meanwhile, if you missed the first part of this series – or need a recap – you can find it here: Building an ORM in PHP.

Extending the ORM’s functionality: modeling domain objects

As I explained at the beginning, the next tier I plan to add to the existing structure of this sample ORM is a domain model. Since my purpose here is to demonstrate how to put the ORM into action by deploying a basic blog program, the model will be composed of four classes. The first one will define the structure and behavior of generic entities, while the remaining three will be tasked with modeling blog entries, comments and authors respectively.

Having said that, here’s the first of these classes:

(Blog/Model/AbstractEntity.php)

<?php

namespace BlogModel;
use BlogModelProxy;

abstract class AbstractEntity
{
    protected $_values = array();
    protected $_allowedFields = array();
   
    /**
     * Constructor
     */
    public function __construct(array $fields)
    {
        foreach ($fields as $name => $value) {
            $this->$name = $value;
        }
    }
   
    /**
     * Assign a value to the specified field via the corresponding mutator (if it exists);
     * otherwise, assign the value directly to the ‘$_values’ protected array
     */
    public function __set($name, $value)
    {  
        if (!in_array($name, $this->_allowedFields)) {
            throw new InvalidArgumentException(‘The field ‘ . $name . ‘ is not allowed for this entity.’); 
        }
        $mutator = ‘set’ . ucfirst($name);
        if (method_exists($this, $mutator) && is_callable(array($this, $mutator))) {
            $this->$mutator($value);          
        }
        else {
            $this->_values[$name] = $value;
        }   
    }
   
    /**
     * Get the value assigned to the specified field via the corresponding getter (if it exists);
     * otherwise, get the value directly from the ‘$_values’ protected array
     */
    public function __get($name)
    {
        if (!in_array($name, $this->_allowedFields)) {
            throw new InvalidArgumentException(‘The field ‘ . $name . ‘ is not allowed for this entity.’);   
        }
        $accessor = ‘get’ . ucfirst($name);
        if (method_exists($this, $accessor) && is_callable(array($this, $accessor))) {
            return $this->$accessor;   
        }
        if (!isset($this->_values[$name])) {
            throw new InvalidArgumentException(‘The field ‘ . $name . ‘ has not been set for this entity yet.’);
        }
        // if the field is a proxy for an entity, load the entity via its ‘load()’ method
        $field = $this->_values[$name];
        if ($field instanceof ProxyEntityProxy) {
            $field = $field->load();
        }
        return $field;
    }

    /**
     * Check if the specified field has been assigned to the entity
     */
    public function __isset($name)
    {
        if (!in_array($name, $this->_allowedFields)) {
            throw new InvalidArgumentException(‘The field ‘ . $name . ‘ is not allowed for this entity.’);
        }
        return isset($this->_values[$name]);
    }

    /**
     * Unset the specified field from the entity
     */
    public function __unset($name)
    {
        if (!in_array($name, $this->_allowedFields)) {
            throw new InvalidArgumentException(‘The field ‘ . $name . ‘ is not allowed for this entity.’);
        }
        if (isset($this->_values[$name])) {
            unset($this->_values[$name]);
            return true;
        }
        return false;
    }
   
    /**
     * Get an associative array with the values assigned to the fields of the entity
     */
    public function toArray()
    {
        return $this->_values;
    }             
}

As seen above, this abstract parent implements, through some magic PHP methods, the logic required to model generic entities. It does nothing more and nothing less. With this base structure up and running, it’s time to define the subclasses that spawn the aforementioned blog entries, comments and authors. Here they are:

(Blog/Model/Entry.php)

<?php

namespace BlogModel;
use BlogModelProxy;

class Entry extends AbstractEntity
{
    protected $_allowedFields = array(
        ‘id’,
        ‘title’,
        ‘content’,
        ‘comments’
    );

    /**
     * Set the entry ID
     */
    public function setId($id)
    {
        if(!filter_var($id, FILTER_VALIDATE_INT, array(‘options’ => array(‘min_range’ => 1, ‘max_range’ => 65535)))) {
            throw new InvalidArgumentException(‘The entry ID is invalid.’);
        }
        $this->_values['id'] = $id;
    }

    /**
     * Set the entry title
     */
    public function setTitle($title)
    {
        if (!is_string($title) || strlen($title) < 2 || strlen($title) > 32) {
            throw new InvalidArgumentException(‘The title of the entry is invalid.’);
        }
        $this->_values['title'] = $title;
    }

    /**
     * Set entry content
     */
    public function setContent($content)
    {
        if (!is_string($content) || empty($content)) {
            throw new InvalidArgumentException(‘The entry is invalid.’);
        }
        $this->_values['content'] = $content;
    }

    /**
     * Set the comments for the blog entry
     * (assigns a Collection Proxy for lazy-loading comments)
     */
    public function setComments(ProxyCollectionProxy $comments)
    {
        $this->_values['comments'] = $comments;
    }
}

 

(Blog/Model/Comment.php)

<?php

namespace BlogModel;
use BlogModelProxy;

class Comment extends AbstractEntity
{
    protected $_allowedFields = array(
        ‘id’,
        ‘content’,
        ‘author’,
        ‘author_id’,
        ‘entry_id’
    );

    /**
     * Set the comment ID
     */
    public function setId($id)
    {
        if(!filter_var($id, FILTER_VALIDATE_INT, array(‘options’ => array(‘min_range’ => 1, ‘max_range’ => 65535)))) {
            throw new InvalidArgumentException(‘The comment ID is invalid.’);
        }
        $this->_values['id'] = $id;
    }

    /**
     * Set the content for the comment
     */
    public function setContent($content)
    {
        if (!is_string($content) || strlen($content) < 2) {
            throw new InvalidArgumentException(‘The comment is invalid.’);
        }
        $this->_values['content'] = $content;
    }

    /**
     * Set the author of the comment
     * (assigns an entity proxy for lazy-loading authors)
     */
    public function setAuthor(ProxyEntityProxy $author)
    {
        $this->_values['author'] = $author;
    }
}

 

(Blog/Model/Author.php)

<?php

namespace BlogModel;

class Author extends AbstractEntity
{
    protected $_allowedFields = array(
        ‘id’,
        ‘name’,
        ‘email’
    );

    /**
     * Set the author ID
     */
    public function setId($id)
    {
        if(!filter_var($id, FILTER_VALIDATE_INT, array(‘options’ => array(‘min_range’ => 1, ‘max_range’ => 65535)))) {
            throw new InvalidArgumentException(‘The ID of the author is invalid.’);
        }
        $this->_values['id'] = $id;
    }

    /**
     * Set the author’s name
     */
    public function setName($name)
    {
        if (!is_string($name) || strlen($name) < 2) {
            throw new InvalidArgumentException(‘The name of the author is invalid.’);
        }
        $this->_values['name'] = $name;
    }

    /**
     * Set the author’s email
     */
    public function setEmail($email)
    {
        if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
            throw new InvalidArgumentException(‘The email address of the author is invalid.’);
        }
        $this->_values['email'] = $email;
    }
}

Definitely, there’s no much that can be said about the above domain classes. They simply implement a few mutators, which come in handy for assigning values (with some constraints, naturally) to the fields of the corresponding entities. Quite possibly, the most interesting detail to note here is the signatures of the “setComments()” and “setAuthor()” methods. These accept proxies instead of real domain objects, as both comments and authors will be lazy-loaded from the database.

Anyway, don’t worry if you still don’t understand how the proxies will fit into the ORM’s structure, as they will be covered in detail in the next part of this tutorial.

So far, so good. With the implementation of the earlier classes, I managed to create a basic domain model, which can be easily extended further to incorporate a few other entities. So, what’s next? Well, since the ORM must be capable of handling one-to-many relationships (like the one that exists between entries and the related comments), it’s necessary to create a class capable of handling collections of entities.

That’s exactly what I’ll be doing in the following segment. Therefore, click on the link that appears below and keep reading. 

{mospagebreak title=Handling Collections of Entities}

In fact, providing the previous domain model with a structure capable of traversing, counting and accessing entities is a straightforward process. Not convinced? Then take a look at the following code bit, which hopefully will make your doubts vanish:

(Blog/Model/Collection/CollectionInterface.php)

<?php

namespace BlogModelCollection;
use BlogModel;

interface CollectionInterface extends Countable, IteratorAggregate, ArrayAccess
{
    public function toArray();

    public function clear();

    public function reset();

    public function add($key, ModelAbstractEntity $entity);
   
    public function get($key);

    public function remove($key);

    public function exists($key);
}

 

(Blog/Model/Collection/EntityCollection.php)

<?php

namespace BlogModelCollection;
use BlogModel;

class EntityCollection implements CollectionInterface
{
    protected $_entities = array();

    /**
     * Constructor
     */
    public function  __construct(array $entities = array())
    {
        $this->_entities = $entities;
        $this->reset();
    }

    /**
     * Get the entities stored in the collection
     */
    public function toArray()
    {
        return $this->_entities;
    }

    /**
     * Clear the collection
     */
    public function clear()
    {
        $this->_entities = array();
    }
    
    /**
     * Rewind the collection
     */    
    public function reset()
    {
        reset($this->_entities);
    }

    /**
     * Add an entity to the collection
     */
    public function add($key, ModelAbstractEntity $entity) {
        return $this->offsetSet($key, $entity);
    }

    /**
     * Get from the collection the entity with the specified key
     */
    public function get($key)
    {
        return $this->offsetGet($key);
    }

    /**
     * Remove from the collection the entity with the specified key
     */
    public function remove($key)
    {
        return $this->offsetUnset($key);
    }

    /**
     * Check if the entity with the specified key exists in the collection
     */
    public function exists($key)
    {
        return $this->offsetExists($key);
    }

    /**
     * Count the number of entities in the collection
     */
    public function count()
    {
        return count($this->_entities);
    }

    /**
     * Get the external array iterator
     */
    public function getIterator()
    {
        return new ArrayIterator($this->toArray());
    }

    /**
     * Add an entity to the collection
     */
    public function offsetSet($key, $entity)
    {
        if (!$entity instanceof ModelAbstractEntity) {
            throw new InvalidArgumentException(‘To add an entity to the collection, it must be an instance of AbstractEntity.’);
        }
        if (!isset($key)) {
            $this->_entities[] = $entity;
        }
        else {
            $this->_entities[$key] = $entity;
        }
        return true;
    }
   
    /**
     * Remove an entity from the collection
     */
    public function offsetUnset($key)
    {
        if ($key instanceof ModelAbstractEntity) {
            $this->_entities = array_filter($this->_entities, function ($v) use ($key) {
                return $v !== $key;
            });
            return true;
        }
        if (isset($this->_entities[$key])) {
            unset($this->_entities[$key]);
            return true;
        }
        return false;
    }
   
    /**
     * Get the specified entity in the collection
     */
    public function offsetGet($key)
    {
        return isset($this->_entities[$key])
            ? $this->_entities[$key] : null;
    } 
   
    /**
     * Check if the specified entity exists in the collection
     */    
    public function offsetExists($key)
    {
        return isset($this->_entities[$key]);
    }
}

There you have it. Even though my previous statement sounded somewhat exaggerated, the truth is that building a class that manipulates collections of entities only requires the creation of a custom array iterator, which implements a very granular interface.

Of course, it’s possible to make the interface even more abstract; to my taste, it reveals too much about what type of object must be passed to its implementers to work as intended. But, for the moment this will be left as homework for you, in case you feel adventurous and want to improve the ORM’s base code.

Final Thoughts

Although at first sight it looks like the implementation of the previous domain model hasn’t influenced the functionality of this custom ORM at all, this is a misleading impression. Still, if you currently think that way, I don’t blame you, because there are still some extra components that must be added to the project to get everything working together in sweet harmony.

In line with this idea, in the final chapter I’ll be incorporating into the ORM the corresponding proxy classes (remember those ones, right?), a simple service layer and some factories. With all of these modules finally working together, you’ll see that manipulating entities via one-to-one and one-to-many relationships will be much simpler that you might think.

Don’t miss the last part!

[gp-comments width="770" linklove="off" ]

chat sex hikayeleri Ensest hikaye