Home arrow PHP arrow Singletons in PHP
PHP

Singletons in PHP


In this first part of a two-part tutorial, I will show you how the use of Singletons can generate a lot of coupling, testing and global-state related issues, which in most cases remain invisible to the eyes of the average developer.

Author Info:
By: Alejandro Gervasio
Rating: 4 stars4 stars4 stars4 stars4 stars / 7
December 05, 2011
TABLE OF CONTENTS:
  1. · Singletons in PHP
  2. · Building a Simple Domain Model

print this article
SEARCH DEVARTICLES

Singletons in PHP
(Page 1 of 2 )

Though in the past they enjoyed both popularity and a certain amount of prestige, without a doubt Singletons have progressively become one of the most evil and despicable villains in object-oriented design. Singletons earned their bad reputation for a reason: bringing them to life requires the programmer to deal at least with a static method. This is simply an elegant masquerade for creating a global access point (which in most cases is mutable as well) throughout an entire application. And we all know that global, mutable access is unquestionably a bad thing that must be avoided at all costs.

Although there's nothing inherently wrong with the need to have one and only one of something (the first example that comes to my mind is a database handler), unfortunately this concept goes hand in hand with the global nature of static methods. This leads directly to the bad reputation that Singletons have these days.

However, this gloomy scenario has a bright side, too: it's relatively easy to remove the global access exposed by Singletons while taking advantage of its "single" part, thanks to the combined implementation of several methodologies, including Dependency Injection, separation of concerns and simple conventions. What's more, the "share-with-nothing" structure of PHP provides developers with a more "relaxed" environment that allows you to get rid of Singletons without generating a profound impact on the way that each layer of an application interacts with the others (unless that you're dealing with legacy code suffering from a serious cases of "Singletonitis").

In the course of this two-part tutorial I'll be using a real-world PHP example to explain how the clever and careful use of the aforementioned programming approaches can be perfect for avoiding the side effects caused by Singletons.

Ready to take the first step of this hopefully didactic journey? Then keep reading!

Building an Extendable PHP Framework

As I just said, one typical use case where Singletons have been utilized widely for years is in the construction of a database adapter. To show you more clearly the potential harm in this scenario, I'm going to create such an adapter. It will be a simple MySQL abstraction class, similar to others shown in some of my PHP articles published here at Developer Shed.

Having explained that, here's the adapter's originating class, along with its associated interface. Check them out: 

(Singletons/Library/Database/DatabaseAdapterInterface.php)

<?php

namespace SingletonsLibraryDatabase;

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 SingletonsLibraryDatabase;

class MysqlAdapter implements DatabaseAdapterInterface
{
    protected $_config = array();
    protected $_link;
    protected $_result;
    protected static $_instance;
   
    /**
     * Get the Singleton instance of the adapter
     */
    public static function getInstance(array $config = array())
    {
        if (!self::$_instance) {
            self::$_instance = new self($config);
        }
        return self::$_instance; 
    }
     
    /**
     * Protected constructor
     */
    protected function __construct(array $config)
    {
        if (count($config) !== 4) {
            throw new InvalidArgumentException('Invalid number of connection parameters.');  
        }
        $this->_config = $config;
    }
   
    /**
     * Prevent to clone the instance of the adapter
     */
    protected function __clone(){}
   
    /**
     * 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();
    }
}

As you can see, the previous "MysqlAdapter" class is effectively a Singleton. Its functionality is built around running all sorts of queries against a specific database, performing SELECTS, INSERTS, UPDATES and DELETES, and a few more things. So far, there's nothing especially difficult to grasp regarding this adapter, so let's make something more useful and use it to hydrate a simple mapping layer, which is made up of the following classes:     

(Singletons/Model/Mapper/AbstractMapper.php)

<?php

namespace SingletonsModelMapper;
use SingletonsLibraryDatabase,
    SingletonsModelCollection,
    SingletonsModel;

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

    /**
     * Constructor
     */
    public function __construct(array $entityOptions = array())
    {
        $this->_adapter = DatabaseMysqlAdapter::getInstance(array(
            'host',
            'user',
            'password',
            'database'
        ));
       
        // 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, 'SampleAppModelAbstractEntity')) {
            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)
    {
        $this->_adapter->select($this->_entityTable, "id = $id");
        if (!$data = $this->_adapter->fetch()) {
            return null;
        }
        return new $this->_entityClass($data);       
    }

    /**
     * Find all the entities that match the specified criteria (or all when no criteria are given)
     */
    public function find($criteria = '')
    {
        $collection = new CollectionEntityCollection;
        $this->_adapter->select($this->_entityTable, $criteria);
        while ($data = $this->_adapter->fetch()) {
            $collection[] = new $this->_entityClass($data);
        }
        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");
    }  
}

 

(Singletons/Model/Mapper/UserMapper.php)

<?php

namespace SingletonsModelMapper;

class UserMapper extends AbstractMapper
{
    protected $_entityTable = 'users';
    protected $_entityClass = 'SingletonsModelUser'; 
}

Despite the apparent complexity of the above abstract mapper (and of its subclass "UserMapper"), its driving logic is simple. It only implements some straightforward methods that allow it to insert, save and delete domain objects (still undefined) from a MySQL database.

While this is all well and fine, you should pay close attention to the mapper's constructor, as it's by far the most interesting piece here: effectively, it stores the Singleton instance of the previous MySQL adapter in a class property, which is used in turn to run the aforementioned database operations.

If you're wondering what's wrong with this approach, here's the answer: the mapper is tied to a concrete adapter implementation, instead of favoring the use of an interface, a basic principle of good object-oriented design. Additionally, the instance is grabbed statically within the constructor -- which means if we try to test the mapper in isolation, we need to create the adapter too, as there's no way to mock it up.    

This shows in a nutshell the pitfalls of using a Singleton. Before I demonstrate how to get rid of it, however, let's cultivate our patience a bit further and see how the layers just constructed can be used to manipulate a few user entities.  

To do this, it is first necessary to create a simple domain model, where the entities can live in sweet harmony. This basic model will be built in the next section, so click on the link 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-2014 by Developer Shed. All rights reserved. DS Cluster - Follow our Sitemap
Popular Web Development Topics
All Web Development Tutorials