Home arrow PHP arrow Removing Singletons in PHP
PHP

Removing Singletons in PHP


In this conclusion to a two-part tutorial, I demonstrate through a fairly realistic example that the elimination of Singletons (and their associated side effects, of course) is a pretty straightforward process. Indeed, I show that it can be successfully tackled using a proper mixture of different programming methodologies and guidelines, such as Inversion of Control and the Separation of Concerns Principle.

Author Info:
By: Alejandro Gervasio
Rating: 5 stars5 stars5 stars5 stars5 stars / 2
December 08, 2011
TABLE OF CONTENTS:
  1. · Removing Singletons in PHP
  2. · Fetching User Entities from Storage in PHP

print this article
SEARCH DEVARTICLES

Removing Singletons in PHP
(Page 1 of 2 )

Even though they look relatively harmless on the surface, when analyzed in depth, Singletons are in fact much more dangerous to good object-oriented programming design than one might think. Implementing Singletons, remember, demands the use of a static method, which opens the undesirable gates of global access. Singletons also introduce a strong coupling effect with any class that consumes them via its static method. They also break the Single Responsibility Principle, as they’re not only responsible for performing the task that they were designed for (whatever it is), but for controlling class instantiation as well.

As with many other design patterns, the best way to demonstrate the side effects produced by Singletons is by means of some concrete, functional code samples. In the first part of this tutorial I developed a small PHP framework, composed of a simple domain model, a couple of mappers and a persistence layer. With all of these tiers up and running, I showed how the use of a Singleton database adapter (consumed inside the mappers) can be really detrimental in terms of testability and decoupling, due to the aforementioned issues.

Of course, if you’re anything like me, you want to see if it’s possible to refactor some portions of this sample framework to get rid of the infamous Singleton, while keeping its original functionality intact. In the lines to come I'll be showing you the framework’s improved version, which thanks to the proper use of Dependency Injection along with the Separation of Concerns principle will mitigate the effects produced by the database adapter.

Refactoring Components of a Framework

As I noted in the introduction, the use of a Singleton database adapter within my sample framework made a big mess with its structure and behavior. It's time to fix this issue pragmatically. Since the adapter implemented a typical static method called “getInstance(),” the first thing we need to do is remove the method, and eliminate once and for all its Singleton nature.

With that said, here’s the revamped incarnation of the adapter, along with its segregated interface:

(Singletons/Library/Database/DatabaseAdapterInterface.php)

<?php

namespace Singletons\Library\Database;

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

 

(Singletons/Library/Database/MysqlAdapter.php)

<?php

namespace Singletons\Library\Database;

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

There you have it. As shown above, the MySQL adapter isn’t a Singleton anymore; therefore, its constructor has been declared public. Obviously, this means that it’s possible to create any number of instances. But what if we need to have only one instance of it, to avoid multiple databases connections? In that case, the instantiation control can be delegated to a separate factory class, similar to the one shown in the following code bit (keep in mind that the factory also implements a simple interface, but this is optional):

(Singletons/Injector/InjectorInterface.php)

<?php

namespace Singletons\Injector;

interface InjectorInterface
{
    public function create();    
}

 

(Singletons/Injector/MysqlAdapterFactory.php)

<?php

namespace Singletons\Injector;
use Singletons\Library\Database;

class MysqlAdapterFactory implements InjectorInterface
{
    protected static $_adapter;
   
    /**
     * Create the Singleton instance of the MySQL adapter
     */
    public function create()
    {
        if (!self::$_adapter) {
            self::$_adapter = new Database\MysqlAdapter(array(
                'host',
                'user',
                'password',
                'database'
            ));
        }
        return self::$_adapter;
    }    
}

That was simple to code and read, wasn’t it? As you can see, the above “MysqlAdapterFactory” class is a low-level factory responsible for creating a Singleton of the previous database adapter. Of course, the existence of this class is useful as long as it’s being used for controlling the adapter’s instantiation, as the constructor of this latter is public. However, the implementation of this approach allows you to delegate object creation to a specific class, in this way making it possible to define the adapter so that it only performs database-related tasks. This is one big bonus that neatly adheres to the Separation of Concerns paradigm.

And now that you understand why I decided to built the previous factory, it’s time to show the refactored version of the corresponding mappers. Here they are:

(Singletons/Model/Mapper/AbstractMapper.php)

<?php

namespace Singletons\Model\Mapper;
use Singletons\Library\Database,
    Singletons\Model\Collection,
    Singletons\Model;

abstract class AbstractMapper
{
    protected $_adapter;
    protected $_entityTable;
    protected $_entityClass;
    protected $_identityMap = array();

    /**
     * Constructor
     */
    public function __construct(Database\DatabaseAdapterInterface $adapter, array $entityOptions = array())
    {
        $this->_adapter = $adapter;
        // Set the entity table if the option has been specified
        if (isset($entityOptions['entityTable'])) {
            $this->setEntityTable($entityOptions['entityTable']);
        }
        // Set the entity class if the option has been specified
        if (isset($entityOptions['entityClass'])) {
            $this->setEntityClass($entityOptions['entityClass']);
        }
        // check if the entity options have been set
        $this->_checkEntityOptions();
    }
   
