Tony Marston's Blog About software development, PHP and OOP

Using PHP 4's Sablotron extension to perform XSL Transformations

Posted on 18th May 2003 by Tony Marston

Amended on 22nd August 2004

Please note that this extension has been removed from PHP 5 and moved to the PECL repository. For details on how to use the XSL extension instead please refer to Using PHP 5's XSL extension to perform XSL Transformations.

Intended Audience
Prerequisites
A sample XML file
- XML file contents
A sample XSL file
- XSL file contents
XSL Include files
- std.pagination.xsl
- std.actionbar.xsl
Performing the XSL Transformation
- Defining optional parameters
- Creating an XSLT resource
- Optional settings
- Invoking the XSLT process
- Dealing with the results
Sample output
References
Amendment History
Comments

Intended Audience

This tutorial is for developers who wish to know how to generate HTML documents using a combination of XML data and XSL stylesheets. It also includes examples of how to use XSL parameters and included stylesheets.

The process of combining the content of an XML file and the formatting instructions of an XSL file to produce another document is known as an XSL Transformation (XSLT for short). This tutorial shows how PHP's inbuilt Sablotron module can be used to perform XSL transformations. It also shows how to pass optional parameters to the XSL stylesheet during the transformation process, and how you can use include files to contain shared stylesheet definitions.

While XSL files are static in nature XML files can be created 'on the fly' to contain whatever data has just been selected from the database. An example of the PHP code which can generate an XML document can be found at Using PHP 4's DOM XML functions to create XML files from SQL data. It is possible therefore to create complete web applications where the PHP scripts do not contain any HTML tags at all. All they do is create XML files from database queries, then use XSL transformations to generate the actual HTML documents.

This method completely splits the presentation layer (i.e. the generation of HTML documents) from the business layer (the application of business rules using a language such as PHP) so that any one of these layers can be modified without affecting the other.

Prerequisites

It is assumed that you have a version of PHP which has been compiled with XSLT support.

It is also assumed that you have some knowledge of XML and XSL.

A sample XML file

Here is a sample XML document. It can either be created by an external process, or it can be created by a PHP script such as by the procedure documented in Using PHP 4's DOM XML functions to create XML files from SQL data.

<?xml version="1.0"?>
<xample.person>
  <person>
    <person_id>PA</person_id>
    <first_name>Pamela</first_name>
    <last_name>Anderson</last_name>
    <initials>pa</initials>
    <star_sign>Virgo</star_sign>
  </person>
  <person>
    <person_id>KB</person_id>
    <first_name>Kim</first_name>
    <last_name>Basinger</last_name>
    <initials>kb</initials>
    <star_sign>Sagittarius</star_sign>
  </person>
  <person>
    .....
  </person>
  <actbar>
    <button id="person_search.php">SEARCH</button>
    <button id="reset">RESET</button>
    <button id="finish">FINISH</button>
  </actbar>
</xample.person>

An XML file contains a collection of nodes which must have an opening tag (<node>) and a corresponding closing tag (</node>). A node can contain either text or any number of child nodes. A node's opening tag may also contain any number of attributes in the format id="value".

XML file contents

Here is an explanation of the contents of the sample XML file:

<?xml version="1.0"> This is the XML declaration which identifies the XML version.
<xample.person> This is the root node. It can contain any text, even 'root', but in this case it is constructed from the database name and the table name. There must be 1, and only 1, root node in a document.
<person> This identifies a node which was constructed from the contents of a database table. In this example the node name is the same as the table name. Note that there are several occurrences of this node in the XML file.
<person_id>
<first_name>
This identifies a node which was named after a column from a row in the database table. The text between the opening and closing tags is the value from the database.
<actbar> This node is the parent of a series of button nodes.
<button> This identifies a child of the <actbar> node. Each button has a text node and an attribute with the name "id".

A sample XSL file

Here is a sample XSL stylesheet document. These files exist outside of PHP and may even be under the control of a separate team of developers.

