Creating User Models in PHP and MySQL

In this fourth part of a programming tutorial series, you will learn how to create a user model in PHP and MySQL using segregated interfaces.

While the name "segregated interface" may sound like a difficult to grasp concept, and while it is closely related to the most complex facets of object-oriented programming, the truth is that a segregated interface is nothing but a regular interface, which defines a contract that provides its implementers with the exact functionality that they require to perform a set of specific tasks. 

These elements play a great role in the definition of classes that have a narrowed, well-defined range of responsibilities, even when using a weakly-typed language such as PHP. Add to this the benefits of dependency injection, and you’ll start seeing segregated interfaces as a blessing, rather than a waste of time (and code).

In previous parts of this series I developed a couple of basic – yet functional – libraries which made use of some segregated interfaces to easily switch over different implementations at runtime (also known as Polymorphism). If you missed those parts or need a refresher, you can find them here:

The usage of segregated interfaces isn’t limited to creating a few standalone libraries; you can also use them to develop a model class, which perform typical CRUD operations in a MySQL table containing data, as I will be demonstrating in this article. 

Before you move on and begin reading, a word of warning is in order here: to keep the whole example short and easy to follow, the model will be closely tied to the underlying storage (a MySQL database). In fact, I’m not a big fan of this approach, so if you want to create a clean domain model, the example can be easily amended by delegating the CRUD methods to a simple data mapper.

Caching model data: a quick look at the previous cache back-ends

Since I want to provide this sample user model with the ability to cache data that has been previously collected through its finders, I’m going to use a slightly modified version of the cache back-ends developed in the preceding tutorial.

As you may recall, these back-ends implemented a segregated interface called “CacheableInterface”, whose modified definition now looks like this:

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


As you can see, the only difference between the old version of the above interface and this enhanced one is that it now declares a new “clear()” method, which will be implemented by the back-ends for clearing their respective caches.

Having said that, here’re the first class that implements the interface in question:

(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{}


Considering that the “ApcCache” class was already discussed in depth, I’m not going to waste your valuable time explaining what it does. So, move on and look at second cache back-end, which caches data using the file system. Here it is:

(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{}

Again, the inner workings of the “FileCache” class should be pretty familiar to you, as this cache backend was also covered in detail in the previous installment of the series. However, it’s important to note that at this point I managed to create an extendable caching system, which will be used later on by the aforementioned user model.

But I’m getting ahead of myself, since it’s necessary to provide the model with the ability to access the underlying storage mechanism in the first place. Not surprisingly, this will be achieved via a simple MySQL abstraction class, which will make use of a segregated interface as well.    

{mospagebreak title=Building MySQL Abstraction Classes}

Implementing the model’s data access layer: building a MySQL abstraction class

The user model will be able to interact with the storage layer through a basic MySQL accessing class, which implements the following segregated interface. Check it out: 

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


The following class implements the pertaining interface and also is a simple wrapper for the “mysqli” PHP extension:

(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{}


This adapter implements a set of discrete methods (most of them declared in the earlier “DatabaseAdapterInterface” interface), which permit you to perform some common operation in a specified MySQL database, such as executing SELECT, INSERT, UPDATE and DELETE commands, fetching and counting rows, etc.

So far, so good. Having outlined how this abstraction class works, it’s time to leave the boring things behind and face the fun ones. With the earlier caching and data access layers already up and running, the next step that must be taken is… yes, to define the sample user model!

{mospagebreak title=Building the User Model}

Consuming the previous cache and data access layers: building a simple user model

As it was stated at the beginning, the user model that I plan to build in the following lines will be aware of the underlying storage mechanism. While this approach can be pretty useful in relative simple use cases, unfortunately it fails when it comes to separating business logic from infrastructure (a clear example of this is Active Record, but I’ll leave my rant for some other occasion). Anyway, if you feel adventurous and want to write some extra code, it’s fairly easy to construct a domain model and transfer the CRUD operations to an intermediate layer such as a data mapper.

But it’s time to stop talking, and show the definition of my sample model. Here it is: 

(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;
    }   
}


From the above code fragment, it’s clear to see the strong dependency that exists between this user model and the storage layer. However, not all are bad things, as the model’s dependencies are injected in its constructor, something that makes it easier to test it in isolation. What’s more, since this method type hints the couple of segregated interfaces defined before, instead of the concrete classes, it’s really simple to pass to it different database adapters and even several cache back-ends.

Finally, it’s worth to make a brief analysis of the model’s CRUD methods, even though they’re fairly easy to grasp: firstly, there’s a couple of generic finders called “findById()” and “findAll()”, which retrieve a single user (or all of them) from the corresponding MySQL table and save this data to the current cache. And lastly, the “insert()” and “delete()” methods add a new user to the specified MySQL table and remove an existing one respectively.

At this moment, it’s perfectly possible to give the model a try and see how it works. But, considering that to do so you’ll need to create a sample MySQL table, plus a couple of scripts that use the model with different cache back-ends, I suggest you to wait until the next tutorial, where I’ll be doing all this hard work for you. 

Final thoughts

In this penultimate chapter of the series, I showed how to create a functional model, whose collaborators relied on the contracts defined by a couple of segregated interfaces to do their businesses. But hang on a second! To demonstrate that the model is as functional as it seems at first glance, it should be properly tested, right? Don’t you feel concerned, however, as this will be done in the final installment of the series.

Don’t miss the last part!


 



 

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