Month: December 2010

Development

Simplified ANSI color term support in PHP

I was working on a script that needed some color terminal output and while it wasn’t particularly complicated, I found it was slowing me down. Flipping back and forth between a list of ANSI color codes and my work was frustrating. So, I did what I am often prone to do, I did a quick Google search for a PHP ANSI color terminal library. I found some things that were old, not maintained and not really fitting what I needed. So then, I did what I always end up doing in that situation, I built one.

The ANSI class is a way to quickly create several different foreground and background color combos along with a few style effects (like underline, inverse, and if you really must, blink). Of course the style effects only work on the standard 16 ANSI VT-100 terminal colors (the normal and “bold” or “bright” versions of black, red, green, yellow, blue, purple, cyan and white.)

The simplest way to use it is to create a new ANSI object for each color combo you want. So if you want red text on a white background, underlined bright green text on a black background and blue text on a yellow background, you could create three objects like so:

include_once('ansi.class.php');
$red_white = new ANSI(ANSI::RED, ANSI::WHITE);
$bright_green_black = new ANSI(ANSI::GREEN, ANSI::BLACK, array(ANSI::BRIGHT, ANSI::UNDERLINE));
$blue_yellow = new ANSI('blue', 'yellow');

Notice that I used a couple different ways of setting the colors, the class constant ints and strings. The effects are set in an array since you can chain multiple effects on a single color scheme (until you get into the extended color space, more on that in a minute.) Once you have these objects, styling your terminal output is simple.

$red_white->p("This is red on a white background, and prints no newline.");
$bright_green_black->p("This is bright green on a black background and prints no newline.");
$blue_yellow->pline("This is blue on a yellow background and will print a newline character.");
$red_white->setInverse(true);
$red_white->pline("This is now white on a red background.");

The p() and pline() methods will spit out the correct escape sequence and color codes to style and color the text, then print the text, then spit out the correct escape sequence and color code to “reset” the term to its default. This means no running a script that displays a warning then leaves your terminal bright yellow text on a red background.

So now that the standard color space is taken care of, how about a little love for the xterm 256 color space? Simple enough. Any int value passed to the color arguments of the constructor greater than 7 will automatically invoke the 256 color space. The first 16 colors (0 – 15) are just the default terminal colors, of course, but colors 16 – 231 are the extended color space, with 24 greyscale values from colors 232 – 255. So how do we know what color is what? Well, we can either call one of the static functions to view the color space (ANSI::showForegroundColors(), ANSI::showBackgroundColors()) or we can pass in a value from the static ANSI::rgb($r, $g, $b) function, which takes, you guessed it, three integer values from 0 – 255. While ANSI::rgb() tries to get to the closest color in the color space it still needs work. The very simplistic manner in which it is currently implemented is not the most accurate. It is on my to-do list somewhere, though.

$grey_gold = new ANSI(ANSI::rgb(31, 31, 31), ANSI::rgb(204, 153, 0));
$grey_gold->pline("This is grey text on a gold background.");
// effects don't work in the extended color space, except for inverse
$grey_gold->setInverse(true);
$grey_gold->pline("This is gold text on a grey background");

If you know of any way to apply the ANSI styles (underline, blink, inverse) in conjunction with the extended color space leave a comment to let me know. If you think the script could use some extra functionality do the same.

It is not incredibly clever or full-featured or any of those sorts of things, but it does what I needed it to do. If you would like to, you can download a copy a copy of the ANSI class (ansi.class.php.zip) – it is released under the MIT license, and is free to use, copy, distribute, etc etc.

Development

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?)