Simon Willison’s Weblog

Subscribe

New PHP experiment, inspired by ColdFusion

17th July 2003

I’ve been reading up on ColdFusion MX recently, and I have to admit it looks like a really nice piece of technology. I’d previously written ColdFusion off as being too simplistic and primitive, but having seen how much its capable of I’m reconsidering my position.

If you’ve never seen ColdFusion before, it’s a server side scripting language/application server from Macromedia (who obtained it when they bought Allaire). MX is the latest version of the language, which sees it completely rewritten in Java to allow it to integrate with Java application servers and existing Java servlet applications. This is shrewd marketing on the part of Macromedia, and I’ve already seen them advertising it as something to make your existing Java web deployments easier to customise.

The thing that put me off ColdFusion originally is that it is a tag-based scripting language, specifically designed to make it easy for HTML developers with little or no previous programming experience to pick up. Here’s an example of a chunk of CF syntax:


<cfinclude template="some-other-file.cfm">

<cfif isdefined("form.name")>
 <cfoutput>
Hi there, #form.name#
 </cfoutput>
</cfif>

The thing that most surprised me about ColdFusion is that although the above syntax looks pretty verbose, it is actually designed in a way that means you can do an awful lot with very little code. The above in PHP would look like this:


<?php
include('some-other-file.php');
if (isset($form['name'])) {
    print "Hi there, $form['name']";
}
?>

It’s less typing, but only by a bit. Where CF gets really clever though is with its handling of SQL. Here’s the code to run a SQL query and loop through outputting the results to a simple table:


<cfquery name="users" datasource="myDSN">
select name, email from users order by name
</cfquery>
<table>
<cfoutput query="users">
<tr>
 <td>#name#</td><td>#email#</td>
</tr>
</cfoutput>
</table>

Here’s the same in PHP, assuming $db contains a connection to a MySQL database.


<?php
$result = mysql_query("select name, email from users order by name", $db);
echo '<table>';
while ($row = mysql_fetch_assoc($result)) {
    echo "<tr>
  <td>$row['name']</td><td>{$row['email']}</td>
</tr>";
}
echo '</table>';
?>

The ColdFusion example achieves the same effect but with less complicated code.

ColdFusion is not without its problems: it comes with a price tag, and it encourages mixing application logic with presentation code (although as with PHP this can be avoided through discipline and careful application design). Never the less, I found it interesting enough that last night I spent a few hours putting together a PHP implementation of a couple of ColdFusion concepts. As with many of the experiments I post here this is very much experimental code—I won’t be supporting it and I would not recommend using it for anything more than casual experimentation.

This demo renders this example Recent Entries page.

Recent Entries  • New weblog, new location • Freeing the postcode • WriteRoom • Tamarin • Fun with ctypes • Graphing requests with Tamper Data • Keep your JSON valid • What I'm excited about, post-conference edition • The YDN Python Developer Center • Sticking with Opera 9

Here’s the Recent Entries page PHP source code:

<?php
// Set up $db as a connection to MySQL ...
include('TemplateParser.class.php');

$parser = new TemplateParser($db, array(
    'title' => 'Recent Entries', 
    'limit' => 10
));

echo $parser->parse(implode('', file('templates/recent.xml')));
?>

It renders this recent.xml template:

<template id="recent">
  <doctype type="xhtml1strict"/>
  <html>
    <head>
      <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
      <title>%title%</title>
      <style type="text/css">
        body {
          font-family: georgia;
          margin: 2em;
        }
      </style>
    </head>
    <body>
      <h1>%title%</h1>
      <sql id="recent">
        select title
        from entries 
        order by added desc
        limit 0, %limit%
      </sql>
      <ul>
        <output sql="recent">
          <li>%title%</li>
        </output>
      </ul>
    </body>
  </html>
</template>

And here’s the TemplateParser.class.php class that does all the work:

<?php

