PHP Singleton? Not really

If you couldn’t tell by the long silence, things around here have been not so quiet as I had hoped. However, while reading the PHP: Patterns page I came across a large number of implementations of the Singleton pattern.

I happen to like the Singleton pattern, and use it in Java and Python (where the VM maintains the “one and only one” instance) but no so much in PHP.

Why, you ask? It is simply this: you cannot create a true Singleton in a PHP web application. Since every page load is executed in a separate thread, every page load has it’s own instance of the class. Any hope of true Singleton behavior is lost.

As a way to illustrate this, here is a PHP “Singleton” class and an associated PHP page. Throw them up on a test server and hit the page.

Then try to increment the counter. See what happens, I’ll wait.

The class:

<?php
/**
 * test for PHP multi-threaded singleton
 */
class Singleton {
  private static $instance;
  private $counter;

  /**
   * Constructor is private
   */
  private function __construct() {
    $this->counter = 0;
  }

  /**
   * Entry point. Get the static instance of Singleton
   */
  public static function getInstance() {
    if (is_null(self::$instance)) {
      self::$instance = new Singleton();
    }
    return self::$instance;
  }

  public function __clone() {
    trigger_error('Clone not allowed for '.__CLASS__, E_USER_ERROR);
  }

  public function incrementCounter() {
    $this->counter++;
  }

  public function getCounter() {
    return $this->counter;
  }
}
?>

The page:

<?php
include_once('singletontest.php');
$s = Singleton::getInstance();
if (isset($_GET['inc'])) {
  $s->incrementCounter();
}
?>
<html>
<head><title>Multi-threading PHP Singleton? Not Likely</title></head>
<body>
<h3>Singleton test</h3>
<p>The counter is at <?php echo $s->getCounter(); ?></p>
<pre><?php var_dump($s); ?></pre>
<p><a href="<?php echo $_SERVER['PHP_SELF']?>?inc=1">Increment the counter</a></p>
</body>
</html>

In this first version, even within one browser the limitations are clear. The Singleton instance is recreated on every page load. So, what if we serialize our $counter variable to disk? Will that help? Let’s try it.

The modified class:

<?php
/**
 * test for PHP multi-threaded singleton
 */
class Singleton {
  private static $instance;
  private $counter;

  /**
   * Constructor is private
   */
  private function __construct() {
    $init = 0;
    if (file_exists('/tmp/singleton.ser')) {
      $str = file_get_contents('/tmp/singleton.ser');
      $init = unserialize($str);
    }
    $this->counter = $init;
  }

  /**
   * Entry point. Get the static instance of Singleton
   */
  public static function getInstance() {
    if (is_null(self::$instance)) {
      self::$instance = new Singleton();
    }
    return self::$instance;
  }

  public function __clone() {
    trigger_error('Clone not allowed for '.__CLASS__, E_USER_ERROR);
  }

  /**
   * Since PHP does not create "only one" instance globally, but by thread, we
   * need a way to store our instance variables so that each thread is getting
   * the same values.
   * Note that threads holding a version of this will have the old value until
   * they reload the Singleton (by a page refresh, etc).
   */
  public function incrementCounter() {
    // We need to update the serialized value
    $handle = fopen('/tmp/singleton.ser', 'w+');
    // Get an EXCLUSIVE lock on the file to block any other reads/writes while
    // we modify
    if (flock($handle, LOCK_EX)) {
      // Only update the instance variable's value AFTER we have a lock
      $this->counter++;
      // empty the file
      ftruncate($handle, 0);
      // write out the value
      fwrite($handle, serialize($this->counter));
      // and unlock so that everyone else can read the new value
      flock($handle, LOCK_UN);
    } else {
      // You would probably prefer to throw an Exception here
      echo "Couldn't get the lock!";
    }
    fclose($handle);
  }

  public function getCounter() {
    return $this->counter;
  }
}
?>

The modified page:

<?php
include_once('singletontest.php');
$s = Singleton::getInstance();
if (isset($_GET['inc'])) {
  $s->incrementCounter();
} else if (isset($_GET['ext'])) {
  $x = true;
}
?>
<html>
<head><title>Multi-threading PHP Singleton? Not Likely</title></head>
<body>
<h3>Singleton test</h3>
<p>The counter is at <?php echo $s->getCounter(); ?></p>
<pre><?php var_dump($s); ?></pre>
<?php if ($x) for ($i = 0; $i < 1000; $i++) {
  $s = Singleton::getInstance();
  echo '<p>The counter is at '.$s->getCounter().'</p><p>';
  // wait
  for ($j = 0; $j < 10000; $j++) { echo '. '; }
    echo '</p>';
  }
?>
<p><a href="<?php echo $_SERVER['PHP_SELF']?>?inc=1">Increment the counter</a></p>
<p><a href="<?php echo $_SERVER['PHP_SELF']?>?ext=1">Do a long list</a> 
fire this off in one browser and increment in another.</p>
</body>
</html>

Using the modified versions above, open two separate browsers. Point both at the page and increment in one then reload the other. So far so good. Now set off the long list in the one and increment in the other while it is still running. What happened? The Singleton pattern works within a given thread, so for as long as that thread runs, changes made to the Singleton’s serialized data will not be available in another thread. There is a possible work-around, which would be to read and unserialize the value every time getCounter() is called. At the expense of a little more overhead the expected behavior in terms of object state can be obtained. But back to the real question: Is it a Singleton? Well no, not really in the sense that most of us think of a Singleton, which is system or application-wide. But it is at least within its containing thread, which might make it more useful for command-line PHP in long-running scripts. (Like those report generation scripts that you are running in a daily cron job that join 18 tables and generate 500,000 line csv files … no? Just me?)

One Response so far

  1. 1

    C said,

    July 1, 2012 @ 5:45 pm

    That’s nice ! I intend to use it in some project of my own.

    I cannot discuss here the details, however we could keep in touch about this topic.

    I also was in need to increment some counter, and to harvest some argument from a database or file, in order to use it later in a webpage.

    I’d like to chat with you about this thing, maybe we could even get involved in that project.

    Thank you !

Comment RSS