PHP Proxy Patterns

In this first part of a three-part tutorial, I introduce you to what proxy objects are and how they can be used for lazy-loading domain objects in PHP. In this case, I will show you how to use proxies in the development of a blog application, to fetch on request the comments associated with a given post.

There’s no doubt that the most challenging facet in the development of an MVC-based web application, even the simplest one, is the implementation of the model(s). While controllers and views are inherently relevant pieces within the pattern’s schema, they can be created to display a more relaxed behavior (at least to some extent), and even work decently upon a poorly-designed infrastructure, be it a full framework or a set of independent libraries.

On the other hand, the model is a pristine and carefully-crafted creature, responsible for the application’s state. Since it plays such a crucial role, it must be conceived from the very beginning to follow well-defined business rules and constraints. These rules and constraints must naturally be independent of the underlying storage mechanism.

Despite this, sooner or later the model must be persisted in some form. At first glance, this looks like a fairly straightforward process. In reality, however, it presents some notorious difficulties, especially when it’s necessary to deal with a batch of interrelated domain objects (or aggregated roots, in Domain-Driven Design’s jargon).

Most of the time, the persistence layer sits on a RDBMS (even though it might be a document-oriented database such as MongoDB). This point implies that retrieving a bunch of related domain objects can be a very expensive and sometimes unnecessary task. Consider the typical blog posts/comments relationship, and you’ll clearly see my point.

Of course, this common issue can be easily addressed via lazy-loading. You’re probably familiar with this approach, and know that it can be implemented in all sorts of forms and flavors. However, in domain models composed of a few entities which have a certain number of restrictions, lazy-loading can be a bit harder to implement than usual, because normally the piece connecting the domain and data access layers is a set of data mappers.

The functionality of a data mapper is built in most cases around a group of generic finders and a few other complementary methods that perform CRUD operations in the database. It’s somewhat difficult to make a mapper differentiate between records that should be lazy-loaded and records that shouldn’t. Fortunately, the logic required to achieve this can be neatly encapsulated behind a proxy object – an object that acts like a placeholder for the actual domain object(s). In fact, lazy-loading is just one use case where the implementation of proxy objects can be of great help; they can also be used in other  situations, such as controlling other objects, unit testing and a few more.

In this tutorial, I’ll show you how to exploit the functionality of proxies to lazy-load the domain objects of a basic — yet extendable — PHP blog application. Keep in mind that the program will be built in a step-by-step fashion, due to the extensive number of sample classes that I plan to define from this point onward. With that said, bear with me and be ready to learn how to create proxy objects in PHP.

Now, let’s get going!         

Taking the first step: building the blog’s domain model

As I said in the introduction, before you see proxy objects lazy-loading records from a blog’s database,  it’s necessary to create the layers upon which the application will be seated.

The first layer that I plan to set up is the domain model. It will be composed of a few blog entries and their associated comments. Here’s the abstract class that I’ll be using later on for modeling the aforementioned entities:  

(MyApp/Model/AbstractEntity.php)

<?php

