Home > Toolbox > Parsing a menu using XML, XSLT, and PHP

  1. Parsing a menu using XML, XSLT, and PHP

Parsing a menu using XML, XSLT, and PHP

While I was evaluating PHP and started to restructure this site, I thought it would be nice to use XML for both content and menu of the site. XML allows a structured design of your content that is both flexible and easy to use. Using XSLT you can easily transform this content to HTML. Actually, since XML requires a structured and well-formed page, you get XHTML 1.1 compliance almost for free. This article gives an overview of how to accomplish this using three ingredients: XML, XSLT, and PHP.


Configuring your server

Obviously you need a server that supports PHP. I've tested my scripts on a Windows XP machine with PHP version 4.3.4, which can be downloaded from http://www.php.net/downloads.php. This site is hosted on a NetBSD configuration though, so you might to need ask your hosting provider for support. XML support is included in this version of PHP by default, but XSLT support requires an additional package: the Sablotron library. The link http://www.php.net/manual/en/ref.xslt.php has some information about this, but this was insufficient for my local (Windows XP) machine. In the end, I simply downloaded the entire PHP package and copied the 'php_xslt.dll' file to the 'Extensions' directory of my PHP installation directory. Then you only need to uncomment the line that references this file in your 'php.ini' file (typically located in your Windows directory).

Editing the XML menu

A menu for a website can be compared to the table of contents used in books. No matter how the menu is displayed, you can always break it down to a simple chapter and paragraph structure. Although I must admit some web designers can put you on the wrong foot very effectively. This sample menu displays a hierarchy that implements the required parent-child relationships of the various items.
<?xml version="1.0"?>
<menugroup>
  <menuitem id="1000" caption="Topic 1"/>
  <menuitem id="2000" caption="Topic 2"/>
  <menuitem id="3000" caption="Topic 3">
    <menuitem id="3100" caption="Topic 3.1"/>
    <menuitem id="3200" caption="Topic 3.2"/>
    <menuitem id="3300" caption="Topic 3.3"/>
  </menuitem>
  <menuitem id="4000" caption="Topic 4">
    <menuitem id="4100" caption="Topic 4.1"/>
    <menuitem id="4200" caption="Topic 4.2"/>
  </menuitem>
</menugroup>
In this example I've used an ID and a caption for the items. Below, I will use these IDs in some PHP functions. You could adapt this to own your own liking; maybe you'ld rather use an URL for the items instead. The key here is that the sample XML structure features the desired parent-child relations. You could do the same thing by including a reference to the parent, by e.g. including a 'parentID' attribute or something. But I think this layout is more intuitive - for XML that is -, which is why I stick with this solution.

Transforming the menu to HTML with XSLT

The menu I want to create is actually a plain table in HTML. Each item is displayed in a single row and cell. The highlighting of an item is done by using styles and dynamic HTML. To redirect the browser to a designated URL, a small javascript is attached to the 'onclick' event, which is fired when the entire cell is clicked. In addition the text itself is a hyperlink, which is useful for spiders of search engines.
<td class="nav1st0" onmouseover="this.className='nav1st1';" 
  onmouseout="this.className='nav1st0';" 
  onclick="javascript:location.replace('index.php?id=3200')">
  <strong><a class="nav" href="index.php?id=3200">Topic 3.2</a></strong>
</td>
		
The stylesheet that is used in this example supports two levels of nesting only. Since each level has its own style element though, I apply the template match for 'menuitem' in the XSTL file two times. Each template translates the XML item to an HTML table element, similar to the sample above. Only one recursion level is displayed below, but in the actual source file both matches call the template 'name' with a level number as parameter. By default, most XSLT parsers will include an XML declaration at the top of the output. Since I want to embed the result in another HTML document, I've removed this tag from the output by specifying the 'omit-xml-declaration' attribute.
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" omit-xml-declaration="yes" />

<xsl:template name="item">
  <xsl:param name="level">0</xsl:param>

  <xsl:variable name="url">index.php?id=<xsl:value-of select="@id"/></xsl:variable>

  <tr>
    <td>
      <xsl:attribute name="class">nav<xsl:value-of select="$level"/>st0</xsl:attribute>
      <xsl:attribute name="onmouseover">this.className='nav<xsl:value-of select="$level"/>st1';
	  </xsl:attribute>
      <xsl:attribute name="onmouseout">this.className='nav<xsl:value-of select="$level"/>st0';
	  </xsl:attribute>
      <xsl:attribute name="onclick">javascript:location.replace('<xsl:value-of select="$url"/>')
	  </xsl:attribute>

      <strong>
        <a class="nav">
          <xsl:attribute name="href"><xsl:value-of select="$url"/></xsl:attribute>
          <xsl:value-of select="@caption"/>
        </a>
      </strong>
    </td>
  </tr>
</xsl:template>

<xsl:template match="menugroup">
  <table cellpadding="0" cellspacing="0" border="0">
    <xsl:apply-templates select="menuitem" />
  </table>
</xsl:template>

<xsl:template match="/menugroup/menuitem">
  <xsl:call-template name="item">
    <xsl:with-param name="level">0</xsl:with-param>
  </xsl:call-template>
  <xsl:apply-templates select="menuitem" />
</xsl:template>

</xsl:stylesheet>
If you're not familiar with XSLT, you should know that it is not a imperative language, such as C or Java, but declarative. This means that you don't really specify flow of control, but use templates instead. A template is matched with a certain pattern. The XSLT parser tries to find a match while it is scanning the input. Like all other languages and specifications, XSLT got extended as well, so it shares a lot of features with many modern programming languages nowadays. I primarily use the XSLT Developer's Guide, maintained by Microsoft, as reference. It has a good introduction to XSLT and features many examples, so you might find it useful.

