Hello and welcome to our community! Is this your first visit?
Register
Enjoy an ad free experience by logging in. Not a member yet? Register.
Results 1 to 5 of 5
  1. #1
    Regular Coder ralph l mayo's Avatar
    Join Date
    Nov 2005
    Posts
    951
    Thanks
    1
    Thanked 31 Times in 29 Posts

    Modular DOMDocument template engine

    This is for PHP 5 only (I think)

    I won't go into the pros and cons of using a templating engine much except to say that it's a rather roundabout way of doing things, but that I think it pays dividends later on in maintainability. This one works with the XML standard, and doesn't need an appreciable modification to work with other XML compliant programs, like XHTML editors. If you have any questions or suggestions I'm open to both, but keep in mind this is pared down for simplicity's sake and isn't exactly indicative of what I'm really doing, just of the general idea.

    Note if you do use a template engine it becomes very important to keep users from using your tags and/or to keep user input from being parsed. In this case, for example, if a user posts a message with <bd:if condition="maliciouscode"> and it makes it into the parser, you've got huge problems.

    re: The "if", XML is a storage format and it's generally not really a good idea to impose logic structures on it, so caveat emptor

    Also, this is a multi-parter, so hang on. It's not too tough.

    First, an XML file to be read in and processed:

    domtest.xml
    Code:
    <?xml version="1.0" encoding="utf-8" ?>
    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
    <html xmlns="http://www.w3.org/1999/xhtml" xmlns:bd="http://example.com/dtd" xml:lang="en" lang="en">
    <!-- the important part of this line is the xmlns:bd property. bd can be anything, it will be your prefix below
        the URL defined here doesn't have to exist until you want to add validation to your template engine, but it
        has to match the value in DOMTemplate.class.php -->
    <head>
        <title>Welcome to <bd:variable name="sitename" /></title>
    </head>
    <body>
        <p>Last month was: <bd:date format="F" when="last month" /></p>
        <bd:if condition="microtime(true)%2 == 0">
            <p>Happy even microsecond!</p>
        </bd:if>
        <bd:else>
            <p>Oh well, maybe next refresh.</p>
        </bd:else>
    </body>
    </html>
    Next, the interface used by the engine itself and its modules:

    XMLTags.interface.php
    PHP Code:
    <?php
    interface XMLTags
    {
        
    /*  returns an array containing all the tags the object is responsible for.
            it may not duplicate or override any other object's tags */
        
    public function describe();
        
    /*  returns a DOMNode that should replace the input DOMNode in the template */
        
    public function doTag($DOMNode);
    }
    ?>
    Next, the actual templating engine.

    DOMTemplate.class.php
    PHP Code:
    <?php
    if (!defined('INCLUDED_DOMTEMPLATE'))
    {
        
    define('INCLUDED_DOMTEMPLATE'1);

        class 
    DOMTemplate implements XMLTags
        
    {
            
    /*private*/ var $dom;
            
    /*private*/ var $processed;
            
    /*private*/ var $handledtags;
            
    /*private*/ var $showelse;
            
    /*public*/ var $registeredvars;
            
    /*public*/ var $cdatare;
           
     
            public function 
    __construct($fromfile false)
            {
                
    $this->cdatare = array();           // inserted nodes in the form <![CDATA[key]]> will be replaced with value
                
    $this->processed false;           // true only when the template has been parsed of the relevant tags
                
    $this->showelse false;            // since the condition isn't included with the else statement, keep track of whether it should appear or not
                
    $this->registeredvars = array();  // add variables to this array to make them accessible from the template
                
    $this->handledtags = array();     // populated by the addTagHandler() function
               
                /* register the built in tags */
                
    $this->addHandler($this);

                
    /*  XML 1.0, UTF-8 encoding.  Doesn't appear to do anything except change the
                    values of the XML declaration */            
                
    $this->dom = new DOMDocument('1.0''utf-8');

                if (
    $fromfile)
                {
                    
    $this->loadTemplate($fromfile);
                }
            }
        
            
    /**
                read a file into the DOMDocument object
            */
            
    public function loadTemplate($filename)
            {
                
    /*  change this to loadHTMLFile() to allow malformed documents.  I use XHTML, 
                     which should be well formed anyway, and I'd rather it catch some errors here 
                     that will show up in validation anyway */
                
    $this->dom->load($filename);
            }
        
            
    /**
                step through the tags in our namespace and call their associated methods to get
                replacements for them
            */
            
    public function process()
            {
                
    /*  this URL must match that in your XML document's namespace declaration (xmlns)
                    but as it is the URL doesn't necessarily have to exist.  '*' is a wildcard. */
                
    $elements $this->dom->getElementsByTagNameNS('http://example.com/dtd''*');
                
                
    /*  don't be tempted to foreach if you're modifying the document in the loop, or
                    things will get messy */
                
    while ($elements->length 0)
                {
                    
    /* get the first occurance of a tag in our namespace */
                    
    $element $elements->item(0);
                    
    /* if it's handled ... */
                    
    if (array_key_exists(strtolower($element->localName), $this->handledtags))
                    {
                        
    /* ... get a replacement node from the responsible object ... */
                        
    $rnode $this->handledtags[strtolower($element->localName)]->doTag($element);
                        
    /* ... import it into our document ... */
                        
    $newnode $this->dom->importNode($rnodetrue);
                        
    /* ... and make the switch. */
                        
    $element->parentNode->replaceChild($newnode$element);
                    }
                    else
                    {
                        
    /*  You can handle this however you want.  It should only really happen during
                            development, so as long as you and any others helping with the project
                            know what's going on. */
                        
    throw new Exception('DOMTemplate::process : tag "' strtolower($element->localName) . '" is unhandled.');
                    }
                }
                
    $this->processed true;
            }
        
            
    /**
                as per interface, return an array of the handled tags. not so necessary here, but needed in modules
            */
            
    public function describe() 
            { 
                return array(
    'if''variable''else''include');            
            }
        
            
    /**
                returns a DOMNode object suitable to replace the node passed to it as $element
            */
            
    public function doTag($element)
            {
                
    /* get a reference to the whole document that can be used to create new nodes */
                
    $parent $element->ownerDocument;
                
                switch (
    strtolower($element->localName))
                {
                    case 
    'variable':
                        
    /*  TextNodes are just what they sound like.  XML sensitive chars like < > are 
                            automatically escaped to &lt; etc */
                        
    $replacement = (isset($this->registeredvars[$element->getAttribute('name')])) ? $this->registeredvars[$element->getAttribute('name')] : '';
                        return 
    $parent->createTextNode($replacement);
                    case 
    'if':
                        eval(
    '$bool = ' $element->getAttribute('condition') . ';');
                        
    /*  save the opposite of the result in case there's an else block coming up */
                        
    $this->showelse = !$bool;
                        if (
    $bool)
                        {
                            
    /* copy the contents of this block into the DOMDocumentFragment */
                            
    return $this->fillDocFrag($parent$element->childNodes);
                        }
                        else
                        {
                            
    /*  rather than deleting the node and breaking doTag's expected behavior,
                                just return a blank text node to replace it with */
                            
    return $parent->createTextNode('');
                        }
                    case 
    'else':
                        if (
    $this->showelse)
                        {
                            
    /* reset our bool */
                            
    $this->showelse false;
                            
                            return 
    $this->fillDocFrag($parent$element->childNodes);
                        }
                        else
                        {
                            return 
    $parent->createTextNode('');
                        }
                    case 
    'include':
                        if (!
    is_readable($src $element->getAttribute('src')))
                        {
                            throw new 
    Exception ('DOMTemplate::doTag : "' $element->getAttribute('src') . '" could not be opened for inclusion.');
                        }
                        if (
    strtolower($element->getAttribute('parse')) != 'true')
                        {
                            
    $fh = @fopen($src'r');
                            
    $inc = @fread($fh, @filesize($src));
                            @
    fclose($inc);
                            
    /*  anything added to $this->cdatare['id'] is replaced with the value if a 
                                matching CDATA section is found.  this allows you to include badly formed
                                HTML chunks */
                            
    $this->cdatare[($key md5(uniqid(rand(), true)))] = $inc;
                            return 
    $parent->createCDATASection($key);
                        }
                        else
                        {
                            
    $inc = new DOMDocument();
                            
    $inc->load($src);
                            
    $newnodes $inc->getElementsByTagName('*');
                            return 
    $this->fillDocFrag($parent$newnodes);
                        }
                    default:
                        
    /*  this isn't normally reachable unless the handledtags array in the constructor 
                            wrongly indicates something that isn't done here, so it's something else
                            you'll see in development */
                        
    throw new Exception('DOMTemplate::doTag : No action is defined for ostensibly handled tag  "' strtolower($element->localName) . '".');
                }
            }

            
    /**
                a utility function to get the nodes from $src into a DocumentFragment in $parent
            */
            
    private function fillDocFrag($parent$src)
            {
                
    $docfrag $parent->createDocumentFragment();
                
                foreach (
    $src as $cn)
                {
                    
    /* clone each node because copying isn't implied by appendChild */
                    
    $copy $cn->cloneNode(true);
                    
    $docfrag->appendChild($copy);                
                }
                
                return 
    $docfrag;
            }
        
            
    /**
                populate $handltags with the responsibilities claimed by the $obj, which should implement XMLTags
            */
            
    public function addHandler($obj)
            {
                if (!
    is_array($tags $obj->describe()))
                {
                    throw new 
    Exception('DOMTemplate::addHandler - interface error: couldn\'t get responsibilities description.');
                }
                foreach (
    $tags as $tag)
                {
                    if (isset(
    $this->handledtags[$tag]))
                    {
                        throw new 
    Exception('DOMTemplate::addHandler - duplicate responsibilities described for tag ' $tag '.');
                    }
                    
    $this->handledtags[$tag] = $obj;
                }
            }

            
    /**
                Kind of a built in plaintext template engine, useful to add chunks of plaintext without the automatic entity
                conversion
            */
            
    function plainTextReplace($doctext)
            {
                foreach (
    $this->cdatare as $id=>$text)
                {
                    
    $doctext str_replace('<![CDATA[' $id ']]>'$text$doctext);
                }
                return 
    $doctext;
            }
            
            
    /**
                returns the processed template body
            */
            
    public function render()
            {
                
    /* if process() wasn't called manually, do it now */
                
    if (!$this->processed)
                {
                    
    $this->process();
                }
                
    /* remove the extraneous namespace declarations DOMDocument slathers all over everything */
                
    $doctext preg_replace('/(?<!html) xmlns=".*?"/'''$this->dom->saveXML());
                
    /* remove your namespace or it will cause a validation error (why? I don't know) */
                
    $doctext str_replace('xmlns:bd="http://example.com/dtd" '''$doctext);
                return 
    $this->plainTextReplace($doctext); // replace any matching CDATA tags
            
    }
        }

    ?>
    Here's the modular part, or a quick example thereof. You can implement XMLTags to do new things without messing around with the engine code (although in this case you might want to mess with the core a bit since it's so minimal). This module creates a path from the XML tag prefix:date to PHP's date() and strtotime() functions:

    dateTimeTags.class.php
    PHP Code:
    <?php
    if (!defined('INCLUDED_DATETIMETAGS'))
    {
        
    define('INCLUDED_DATETIMETAGS'1);

        class 
    dateTimeTags implements XMLTags
        
    {
            public function 
    describe()
            {
                
    /* this must be an array even if there's only one tag */
                
    return(array('date'));
            }
        
            public function 
    doTag($element)
            {
                
    $parent $element->ownerDocument;
            
                switch (
    strtolower($element->localName))
                {
                    case 
    'date':
                        
    /* use the specified formatting if it's available, else a generic string */ 
                        
    $format = ($element->hasAttribute('format')) ? $element->getAttribute('format') : 'l, F j g:iA';
                        
    /* if the when attribute is available and it's a number, pass it, if it's 
                            a string, run it through strtotime(), if it's not set, assume now */
                        
    $when = ($element->hasAttribute('when')) ? (is_numeric($element->getAttribute('when')) ? $element->getAttribute('when') : strtotime($element->getAttribute('when'))) : time();
                        return 
    $parent->createTextNode(date($format$when));
                    default:
                        throw new 
    Exception('dateTimeTags::doTag : No action is defined for ostensibly handled tag  "' strtolower($element->localName) . '".');
                }
            }
        }
    }
    ?>
    And finally, a file to pull it all together and make the required connections to set things rolling:

    index.php
    PHP Code:
    <?php
    include('XMLTags.interface.php');
    include(
    'DOMTemplate.class.php');
    include(
    'dateTimeTags.class.php');

    /* instantiations */
    $template = new DOMTemplate('domtest.xml');
    $dt = new dateTimeTags();

    /* setup */
    $template->registeredvars['sitename'] = 'Web 1.0 SP2';
    $template->addHandler($dt);

    /* delayed gratification */
    echo $template->render();
    ?>
    Example output:

    Code:
    <?xml version="1.0" encoding="utf-8"?>
    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
    <html xmlns="http://www.w3.org/1999/xhtml"  xml:lang="en" lang="en">
    <!-- the important part of this line is the xmlns:bd property. bd can be anything, it will be your prefix below
        the URL defined here doesn't have to exist until you want to add validation to your template engine, but it
        has to match the value in DOMTemplate.class.php -->
    <head><meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
        <title>Welcome to Web 1.0 SP2</title>
    </head>
    <body>
        <p>Last month was: December</p>
        
            <p>Happy even microsecond!</p>
        
        
    
    </body>
    </html>
    Worth every penny!
    Last edited by ralph l mayo; 01-14-2006 at 08:23 PM. Reason: optimization

  • #2
    Senior Coder missing-score's Avatar
    Join Date
    Jan 2003
    Location
    UK
    Posts
    2,194
    Thanks
    0
    Thanked 0 Times in 0 Posts
    Thats quite nice . It might sound weird, but the worst thing about it is that you would have to use valid XML in order for it to work, so you couldn't really have partial templates unless they all were a full valid XML section.

    Also, where you use your ternary ($x->getAttribute('attr')) ?, you could also use (and would be semantically better to use):

    PHP Code:
    ($x->hasAttribute('attr')) 

  • #3
    Regular Coder ralph l mayo's Avatar
    Join Date
    Nov 2005
    Posts
    951
    Thanks
    1
    Thanked 31 Times in 29 Posts
    Thanks for the hasAttribute() thing, that makes more sense.

    I ran into some problesm with the only valid XML restriction too, so I added a tag that allows importing arbitrary fragments. Now only the main template has to be valid XML, and you can import snippets of any description.

    I didn't include an example because I'd have to add yet another file, but the usage is:
    Code:
    <!-- parse="true" requires valid XML and is necessary if there are further template tags in the file-->
    <bd:include src="valid.xml" parse="true" />
    <!-- simple replace no matter what the file contents -->
    <bd:include src="foo.html" />

  • #4
    New Coder
    Join Date
    Mar 2006
    Location
    I'm lost, livin inside myself
    Posts
    97
    Thanks
    0
    Thanked 0 Times in 0 Posts
    I'm playing around with this so I can use it on my website. What I'm trying to do is add in another tag that allows for the use of foreach if I use some arrays. I'm fairly unfamiliar with a lot of these XML functions so I'm struggling to get this code right.

    I want to be able to take code like this in the template:

    Code:
    <bd:foreach select="content">
    <p><bd:variable name="content" /></p>
    </bd:foreach>
    This is assuming content has been passed in as an array. I can't figure out how to do this. I'm also trying to figure out how to do it in a way where I could use multiple arrays to create a table:

    Code:
    <bd:foreach select="variable">
    <tr><td><bd:variable name="variable" /></td><td><bd:variable name="variable2" /></td></tr>
    </bd:foreach>
    I'm struggling to create the right code to handle this. I've gotten to the point where I could get it to just print out the array, but I don't know how to get it to access the code between the foreach tags and evaluate it properly.

    Code:
    case 'foreach':
    	if(isset($this->registeredvars[$element->getAttribute('select')]))
    	{
    		foreach($this->registeredvars[$element->getAttribute('select')] as $value)
    		{
    			//the code between the tags evaluated as an array
    		}
    	}
    Sorry to bump such an old thread for this, but this is giving me headaches like you wouldn't believe.

  • #5
    Regular Coder ralph l mayo's Avatar
    Join Date
    Nov 2005
    Posts
    951
    Thanks
    1
    Thanked 31 Times in 29 Posts
    I really wouldn't recommend using this. Try out Smarty or one of the other zillion templating engines that are maintained & supported.


  •  

    Posting Permissions

    • You may not post new threads
    • You may not post replies
    • You may not post attachments
    • You may not edit your posts
    •