Working with User Models in PHP and MySQL

Being one of the big pillars of Object-Oriented Design, The Interface Segregation Principle (ISP) permits you to define fine-grained contracts that provide client classes with the functionality to perform a set of well-defined tasks. As with other SOLID principles (Single responsibility, Open-closed, Liskov substitution, Interface segregation and Dependency inversion), ISP is language-agnostic, meaning that it can be applied in several programming languages, including PHP.

In keeping with this concept, in earlier chapters of this series I demonstrated how to use this principle to develop a few basic scripts, such as an extendable registry library, a caching system capable of using different behind the scenes cache back-ends, and even a simple model layer, whose focus was to perform CRUD operations in a MySQL table containing data on some hypothetical users.

This last example is still in an incomplete state, as it’s necessary to prove if the model is really as functional as it seems at first sight. In this last installment of the series I’m going to create a couple of scripts which will show how to put this sample user model to work, first by using the prior APC cache-backend and lastly via its counterpart “FileCache” class.

If you missed the prior installments in this series or need to refresh your memory, you can find them here:

Recap time: showing the source code developed so far

To begin with, here’s the segregated interface implemented by the couple of cache-back-ends using the APC extension and the file system respectively:  

(CacheableInterface.php)

<?php

interface CacheableInterface
{
    public function set($key, $data);
   
    public function get($key);
   
    public function delete($key);
   
    public function exists($key);
   
    public function clear();    
}


Without a doubt, understanding the contract declared by the “CacheableInterface” interface is fairly straightforward. Keeping that in mind, let’s look at its implementers. The first one is a simple wrapper for the APC extension and its source code is as follows:

(ApcCache.php)

<?php

class ApcCache implements CacheableInterface
{
    /**
     * Save data to the cache
     */
    public function set($key, $data)
    {
        if (!apc_store(strtolower($key), $data)) {
            throw new ApcCacheException('Error saving data with the key ' . $key . ' to the APC cache.');
        }
        return $this;
    }
   
    /**
     * Get the specified data from the cache
     */
    public function get($key)
    {
        if ($this->exists($key)) {
            if (!$data = apc_fetch(strtolower($key))) {
                throw new ApcCacheException('Error fetching data with the key ' . $key . ' from the APC cache.');
            }
            return $data;
        }
        return null;
    }
   
    /**
     * Delete the specified data from the cache
     */
    public function delete($key)
    {
        if ($this->exists($key)) {
            if (!apc_delete(strtolower($key))) {
                throw new ApcCacheException('Error deleting data with the key ' . $key . ' from the APC cache.');
            }
            return true;
        }
        return false;
    }
   
    /**
     * Check if the specified cache key exists
     */
    public function exists($key)
    {
        return (boolean) apc_exists(strtolower($key));
    }
   
    /**
     * Clear the cache
     */
    public function clear($cacheType = 'user')
    {
        return apc_clear_cache($cacheType);
    }       
}



(ApcCacheException.php)

<?php

class ApcCacheException extends Exception{}

The “ApcCache” class acts like a proxy for the most relevant APC functions, whichs allow you to save, retrieve and clear opcode with minor effort.

Pay attention to the following class, whose functionality is quite similar to the one provided by the APC back-end, only that in this case it uses the file system to cache data:

(FileCache.php)

<?php

class FileCache implements CacheableInterface
{
    protected $_cacheDir = 'cache';
   
    /**
     * Constructor
     */
    public function __construct($cacheDir = '')
    {
        if ($cacheDir !== '') {
            if (!is_dir($cacheDir)) {
                throw new FileCacheException('The specified cache directory is invalid.');
            }
            $this->_cacheDir = $cacheDir;
        }
    }
   
    /**
     * Save data to the specified cache file
     */
    public function set($key, $data)
    {
        $cacheFile = $this->getCacheFile($key);
        if (!file_put_contents($cacheFile, serialize($data))) {
            throw new FileCacheException('Error saving data with the key ' . $key . ' to the cache file.');
        }
        return $this;
    }
   
    /**
     * Get data from the specified cache file
     */
    public function get($key)
    {
        if ($this->exists($key)) {
            $cacheFile = $this->getCacheFile($key);
            if (!$data = unserialize(file_get_contents($cacheFile))) {
                throw new FileCacheException('Error reading data with the key ' . $key . ' from the cache file.');
            }
            return $data;
        }
        return null;
    }
   
    /**
     * Delete the specified cache file
     */
    public function delete($key)
    {
        if ($this->exists($key)) {
            $cacheFile = $this->getCacheFile($key);
            if (!unlink($cacheFile)) {
                throw new FileCacheException('Error deleting the file cache with key ' . $key);
            }
            return true;
        }
        return false;
    }
   
    /**
     * Check if the specified cache file exists
     */
    public function exists($key)
    {
        $cacheFile = $this->getCacheFile($key);
        return file_exists($cacheFile);
    }
   