<?xml version='1.0'?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">

<xsl:output method='html'/>

<!-- param values may be changed during the XSL Transformation -->
<xsl:param name="title">List PERSON</xsl:param>
<xsl:param name="script">person_list.php</xsl:param>
<xsl:param name="numrows">0</xsl:param>
<xsl:param name="curpage">1</xsl:param>
<xsl:param name="lastpage">1</xsl:param>
<xsl:param name="script_time">0.2744</xsl:param>

<!-- include common templates -->
<xsl:include href="std.pagination.xsl"/>
<xsl:include href="std.actionbar.xsl"/>

<xsl:template match="/">

<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <title><xsl:value-of select="$title"/></title>
    <style type="text/css">
      <![CDATA[
      <!--
        caption { font-weight: bold; }
        th { background: #cceeff; }
        tr.odd { background: #eeeeee; }
        tr.even { background: #dddddd; }
        .center { text-align: center; }
      -->
      ]]>
    </style>

</head>
<body>
  
  <form method="post" action="{$script}">
  <div class="center">

  <table border="0">
    <caption><xsl:value-of select="$title"/></caption>
    <thead>
      <tr>
        <th>Person ID</th>
        <th>First Name</th>
        <th>Last Name</th>
        <th>Star Sign</th>
      </tr>
    </thead>
	  
    <tbody>
      <xsl:apply-templates select="//person" />
    </tbody>
	  
  </table>
	
  <!-- insert the page navigation links -->
  <xsl:call-template name="pagination" />

  <!-- create standard action buttons -->
  <xsl:call-template name="actbar"/>

  </div>
  </form>
</body>
</html>

</xsl:template>

<xsl:template match="person">

  <tr>
    <xsl:attribute name="class">
      <xsl:choose>
        <xsl:when test="position()mod 2">odd</xsl:when>
        <xsl:otherwise>even</xsl:otherwise>
      </xsl:choose>
    </xsl:attribute>

    <td><xsl:value-of select="person_id"/></td>
    <td><xsl:value-of select="first_name"/></td>
    <td><xsl:value-of select="last_name"/></td>
    <td><xsl:value-of select="star_sign"/></td>
  </tr>

</xsl:template>

</xsl:stylesheet>

XSL file contents

As you can see the XSL file contains a mixture of standard HTML tags and a series of other tags which have the <xsl: prefix. This is similar to a PHP script which can contain a mixture of HTML tags and PHP instructions. Anything which is within an XSL tag will be processed by the XSLT engine in some way, while everything else will be output 'as is'.

Note that this example has its CSS definitions defined internally. In a proper production environment I would expect these to be held in an external file.

Here is an explanation of the various XSL tags:

<xsl:stylesheet> Everything within this tag is a stylesheet. Note that the first stylesheet declaration identifies the official W3C XSL recommendation namespace and the version number.
<xsl:output /> This controls the format of the stylesheet output. The default is 'html' with other options being 'xml' and 'text'. Note that the Sablotron extension allows a method of 'xhtml' which ensures that the output conforms to the XHTML rather than the HTML standard.
<xsl:param> This identifies a parameter that can be referenced from anywhere within the stylesheet by its name (but with a '$' prefix). A default value may be declared within the stylesheet, although a different value may be supplied during the transformation process.
<xsl:include /> This is similar to PHP's include function. It allows common stylesheets to be defined once then included as and when necessary. Note that you will need to use the xsl_set_base() command within your PHP script in order to define the base directory which contains the include files.
<![CDATA[......]]> Identifies a piece of text to be ignored by the XML parser. Without this the '<' and '>' would cause errors as the parser would treat them as if they were part of start or end tags.
<xsl:template match="/"> This defines the start of a template. The match="/" attribute associates (matches) the template to the root (/) of the XML source document.
<xsl:apply-templates> />; This defines a set of nodes to be processed. The select="//person" attribute identifies those nodes with the name 'person'. The '//' is used to signify 'child of the root node'. This will look for a template which matches that rule, in this case <xsl:template match="person" >.
<xsl:call-template /> This is used to invoke a named template, as identified by the name attribute. This is analogous to calling a function within PHP. This will look for a template which matches that rule, in this case <xsl:template name="..." >
<xsl:value-of /> This outputs the string value of the expression. The expression can be one of the following:
  • If it has a '$' prefix then it identifies the <xsl:param> with that name.
  • Without this prefix it identifies a node within the current path of the XML document.
Note that in some cases this tag can be shortened to an expression within curly braces, as in {$script}. This is similar to the use of curly braces within PHP.
position() This equates to the position of the node being tested among its siblings of the same name (i.e. its row number). Note that this number starts from 1, not 0 as in PHP.
mod This produces the modulus of the specified expression. For example, position()mod 2 tells us whether the row number of the current node is odd or even so that we can set the class attribute accordingly.
<xsl:attribute> Inserts an attribute into the current HTML tag.
<xsl:choose >
<xsl:when >
<xsl:otherwise >
This is equivalent to IF .... ELSEIF .... ELSE

XSL Include files

It is possible to store common templates in separate files and incorporate them into your stylesheet at runtime by means of the <xsl:include> command. This is similar to calling a subroutine in other programming languages. Here are listings and explanations of the include files used in this example.

std.pagination.xsl

<?xml version='1.0'?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">

<!--
//*****************************************************************************
// Copyright 2003 by A J Marston <tony@tonymarston.net>
// Distributed under the GNU General Public Licence
//*****************************************************************************
-->

<xsl:template name="pagination">

  <table border="0" class="pagination">
    <tr class="pagination">
      <xsl:choose>
        <xsl:when test="$curpage&lt;=1">
          <!-- we are on page 1, so there is no navigation backwards -->
          <td class="pagination">FIRST</td>
          <td class="pagination">PREV</td>
        </xsl:when>
        <xsl:otherwise>
          <!-- insert links for first/previous page -->
          <td class="pagination"><a href="{$script}?page=1"><b>FIRST</b></a></td>
          <td class="pagination"><a href="{$script}?page={$curpage -1}"><b>PREV</b></a></td>
        </xsl:otherwise>
      </xsl:choose>
	  
      <!-- insert "page x of y" -->
      <td class="pagination">
        <xsl:text>Page </xsl:text> 
        <xsl:value-of select="$curpage"/>
        <xsl:text> of </xsl:text> 
        <xsl:value-of select="$lastpage"/>
      </td>
	  
      <xsl:choose>
        <xsl:when test="$curpage=$lastpage">
          <!-- we are on the last page, so there is no navigation forwards -->
          <td class="pagination">NEXT</td>
          <td class="pagination">LAST</td>
        </xsl:when>
        <xsl:otherwise>
          <!-- insert links for last/next page -->
          <td class="pagination"><a href="{$script}?page={$curpage +1}"><b>NEXT</b></a></td>
          <td class="pagination"><a href="{$script}?page={$lastpage}"><b>LAST</b></a></td>
        </xsl:otherwise>
      </xsl:choose>
	  
    </tr>
  </table>
  
</xsl:template>

</xsl:stylesheet>

$curpage, $lastpage and $script are parameters defined with the <xsl:param> tag, and which can have values supplied at runtime during the XSL transformation process. Access to these values allows this template to output the following:

std.actionbar.xsl

<?xml version='1.0'?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">

<!--
//*****************************************************************************
// Copyright 2003 by A J Marston <tony@tonymarston.net>
// Distributed under the GNU General Public Licence
//*****************************************************************************
-->

<xsl:template name="actbar">

  <table border="0" class="actionbar">
    <tr class="actionbar">
      <xsl:for-each select="//actbar/*">
        <!-- create a button for each element within actionbar -->
        <td class="actionbar">
           <input type="submit" name="{@id}" value="{node()}" />
        </td>
      </xsl:for-each>
    </tr>
  </table>
  
  <p class="script_time">page loaded in <xsl:value-of select="$script_time"/> seconds</p>

</xsl:template>

</xsl:stylesheet>
<xsl:for-each> This is similar to PHP's foreach function. The "//actbar/*" directive will select all available children of the actbar node.
(@id) This will output the contents of the attribute with the name 'id'.
{node()} This will output the value from the current node.
$script_time This is one of the parameters that were specified during the transformation process.

This method means that the buttons on the action bar are not hard coded within the XSL script but specified within the XML data. Thus the process which generates the XML data can vary the action buttons depending on its own internal logic.

Performing the XSL Transformation

You will find that there are numerous options available when you come to perform the XSL transformation. Below are some of these options with an explantion of their use.

Defining optional parameters

Any optional parameters which you want to pass to the XSL transformation process must be held within an array.

$xsl_params['script']  = $_SERVER['PHP_SELF'];
$xsl_params['title']   = $title;
$xsl_params['numrows'] = $numrows;
$xsl_params['curpage'] = $curpage;
$xsl_params['lastpage'] = $lastpage;
$xsl_params['script_time'] = $elapsed_time;

Creating an XSLT resource

This requires a single simple instruction.

$xp = xslt_create() or trigger_error('Could not create XSLT process.', E_USER_ERROR);

Optional settings

You may also wish to set the output encoding for the XSLT transformation.

xslt_set_encoding($xp, 'utf-8');

This next command will set the base directory name for 'xsl:include', 'xsl:import' etc. In this example I am specifying a directory called 'xsl' which exists immediately below the current directory.

xslt_set_base($xp, 'file://' .dirname($_SERVER['SCRIPT_FILENAME']) .'/xsl/');

Invoking the XSLT process

When it comes to actually performing the XSL transformation you have several options to choose from depending on the current location of your XML and XSL files.

This first method will read both files from disk into memory before the transformation.

// read the files into memory
$xsl_string = join('', file('example.xsl'));
$xml_string = join('', file('example.xml'));

// set the argument buffer
$arg_buffer = array('/xml' => $xml_string, '/xsl' => $xsl_string);

// process the two files to get the desired output
if (($result = xslt_process($xp, 'arg:/xml', 'arg:/xsl', NULL, $arg_buffer, $xsl_params))) {

The second method will have one file in memory and the other file on disk.

// read one file into memory
$xml_string = join('', file('example.xml'));

// set the argument buffer
$arg_buffer = array('/xml' => $xml_string);

// process the two files to get the desired output
if (($result = xslt_process($xp, 'arg:/xml', 'example.xsl', NULL, $arg_buffer, $xsl_params))) {

The third method will leave both files on disk.

if (($result = xslt_process($xp, 'example.xml', 'example.xsl', NULL, NULL, $xsl_params))) {

Note that if you have specified a directory using xsl_set_base in optional settings then any filename that you specify on the xslt_process line should also be in this directory.

Dealing with the results

Whichever of the above three methods you use, it needs to be followed by code which will either output the result of the transformation or display a suitable error message:

   // print output
   echo $result;
} else {
   // display error
   $errmsg = 'XSLT error: ' .xslt_error($xp) .' (error code: ' .xslt_errno($xp) .')';
   trigger_error($errmsg, E_USER_ERROR);
} // endif

// free the resources occupied by the handler
xslt_free($xp);

sample output

If you perform the XSL transformation using the sample XML and XSL files provided in this article the results should look like the following:

sablotron (11K)

If the appearance is not quite to your taste then you can alter it, not by modifying any PHP code, but by altering the stylesheet definitions contained within either your .CSS file or your .XSL file.

References


Amendment History

22 Aug 2004 Added reference to Using PHP 5's XSL extension to perform XSL Transformations which explains how to achieve the same result in PHP 5.

counter