class TemplateParser {
    var $collect = false; // If true, tags are "collected" to $buffer rather than being added to $output
    var $buffer = '';
    var $output = '';
    var $parser;
    var $sqlid;
    var $outputsql;
    var $sql = array();   // 2D array of extracted SQL
    // -
    var $db;
    var $replacements = array(); // 2D array of replacement to make on cdata
    var $templatedir = 'templates/';
    // -
    var $doctypes = array(
        'html4strict' => '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">',
        'html4trans'  => '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">',
        'xhtml1strict' => '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">',
    );
    function TemplateParser($db, $replacements = array()) {
        $this->db = $db;
        $this->replacements = $replacements;
    }
    function parse($data) {
        $this->parser = xml_parser_create();
        // Set XML parser to take the case of tags in to account
        xml_parser_set_option($this->parser, XML_OPTION_CASE_FOLDING, false);
        // Set XML parser callback functions
        xml_set_object($this->parser, $this);
        xml_set_element_handler($this->parser, 'tag_open', 'tag_close');
        xml_set_character_data_handler($this->parser, 'cdata');
        if (!xml_parse($this->parser, $data)) {
            die(sprintf('XML error: %s at line %d',
                xml_error_string(xml_get_error_code($this->parser)),
                xml_get_current_line_number($this->parser)));
        }
        xml_parser_free($this->parser);
        return trim($this->output);
    }
    function tag_open($parser, $tag, $attr) {
        $singletag = false;
        switch ($tag) {
            case 'doctype':
                $this->handle_doctype($attr);
                return;
            case 'include':
                if (!isset($attr['template'])) {
                    die('include tag has no template attribute');
                }
                $path = $this->templatedir.$attr['template'].'.xml';
                if (!file_exists($path)) {
                    die("Template $path does not exist");
                }
                $temp = new TemplateParser($this->db, $attr);
                $this->output .= $temp->parse(implode('', file($path)));
                return;
            case 'ignore-start':
                $this->collect = true; // Although we have no intention of using it
                return;
            case 'ignore-stop':
                $this->buffer = '';
                $this->collect = false;
                return;
            case 'sql':
                if (!isset($attr['id'])) {
                    die('sql tag has no id attribute');
                }
                $this->sqlid = $attr['id'];
                $this->collect = true;
                return;
            case 'output':
                if (!isset($attr['sql'])) {
                    die('output tag has no sql attribute');
                }
                $this->outputsql = $attr['sql'];
                $this->collect = true;
                return;
            case 'template':
                return;
            case 'img':  // These are all HTML elements with no end tag
            case 'br':
            case 'link':
            case 'meta':
            case 'hr':
                $singletag = true;
        }
        // Now either collect or add the tag to output
        $xml = '<'.$tag.$this->makeattr($attr);
        if ($singletag) {
            $xml .= ' />';
        } else {
            $xml .= '>';
        }
        if ($this->collect) {
            $this->buffer .= $xml;
        } else {
            $this->output .= $xml;
        }
    }
    function tag_close($parser, $tag) {
        switch ($tag) {
            case 'sql':
                $this->sql[$this->sqlid] = $this->convert($this->buffer);
                $this->buffer = '';
                $this->collect = false;
                return;
            case 'output':
                // This is the tricky one. Run the SQL query, loop through it
                // and run convert on the saved block multiple times replacing with
                // the results of the query
                // First clear the buffer
                $block = $this->buffer;
                $this->buffer = '';
                $this->collect = false;
                // Now run the query
                $sql = $this->sql[$this->outputsql];
                $result = mysql_query($sql, $this->db);
                if (!$result) {
                    die("SQL query failed: '$sql'");
                }
                $i = 0;
                while ($row = mysql_fetch_assoc($result)) {
                    $row['#'] = ++$i; // '%#%' is now available as a counter var
                    $this->output .= $this->convert($block, $row);
                }    
                return;
            case 'template': // These are template elements with no end tag
            case 'include':
            case 'ignore-start':
            case 'ignore-stop':
            case 'doctype':
            case 'img':  // These are all HTML elements with no end tag
            case 'br':
            case 'link':
            case 'meta':
            case 'hr':
                return;
        }
        // Add the end tag to the buffer or output
        $xml = '</'.$tag.'>';
        if ($this->collect) {
            $this->buffer .= $xml;
        } else {
            $this->output .= $xml;
        }
    }
    function cdata($parser, $data) {
        if ($this->collect) {
            $this->buffer .= $data;
            return;
        }
        $this->output .= $this->convert($data);
    }
    function convert($text, $replacements = false) {
        if (!$replacements) {
            $replacements = $this->replacements;
        }
        if (strpos($text, '%') === false) {
            return $text; // Nothing to replace
        }
        foreach ($replacements as $from => $to) {
            $text = str_replace('%'.$from.'%', $to, $text);
        }
        return $text;
    }
    function makeattr($attr) {
        $return = '';
        foreach ($attr as $name => $value) {
            $return .= $name.'="'.$value.'"';
        }
        if ($return != '') {
            $return = ' '.$return;
        }
        return $return;
    }
    function handle_doctype($attr) {
        if (!isset($attr['type'])) {
            die('doctype requires type attribute');
        }
        if (!in_array($attr['type'], array_keys($this->doctypes))) {
            die('doctype requires valid type attribute');
        }
        $this->output .= $this->doctypes[$attr['type']];
    }
}

?>

With a bit more development, something like this could be a useful tool for quick-and-dirty PHP scripts that simply pull data out of MySQL and display it as HTML. I still prefer PHP, but ColdFusion has a lot of good ideas which are well worth knowing about.

Update July 1st 2025: I stumbled across this long-forgotten experiment today and decided to have Claude write me some documentation for it, since this blog entry didn’t describe all of its features. Here’s the result (and the Claude transcript). Amusingly, Claude described the code like this:

This appears to be a custom template system from the mid-2000s era, designed to separate presentation logic from PHP code while maintaining database connectivity for dynamic content generation.

This is New PHP experiment, inspired by ColdFusion by Simon Willison, posted on 17th July 2003.

Next: The Google Browser

Previous: Netscape R.I.P.

Monthly briefing

Sponsor me for $10/month and get a curated email digest of the month's most important LLM developments.

Pay me to send you less!

Sponsor & subscribe

Previously hosted at http://simon.incutio.com/archive/2003/07/17/phpAndColdFusion