    /**
     * Remove all the cache files
     */
    public function clear()
    {
        $cacheFiles = $this->_cacheDir . DIRECTORY_SEPARATOR . '*.cache';
        array_map('unlink', glob($cacheFiles));
    }
     
    /**
     * Get the specified cache file
     */
    protected function getCacheFile($key)
    {
        return $this->_cacheDir . DIRECTORY_SEPARATOR . strtolower($key) . '.cache';
    }     
}



(FileCacheException.php)

<?php

class FileCacheException extends Exception{}

If you found this code easy to grasp, then you shouldn’t have any problem understanding how the earlier “FileCache” back-end does its thing. What you should be aware of, however, is that both classes implement the same segregated interface, something that makes the whole caching layer extremely easy to extend. 

And speaking of layers, here’s the one responsible for interacting with the storage mechanism used by the user model. Take a peek at it:

(DatabaseAdapterInterface.php)

<?php

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



(MySQLAdapter.php)

<?php

class MySQLAdapter implements DatabaseAdapterInterface
{
    protected $_config = array();
    protected $_link;
    protected $_result;
   
    /**
     * Constructor
     */
    public function __construct(array $config)
    {
        if (count($config) !== 4) {
            throw new MySQLAdapterException('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 MySQLAdapterException('Error connecting to MySQL : ' . mysqli_connect_error());
            }
            unset($host, $user, $password, $database);      
        }
    }

    /**
     * Execute the specified query
     */
    public function query($query)
    {
        if (!is_string($query) || empty($query)) {
            throw new MySQLAdapterException('The specified query is not valid.');  
        }
        // lazy connect to MySQL
        $this->connect();
        if (!$this->_result = mysqli_query($this->_link, $query)) {
            throw new MySQLAdapterException('Error executing the specified query ' . $query . mysqli_error($this->_link));
        }
    }
   
    /**
     * 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();
    }
   
    /**
     * Single quote 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_object($this->_result))) {
                $this->freeResult();
                return false;
            }
            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) {
            mysqli_free_result($this->_result);  
        }
    }
   
    /**
     * Close explicitly the database connection
     */
    public function disconnect()
    {
        if ($this->_link !== null) {
            mysqli_close($this->_link);
            $this->_link = null;
        }
    }
   
    /**
     * Close automatically the database connection when the instance of the class is destroyed
     */
    public function __destruct()
    {
        $this->disconnect();
    }
}



(MySQLAdapterException.php)

<?php

class MySQLAdapterException extends Exception{}

That was pretty lengthy, right? There’s no need to feel overwhelmed, though, as the earlier “MySQLAdapter” class is simply a proxy for the “mysqli” PHP extension, which allows you to run SQL statements as well as fetch and count rows in a specified MySQL table.

{mospagebreak title=Performing CRUD Operations in MySQL}

So far, so good. Having already showed the source code corresponding to the caching and data access layers of this sample application, the only thing left to do is to show the definition of the pertaining user model:

(UserModel.php)

<?php

class UserModel
{
    protected $_adapter;
    protected $_cache;
   
    /**
     * Constructor
     */
    public function __construct(DatabaseAdapterInterface $adapter, CacheableInterface $cache)
    {
        $this->_adapter = $adapter;
        $this->_cache = $cache;
    }
   
    /**
     * Fetch a user by their ID
     */
    public function fetchById($id)
    {
        // try to fetch user data from the cache system
        if ($user = $this->_cache->get($id)) {
            return $user;
        }
        // otherwise fecth user data from the database
        $this->_adapter->select('users', "id = $id");
        if ($user = $this->_adapter->fetch()) {
            $this->_cache->set($id, $user);
            return $user;
        }
        return null;
    }
   
    /**
     * Fetch all users
     */
    public function fetchAll()
    {
        // try to fetch users data from the cache system
        if ($users = $this->_cache->get('all')) {
            return $users;
        }
        // otherwise fecth users data from the database
        $this->_adapter->select('users');
        $users = array();
        while ($user = $this->_adapter->fetch()) {
            $users[] = $user;
        }
        if (count($users) !== 0) {
           $this->_cache->set('all', $users);
        }
        return $users;
    }
   
    /**
     * Insert new user data (the cache is cleared also)
     */
    public function insert(array $data)
    {
        if ($insertId = $this->_adapter->insert('users', $data)) {
            $this->_cache->clear();
            return $insertId;
        }
        return false;
    }
   
    /**
     * Delete the table row corresponding to the specified user (the cache is cleared also)
     */
    public function delete($id)
    {
        if ($affectedRow = $this->_adapter->delete('users', "id = $id")) {
            $this->_cache->clear();
            return $affectedRow;
        }
        return false;
    }   
}


Although it’s fair to admit that the previous model is way too coupled to the underlying storage (which isn’t something that I find particularly appealing), its structure is pretty flexible, since the collaborators that it accepts in its constructor can be easily swapped at runtime.

