Home arrow PHP arrow Page 2 - PHP Proxy Patterns

Implementing the blog's data access layer - PHP

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.

TABLE OF CONTENTS:
  1. PHP Proxy Patterns
  2. Implementing the blog's data access layer
By: Alejandro Gervasio
Rating: starstarstarstarstar / 0
October 24, 2011

print this article
SEARCH DEV SHED

TOOLS YOU CAN USE

advertisement

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!



 
 
>>> More PHP Articles          >>> More By Alejandro Gervasio
 

blog comments powered by Disqus
escort Bursa Bursa escort Antalya eskort
   

PHP ARTICLES

- Hackers Compromise PHP Sites to Launch Attac...
- Red Hat, Zend Form OpenShift PaaS Alliance
- PHP IDE News
- BCD, Zend Extend PHP Partnership
- PHP FAQ Highlight
- PHP Creator Didn't Set Out to Create a Langu...
- PHP Trends Revealed in Zend Study
- PHP: Best Methods for Running Scheduled Jobs
- PHP Array Functions: array_change_key_case
- PHP array_combine Function
- PHP array_chunk Function
- PHP Closures as View Helpers: Lazy-Loading F...
- Using PHP Closures as View Helpers
- PHP File and Operating System Program Execut...
- PHP: Effects of Wrapping Code in Class Const...

Developer Shed Affiliates

 


Dev Shed Tutorial Topics: