14th May 2005

Posted in

Blog :: PHP and XML editing

So I realise that PHP5 and simpleXML are the promised land for PHP devs in dealing with XMl.  The problem, for me and probably for you, is that I'm still stuck in the PHP 4.3.x series.  This is for a variety of reasons; I'm writing a relatively small and simple CMS that is targeting the shared hosting market segment (ie you don't have root, you can't install reallycoollibversion4.23.14b to make my cms work, and you don't have control over your PHP version.)

Now even PHP 4.3 boasts some nice integrated XML libraries. XSLT and DOM support etc. All not turned on by default of course. I am open to better solutions, but for PHP 4.3, it looks like the PEAR XML_Transformer class is the best bet for editing existing XML. This of course brings up the hideous lack of documentation online. Via PHP Kitchen's list of PEAR tutorials I found this tutorial wherein the author touts the ease of use of XML_Transform, demonstrates the settings for case folding, and never actually transforms any XML. So, the following brief snippet demonstrates a simple transformation.

What I want to do is have the equivalent of the domNode.innerHTMl = string way of editing html.  Not theoretically pure I konw, bu quick n easy!  My CMS has little snippets of XML that describe a page's configuration and in editing them I need to find a named BLOCK tag (which corresponds to a Template_Sigma variable) and replace it's current content (which may contain TEMPLATE, BLOCK, and OBJECT tages) with a new snippet of XML (which may contain new TEMPLATE, BLOCK, and OBJECT tags).

Basically, I write a class (replaceBlockContent) that extends the XML_Transformer_Namespace class. The base class handles all the opening and closing tag events, but calls methods in the child class if the tag type matches a defined function. For my class I have defined start_block and end_block methods, so they get called when a block tag (opening and closing) is found. I copied the default return statements from the base class. The block_start functions watches for a block tag to come along with the right name. It then sets the state of the class ($this->_capture=true) or increases the depth count if it is already in "capture mode". The block_end method usually just returns the closing tag and any cdata but if it returns an array of ($string, true) than $string overwrites all the returns back up to the opening tag. This is the behaviour we are looking for. The depth indicator is checked to account for the nested recursive nature of my XML schema. The following code would change this fragment of XML

<template name="index.html">
   <block name="menu">
       <data_object name="menu"></data_object>
   </block>
   <block name="middle_content">
   <template name="blog.html">
       <block name="blog">
               <data_object name="blog" factory="1" />
       </block>
       <block name="tags">
           <data_object name="blog" factory="1" tags="1" />
       </block>            
   </template>
   </block>
</template>

to this

<template name="index.html">
   <block name="menu">
       <data_object name="menu"></data_object>
   </block>
   <block name="middle_content">
       <data_object name="text"></data_object>
   </block>
</template>

Here is the code:
<?php

require_once 'XML/Transformer.php';
require_once 'XML/Transformer/Namespace.php';
class replaceBlockContent extends XML_Transformer_Namespace {
    var $_blockname ='';
    var $_data = '';
    var $_capture = false;
    var $_depth = 0;
    function replaceBlockContent($blockname, $xml)
    {
        $this->blockname = strtoupper($blockname);
        $this->xml = $xml;
    }
    function start_block($attributes)
    {
        if($this->_capture==true)
            $this->_depth++;
        if(strtoupper($attributes['name']) == $this->blockname)
        {
            $this->_blockname = $attributes['name'];
            $this->_capture=true;
        }
        return sprintf("<block %s>", XML_Util::attributesToString($attributes));
    }

    function end_block($cdata)
    {
        if($this->_capture==true and $this->_depth==0)
        {
            $this->_capture = false;
            return array('<block name="' . $this->_blockname . '">' . $this->xml . '</block>', true);
        }
        elseif($this->_capture==true)
        {
            $this->_depth--;
        }
        return array( sprintf('%s</block>', $cdata), FALSE);
    }
}

$name="middle_content"; $t=new XML_Transformer();
$t->overloadNamespace('', new replaceBlockContent($name, "<data_object name='text' />"));
$newschema = $t->transform($schema);

?>

Posted on May 14th 2005, 12:46 AM