In the next section I’m going to create a script which will use this sample model to fetch, insert and delete users from a sample MySQL table.

Using the model with the APC cache-backend: performing CRUD operations in a sample database

The first example will show how to use the user model to run CRUD operations in a MySQL table. But before you see how the example in question looks, here’s the table that will be queried (indirectly) by the model:



Even though the “users” table only contains a couple of rows, it comes in handy for illustrating the actual functionality of the sample model. What’s more, the following script shows how to take advantage of the model for fetching existing users, then inserting a new one and finally removing a row from the table:

<?php

// example using the ApcCache class

// include the autoloader
require_once 'Autoloader.php';
Autoloader::getInstance();

// create a user model
$userModel = new UserModel(new MySQLAdapter(array('host', 'user', 'password', 'database')), new ApcCache);

// fetch a single user and display their data
$user = $userModel->fetchById(1);


echo ' First Name: ' . $user->fname .
     ' Last Name: ' . $user->lname .
     ' Email: ' . $user->email;

// fetch all the users and display their data
$users = $userModel->fetchAll();
foreach ($users as $user) {
echo ' First Name: ' . $user->fname .
     ' Last Name: ' . $user->lname .
     ' Email: ' . $user->email . '<br />';
}

// insert a new user
$userModel->insert(array(
    'fname' => 'Susan',
    'lname' => 'Norton',
    'email' => 'susan@domain.com'
));

// delete an existing user
$userModel->delete(1);

To keep the whole code sample clean and uncluttered, I haven’t included the definition of the autoloader. This shouldn’t be an obstacle, however, for grasping how the above example works. Once the model has been properly instantiated, it’s utilized to perform the aforementioned CRUD operations in the sample MySQL table.

Aside from this, you should pay close attention to the way the model has been spawned. Effectively, an instance of the APC cache class has passed in to its constructor, which implies that each time a finder is called (be it “findById()” or “findAll()”), the fetched data will be cached in shared memory.

At this time you’ll be wondering if it’s feasible to instruct the model to cache user data using the file system, instead of RAM. Well, the answer is … yes! In the coming section I’m going to redefine the earlier script, which this time will pass to the model’s constructor an instance of the “FileCache” class.

Setting up a final example: using the model with the “FileCache” class

Since the model’s constructor only type hints segregated interfaces, instead of concrete implementations, it’s really simple to use it with different cache back-ends or even with distinct database adapters. To see how this can be accomplished in a snap, take a look at the following script, which looks very similar to the one created before, only that in this case the model caches user data using the file system:

<?php

// example using the FileCache class

// include the autoloader
require_once 'Autoloader.php';
Autoloader::getInstance();

// create a user model
$userModel = new UserModel(new MySQLAdapter(array('host', 'user', 'password', 'database')), new FileCache);

// fetch a single user and display their data
$user = $userModel->fetchById(1);
echo ' First Name: ' . $user->fname .
     ' Last Name: ' . $user->lname .
     ' Email: ' . $user->email;

// fetch all the users and display their data
$users = $userModel->fetchAll();
foreach ($users as $user) {
echo ' First Name: ' . $user->fname .
     ' Last Name: ' . $user->lname .
     ' Email: ' . $user->email . '<br />';
}

// insert a new user
$userModel->insert(array(
    'fname' => 'Susan',
    'lname' => 'Norton',
    'email' => 'susan@domain.com'
));

// delete an existing user
$userModel->delete(1);

There you have it. See how simple is to create a model layer that swaps between several different cache back-ends when fetching data from a given storage? And best of all, most of this flexibility has been achieved thanks to the combined use of segregated interfaces and dependency injection.

Of course, it’s dead simple to make the model accept other caching systems, (i.e. “XCache” and “Memcached”). The beauty of this is that the whole extension process would be reduced to defining a new implementer of the previous “CacheableInterface” interface and nothing else. The model’s source code wouldn’t be changed at all.

So, what are you waiting for? Build your own wrapper for some of the mentioned cache libraries and inject it straight into the model’s internals. It’ll be a pretty instructive experience, as you’ll learn one or two more things on how to take advantage of interface segregation in PHP.

Final thoughts

Unfortunately, we’ve come to the end of this series. But, overall this humble journey has been didactical, as you learned (or at least that was my intention from the very beginning) how to use a few segregated interfaces in the deployment of several PHP projects, ranging from the construction of dynamic registry and a caching system, to the development of a simple model layer that interacts with a MySQL database.

While you should evaluate these examples only by what they are, they come in handy for demonstrating how to create classes whose functionality is achieved thanks to the implementation of fine, highly-granular contracts. What’s more, if you’re still hesitating about using interfaces when building your own object-oriented applications, think twice: even in a weakly-typed language like PHP, design by contract has a lot of sense, even at expenses of writing a few more lines of code. In the end, the payback will be invaluable, trust me.

See you in the next PHP development tutorial!     

 

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