Creating the breadcrumb trail

To aid the user of the web site, many sites include a so-called breadcrumb trail. This is the path, or level of descent as you wish, of the current item relative to the root of the site. Since my menu has no indication of the currently selected item, this path is essential to provide some navigation information. Hansel and Gretel left a trail of breadcrumbs in the forest so they could find their way home. The path we're going to create is inspired by their cleverness.
Possibly this could be done using XSLT (feel free to send me a demonstration), but I've chosen to do this with a PHP function and the XML document object model. For ease of use, I copy the items of entire XML menu to a global array, that is indexed by the ID of the item. In addition, I include the ID of its parent as well (now I think this is more intuitive).
 49: // initializes a structured array of items obtained from an XML file
 50:
function init_menu_data($file)
 51: {
 52:     
$xml_parser = xml_parser_create();
 53:     
xml_set_element_handler($xml_parser, "start_element", "end_element");
 54:     if (!(
$fp = fopen($file, "r")))
 55:     {
 56:         die(
"could not open XML input");
 57:     }
 58:
 59:     while (
$data = fread($fp, 4096))
 60:     {
 61:         if (!
xml_parse($xml_parser, $data, feof($fp)))
 62:         {
 63:             die(
sprintf("XML error: %s at line %d",
 64:                 
xml_error_string(xml_get_error_code($xml_parser)),
 65:                 
xml_get_current_line_number($xml_parser)));
 66:         }
 67:     }
 68:
 69:     
xml_parser_free($xml_parser);
 70: }

On line 53 two functions are bound to the parser. The first ('start_element') is invoked when a new item -which corresponds to an opening tag in XML- is processed by the XML parser. Naturally, the second function is invoked when a closing tag is encountered in the input stream. The parsing itself is being executed on line 61. It uses a small buffer for the input, which is read in blocks of 4 KB. Finally, on line 69 the resources of the parser are deallocated.

 20: // call-back function that processes the event of a new element
 21:
function start_element($parser, $name, $attrs)
 22: {
 23:     global
$depth;
 24:     global
$parentid;
 25:     global
$items;
 26:
 27:     if (isset(
$attrs) && array_key_exists('ID', $attrs))
 28:     {
 29:         
$id = $attrs['ID'];
 30:         
$pid = 0;
 31:         
$caption = $attrs['CAPTION'];
 32:         if (
array_key_exists($depth, $parentid))
 33:             
$pid = $parentid[$depth];
 34:         
$items[$id] = array("PID" => $pid, "CAPTION" => $caption);
 35:
 36:         
$parentid[$depth + 1] = $attrs['ID'];
 37:     }
 38:
 39:     
$depth++;
 40: }
 41:
 42:
// call-back function that processes the event of an element being closed
 43:
function end_element($parser, $name)
 44: {
 45:     global
$depth;
 46:     
$depth--;
 47: }
The two call-back functions use a global variable that maintains the current level of the element in the XML structure. I assume a valid XML input here. The 'items' array is initialized by the 'start_element' function. In addition a global array is updated to maintain the parent IDs. Since each item in XML could have multiple siblings, the entire trail has to be stored. Each new branch will overwrite the existing entries on their corresponding level, though. This is not a problem, since we're using a depth-first algorithm to visit the nodes in the XML input tree.

 77: function create_breadcrumb_trail($id)
 78: {
 79:     global
$items;
 80:     
$crum = "";
 81:     
$linkable = FALSE;
 82:
 83:     while (
array_key_exists($id, $items))
 84:     {
 85:             
$item = $items[$id]["CAPTION"];
 86:             if (
$linkable)
 87:                 
$item = create_item_url($id, $item);
 88:             else
 89:                 
$linkable = TRUE;
 90:
 91:             
$crum = SEPARATOR . $item . ' ' . $crum;
 92:             
$id = $items[$id]["PID"];
 93:     }
 94:
 95:     if (
$crum <> "")
 96:         
$crum = "<a href=\"" . PAGE . "\">Home</a>" . $crum;
 97:
 98:
 99:     return
$crum;
100: }
Finally the function 'create_breadcrumb_trail' creates the proper HTML string for the current path. The 'id' parameter is used to traverse the entries in the global 'items' array. With the work we've done in the previous functions, this is a rather easy job now. The parent ID of each item is prepended to the string, until the ID is no longer found. The link to the home page is included separately, since I haven't included this in the XML menu. The function to create the URL of the item is not displayed, but there's no real magic there either. I've made this a function, so you could extend this with your own code. My site includes optional URLs for instance. The 'linkable' boolean is used to prevent that the last item in the path is linkable. Since it is the page that is being displayed, there is no need to link it.



Putting it all together

With all the elements in place, it's only a matter of designing the HTML container. The XSLT translation for the menu is done on the server. You could use the same technique for your content. However, this example simply renders the caption and ID of the items found in the menu XML file.

102: function create_main_menu()
103: {
104:     
$xslthandler = xslt_create();
105:     
xslt_set_base($xslthandler, 'file://' . __FILE__);
106:     
$menu = xslt_process($xslthandler, XMLMENUFILE, XSLMENUFILE);
107:     
xslt_free($xslthandler);
108:
109:     return
$menu;
110: }

The code above is rather straightforward, I only had to call the function 'xslt_set_base' (line 105) for it to work properly on my server. Although this is not necessary according to the documentation, the required variable was uninitialized otherwise.


Valid XHTML 1.1! Copyright © 2004. You can contact the author at contact@markd.nl. Hosted by BytePark.