    /**
     * Check if the entity options have been set
     */
    protected function _checkEntityOptions()
    {
        // check if the entity table has been set
        if (!isset($this->_entityTable)) {
            throw new \InvalidArgumentException('The entity table has been not set yet.');
        }
        // check if the entity class has been set
        if (!isset($this->_entityClass)) {
            throw new \InvalidArgumentException('The entity class 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($entityTable) || empty($entityTable)) {
            throw new \InvalidArgumentException("The given entity table '$entityTable' is invalid.");
        }
        $this->_entityTable = $entityTable;
       
    }
    
    /**
     * Get the entity table
     */
    public function getEntityTable()
    {
        return $this->_entityTable;
    }
   
    /**
     * Set the entity class
     */
    public function setEntityClass($entityClass)
    {
        if (!is_subclass_of($entityClass, 'Singletons\Model\AbstractEntity')) {
            throw new \InvalidArgumentException("The given entity class '$entityClass' is invalid. It must be a subclass of AbstractEntity.");
        }
        $this->_entityClass = $entityClass;
    }
   
    /**
     * Get the entity class
     */
    public function getEntityClass()
    {
        return $this->_entityClass;
    }
          
    /**
     * Find an entity by its ID
     */
    public function findById($id)
    {
        // return the entity from the identity map when possible
        if (isset($this->_identityMap[$id])) {
            return $this->_identityMap[$id];
        }
        // otherwise, fetch it from the storage
        $this->_adapter->select($this->_entityTable, "id = $id");
        if (!$data = $this->_adapter->fetch()) {
            return null;
        }
        $entity = new $this->_entityClass($data);
        $this->_identityMap[$entity->id] = $entity;
        return $entity;       
    }

    /**
     * Find all the entities that match the specified criteria (or all when no criteria are given)
     */
    public function find($criteria = '')
    {
        $collection = new Collection\EntityCollection;
        $this->_adapter->select($this->_entityTable, $criteria);
        while ($data = $this->_adapter->fetch()) {
            $entity = new $this->_entityClass($data);
            // add each entity to the identity map
            $this->_identityMap[$entity->id] = $entity;
            $collection[] = $entity;
        }
        return $collection;
    }
   
    /**
     * Insert a new row in the table corresponding to the specified entity
     */
    public function insert($entity)
    {
        if (!$entity instanceof $this->_entityClass) {
            throw new \InvalidArgumentException("The entity to be inserted must be an instance of '$entityClass'.");
        }
        return $this->_adapter->insert($this->_entityTable, $entity->toArray());
    }

    /**
     * Delete the row in the table corresponding to the specified entity or ID
     */
    public function delete($id)
    {
        if ($id instanceof $this->_entityClass) {
            $id = $id->id;
        }
        return $this->_adapter->delete($this->_entityTable, "id = $id");
    }
   
    /**
     * Remove an entity from the identity map
     */
    public function removeFromMap($id)
    {
        if (isset($this->_identityMap[$id])) {
            unset($this->_identityMap[$id]);
            return true;
        }
        return false;
    }
     
    /**
     * Clear the identity map
     */
    public function clearIdentityMap()
    {
        $this->_identityMap = array();
    }    
}

 

(Singletons/Model/Mapper/UserMapper.php)

<?php

namespace Singletons\Model\Mapper;

class UserMapper extends AbstractMapper
{
    protected $_entityTable = 'users';
    protected $_entityClass = 'Singletons\Model\User'; 
}

I don't want to sound like I’m bragging here, but the definitions of the above data mappers now look much better. Not only have I added a simple identity map to them (an optional feature, by the way), but I made them accept in the constructor an instance of any class implementing the “DatabaseAdapterInterface” interface (including the earlier MySQL adapter).

This wins a battle on two fronts. First, the mappers rely on an interface instead of on a concrete implementation. And second, they finally get rid of the global access of a Singleton by using clean and uncluttered Dependency Injection. What more can you ask for?

So far, so good. Now that you've grasped how easy it is to eliminate Singletons and their global access, thanks to the combined use of Dependency Injection and separation of concerns, the last step that must be taken is to set up a script that puts the revamped version of this sample framework into action.

This will be done in the following section, so click on the link that appears below and keep reading.


blog comments powered by Disqus
PHP ARTICLES

- Removing Singletons in PHP
- Singletons in PHP
- Implement Facebook Javascript SDK with PHP
- Making Usage Statistics in PHP
- Installing PHP under Windows: Further Config...
- File Version Management in PHP
- Statistical View of Data in a Clustered Bar ...
- Creating a Multi-File Upload Script in PHP
- Executing Microsoft SQL Server Stored Proced...
- Code 10x More Efficiently Using Data Access ...
- A Few Tips for Speeding Up PHP Code
- The Modular Web Page
- Quick E-Commerce with PHP and PayPal
- Regression Testing With JMeter
- Building an Iterator with PHP

Watch our Tech Videos 
Dev Articles Forums 
 RSS  Articles
 RSS  Forums
 RSS  All Feeds
Write For Us 
Weekly Newsletter
 
Developer Updates  
Free Website Content 
Contact Us 
Site Map 
Privacy Policy 
Support 

Developer Shed Affiliates

 




© 2003-2017 by Developer Shed. All rights reserved. DS Cluster - Follow our Sitemap
Popular Web Development Topics
All Web Development Tutorials