namespace MyAppModel;

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’ array
     */
    public function __set($name, $value)
    {
        if (!in_array($name, $this->_allowedFields)) {
            throw new InvalidArgumentException("Setting 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 of the specified field (via the getter if it exists or by getting it from the $_values array)
     */
    public function __get($name)
    {
        if (!in_array($name, $this->_allowedFields)) {
            throw new InvalidArgumentException("Getting 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])) {
            return $this->_values[$name];  
        }
        throw new InvalidArgumentException("The field ‘$name’ has not been set for this entity yet.");   
    }
         
    /**
     * 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("Unsetting the field ‘$name’ is not allowed for this entity.");
        }
        if (isset($this->_values[$name])) {
            unset($this->_values[$name]);
            return true;
        }
        throw new InvalidArgumentException("The field ‘$name’ has not been set for this entity yet.");
    }

    /**
     * Get an associative array with the values assigned to the fields of the entity
     */
    public function toArray()
    {
        return $this->_values;
    }   
}

Even though the implementation of the above "AbstractEntity" class seems to be rather complex at first glance, this is a misleading impression. In reality, this class only defines the structure and constraints of generic entities, which can be assigned and removed properties via the magic "__set()", "__get()" and "__unset()" PHP methods (or through the corresponding mutators/getters if they’ve been explicitly implemented). 

With this abstract parent already set, the next step is to create a couple of subclasses, responsible for generating blog entries and comments.

The class below is the blueprint for the former:
 
(MyApp/Model/Entry.php)

<?php

namespace MyAppModel;
use MyAppCommon;

class Entry extends AbstractEntity
{  
    protected $_allowedFields = array(‘id’, ‘title’, ‘content’, ‘author’, ‘comments’);
   
    /**
     * Set the entry ID
     */
    public function setId($id)
    {
        if (!filter_var($id, FILTER_VALIDATE_INT, array(‘options’ => array(‘min_range’ => 1, ‘max_range’ => 999999)))) {
            throw new InvalidArgumentException(‘The entry ID is invalid.’);
        }
        $this->_values['id'] = $id;
    }
   
    /**
     * Set the title of the entry
     */ 
    public function setTitle($title)
    {
        if (!is_string($title) || strlen($title) < 2 || strlen($title) > 64) {
            throw new InvalidArgumentException(‘The title of the entry is invalid.’);
        }
        $this->_values['title'] = $title;
    }
   
    /**
     * Set the content of the entry
     */
    public function setContent($content)
    {
        if (!is_string($content) || strlen($content) < 2) {
            throw new InvalidArgumentException(‘The entry is invalid.’);
        }
        $this->_values['content'] = $content;
    }
   
    /**
     * Set the author of the entry
     */
    public function setAuthor($author)
    {
        if (!is_string($author) || strlen($author) < 2 || strlen($author) > 64) {
            throw new InvalidArgumentException(‘The author of the entry is invalid.’);
        }
        $this->_values['author'] = $author;
    }
   
    /**
     * Set the comments of the entry (assigns a collection proxy for lazy-loading comments)
     */
    public function setComments(CommonCollectionProxy $comments)
    {
        $this->_values['comments'] = $comments;
    }                                                                         
}

As you can see above, the "Entry" subclass is nothing but a refined implementation of its abstract parent, which assigns some typical fields to a blog entry via a bunch of mutators, including an ID, the title, content, author and the related comments. While this is fairly easy to digest, you should pay attention to the signature of the class’ "setComments()" method. It deserves special analysis: effectively, instead of taking something more banal like a plain string, the method accepts a collection proxy object.

Even though the class that spawns this kind of object hasn’t been defined yet, the method should give you a clearer idea of how a proxy can be used for lazy-loading database records. In this case, the proxy is used as a mediator (or a stand-in) for the actual comment objects, which encapsulates all the logic required for fetching them on request from the storage layer.

But let’s slow down a second and not get ahead of ourselves, as the implementation of the corresponding proxy classes will be discussed in depth in the next tutorial. So, for the moment take a look at the following domain class. It’s tasked with modeling the aforementioned comment objects:      

(MyApp/Model/Comment.php)

<?php

namespace MyAppModel;

class Comment extends AbstractEntity
{  
    protected $_allowedFields = array(‘id’, ‘content’, ‘author’, ‘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’ => 999999)))) {
            throw new InvalidArgumentException(‘The comment ID is invalid.’);
        }
        $this->_values['id'] = $id;       
    }
   
    /**
     * Set the comment content
     */
    public function setContent($content)
    {
        if (!is_string($content) || strlen($content) < 2) {
            throw new InvalidArgumentException(‘The comment is invalid.’);
        }
        $this->_values['content'] = $content;
    }
   
    /**
     * Set the comment author
     */
    public function setAuthor($author)
    {
        if (!is_string($author) || strlen($author) < 2 || strlen($author) > 64) {
            throw new InvalidArgumentException(‘The author of the comment is invalid.’);
        }
        $this->_values['author'] = $author;
    }                                                                    
}

Admittedly, this class is a no-brainer; all of its functionality is focused on modeling the structure and constraints of blog post comments. Similar to its counterpart "Entry," the "Comment" class uses a few mutators for setting the ID, content and author of a comment, a process that’s a breeze to grasp.

So far, so good. With the implementation of this last subclass, the domain model corresponding to this sample blog application is finally up and running. Therefore, the next step is to set up the blog’s data access layer, which will be charged with persisting the pertinent model in a couple of MySQL tables. 

This brand new layer will be covered in the next segment, so click the link that appears below and keep reading.

{mospagebreak title=Implementing the blog’s data access layer}

To be frank, the functionality of the blog’s data access layer will be somewhat limited. Its building blocks will include only a segregated interface, along with a basic MySQL adapter class. That’s about it.

Moreover, if you’ve read some of my PHP articles published here at the Developer Shed Network, you may find this layer familiar, as I’ve used it before with a few other topics.

Having clarified that, here’s the layer in question:  

(MyApp/Library/Database/DatabaseAdapterInterface.php)

<?php

namespace MyAppLibraryDatabase;

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

 

(MyApp/Library/Database/MysqlAdapter.php)

<?php

namespace MyAppLibraryDatabase;

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
     */
    public function fetch($mode = MYSQLI_ASSOC)
    {
        if ($this->_result === null) {
            return false;  
        }
        if (!in_array($mode, array(MYSQLI_NUM, MYSQLI_ASSOC, MYSQLI_BOTH))) {
            $mode = MYSQLI_ASSOC;
        }
        if (($row = mysqli_fetch_array($this->_result, $mode)) === false) {
            $this->freeResult();
        }
        return $row;
    }

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

I don’t want to be too verbose about the goodies offered by the above abstraction class; it’s a simple adapter that performs common database tasks, such as running CRUD operations, fetching table rows, finding insertion IDs and so forth. However, its functionality is more than enough for our purposes: implementing an extendable data access layer, which allows us to persist the previous domain model in MySQL.

But how can this be done? Considering that the model and the DAL have been neatly isolated from each other, there must be an additional layer that allows data to move easily between them. Not surprisingly, this extra structure will be a mapping layer, and in the following section I’ll be showing you the set of classes that will make it up. 

Moving data between the domain and data access layers: creating some data mappers

Similar to the domain model developed a few moments ago, the development of the blog’s mapping layer will be set around three different classes. The first one will be an abstract mapper, which not surprisingly, will encapsulate all the functionality shared by concrete mappers.

If you want to see how this base mapper looks, here it is:

(MyApp/Model/Mapper/AbstractMapper.php)

<?php

namespace MyAppModelMapper;
use MyAppLibraryDatabase,
    MyAppCommon;

abstract class AbstractMapper
{
    protected $_adapter;
    protected $_entityTable;

    /**
     * Constructor
     */
    public function __construct(DatabaseDatabaseAdapterInterface $adapter, $entityTable = null)
    {
        $this->_adapter = $adapter;
        // Set the entity table if the option has been specified
        if (isset($entityTable)) {
            $this->setEntityTable($entityTable);
        }
        // check if the entity table has been set
        if (!isset($this->_entityTable)) {
            throw new InvalidArgumentException(‘The entity table 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;
    }
     
    /**
     * Get the entity table
     */
    public function getEntityTable()
    {
        return $this->_entityTable;
    }
       
    /**
     * 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 all the entities that match the specified criteria (or all when no criteria are given)
     */
    public function find($criteria = ”)
    {
        $collection = new CommonEntityCollection;
        $this->_adapter->select($this->_entityTable, $criteria);
        while ($data = $this->_adapter->fetch()) {
            $collection[] = $this->_createEntity($data);
        }
        return $collection;
    }
   
    /**
     * Reconstitute the corresponding entity with the supplied data
     * (implementation delegated to child mappers)
     */
    abstract protected function _createEntity(array $fields);  
}

From the previous code fragment, it’s clear that the implementation of this abstract mapper is pretty standard. The class defines a couple of finders, which can be used to retrieve entities from the database, either by specifying their IDs or other conditions (don’t worry for now about the "find()" method, since processing entity collections will be discussed later).

Having defined a generic mapper that retrieves data from the storage layer, it’s time to create the concrete ones responsible for dealing with blog entries and comments respectively. Here’s the first of these subclasses:  

(MyApp/Model/Mapper/EntryMapper.php)

<?php

namespace MyAppModelMapper;
use MyAppLibraryDatabase,
    MyAppModel,
    MyAppCommon;

class EntryMapper extends AbstractMapper
{
    protected $_commentMapper;
    protected $_entityTable = ‘entries’;
   
    /**
     * Constructor
     */
    public function __construct(DatabaseDatabaseAdapterInterface $adapter, CommentMapper $commentMapper)
    {
        $this->_commentMapper = $commentMapper;
        parent::__construct($adapter);
    }
   
    /**
     * Get the comment mapper
     */
    public function getCommentMapper()
    {
        return $this->_commentMapper;    
    }
     
    /**
     * Insert a new entry
     */
    public function insert(ModelEntry $entry)
    {
        return $this->_adapter->insert($this->_entityTable, $entry->toArray());
    }
   
    /**
     * Delete an existing entry (the related comments are deleted also)
     */
    public function delete($id)
    {
        if ($id instanceof ModelEntry) {
            $id = $id->id;
        }
        $this->_adapter->delete($this->_entityTable, "id = $id");
        return $this->_commentMapper->delete($id, $col = ‘entry_id’);
    }
       
    /**
     * Create an entry entity with the supplied data
     * (the ‘comments’ field is filled with a collection proxy for lazy-loading comments)
     */
    protected function _createEntity(array $fields)
    {
        return new ModelEntry(array(
            ‘id’       => $fields['id'],
            ‘title’    => $fields['title'],
            ‘content’  => $fields['content'],
            ‘author’   => $fields['author'],
            ‘comments’ => new CommonCollectionProxy($this->_commentMapper, "entry_id = {$fields['id']}")
        ));
    }    
}

Although the definition of the earlier "Entry" mapper is rather short, it performs a few tasks worth noting. First and foremost, it overrides its parent’s constructor to take an additional dependency — in this case, the mapper that operates with comments. If you’re wondering why I decided to do this, look at the "delete()" method, which not only removes a specified entry from the storage, but uses the mentioned mapper to delete the associated comments, too. Got that point? Good.

Finally, be sure to check the "_createEntity()" method. As you can see, aside from reconstituting a blog entry object with its ID, title, content and author, the method also assigns a collection proxy object to the "comments" field.

As I explained before, this is done to lazy-load the related comments from the database. But, if you’re an insightful observer, you’ll have noticed that the proxy uses the comment mapper to achieve this. Note, however, that the implementation of proxies and other blog components will be covered in the next part of this tutorial.

Meanwhile, take a look at the class below. It defines the mapper that works with comment objects:      

(MyApp/Model/Mapper/CommentMapper.php)

<?php

namespace MyAppModelMapper;
use MyAppLibraryDatabase,
    MyAppModel,
    MyAppCommon;

class CommentMapper extends AbstractMapper
{
    protected $_entityTable = ‘comments’;
   
    /**
     * Insert a new comment
     */
    public function insert(ModelComment $comment)
    {
        return $this->_adapter->insert($this->_entityTable, $comment->toArray());
    }
   
    /**
     * Delete an existing comment
     */
    public function delete($id, $col = ‘id’)
    {
        if ($id instanceof ModelComment) {
            $id = $id->id;
        }
        return $this->_adapter->delete($this->_entityTable, "$col = $id");
    }
    
    /**
     * Create a comment entity with the supplied data
     */
    protected function _createEntity(array $fields)
    {
        return new ModelComment(array(
            ‘id’       => $fields['id'],
            ‘content’  => $fields['content'],
            ‘author’   => $fields['author']
        ));
    }    
}

Certainly, this one is much simpler to grasp than its partner "EntryMapper," so I’m sure that you won’t have major troubles understanding how it does its thing. And best of all, after showing you how this concrete mapper looks, the blog’s mapping layer is finally set and ready to be put into action.

Of course, there are a few additional steps that must be taken before you see the functionality of proxy objects in the context of this sample blog program. We’ll continue this in the next part. 

Closing Thoughts

That’s all for now. In this first installment of the tutorial, I provided you with a quick introduction to what proxy objects are and how they can be used to lazy-load domain objects in PHP. In this particular case, I decided to take the longer road and show you how to use proxies in the development of a blog application, to fetch on request the comments associated with a given post.

The program in its current state isn’t fully functional yet. It’s necessary to build a few other components, including collections of entities, views, and of course, the proxies themselves. But, don’t get concerned, as the implementation of all these additional elements will be covered in the next part.

Don’t miss it!

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