<?php
/**
* Exception class for the XMLDocument class.
*/
class XMLDocumentException extends Exception { }
/**
* A simple stack-based XML string constructor for creating XML.
* @author Peter Goodman
*/
class XMLDocument {
const TAG_CLOSING = 1,
TAG_NON_CLOSING = 0;
private $tags = array(),
$indent = -1;
/**
* Construct the class and put in the base <xml> tag. This tag is non-
* removable from the stack and isn't built in the usual way :) It's an
* exception to the rule.
*/
public function __construct() {
$this->push(' ');
$xml =& $this->tags[$this->indent];
$xml['children'] = -1;
$xml['content'] = '<?xml version="1.0"?>';
$this->indent = -1;
}
// destructor, obvious.
public function __destruct() {
unset($this->stack);
}
/**
* Add some content to the current tag on the top of the stack.
*/
public function hasContent($content = "") {
$tag =& $this->tags[$this->indent+1];
// wrap the content in a CDATA block if it contains tags
if(preg_match("~[\<\>]~i", $content))
$content = "<![CDATA[\n{$content}\n]]>";
// what should the indent prefix be?
$prefix = $this->indent > 0 ? str_repeat("\t", $this->indent) : '';
// prefix the content
$c = "";
foreach(explode("\n", $content) as $line) {
if(strlen($line) > 0) {
$tag['content_num_lines']++;
$c .= $prefix . $line;
}
}
// only one line, lets remove the prefix
if($tag['content_num_lines'] == 1)
$c = substr($c, strlen($prefix));
$tag['content'] .= $c;
return $this;
}
/**
* Push a tag onto the XML tag stack.
*/
public function push($name = '', $type = TAG_CLOSING) {
// we can't push no tag onto the tag stack
if(empty($name) || !is_string($name))
throw new XMLDocumentException("Tag name expected in push operation.");
$this->indent++;
$this->tags[] = array('name' => $name,
'closing' => (bool)$type,
'attr' => '',
'content' => '',
'children' => 0,
'content_num_lines' => 0);
// get the parent tag and tell it that it has one more child. The
// children is also incremented in the pop method; however, these
// don't conflict because the lowest level child will have 0 children
// and this will have already been called. Thus, 1 + 0 accurately
// represents the number of children the node at the top of the stack
// has when a leaf node is popped, and so it all bubbles up nicely.
$parent =& $this->tags[$this->indent];
$parent['children']++;
return $this;
}
/**
* Pop the current tag off of the tag stack.
*/
public function pop($expect = NULL) {
// we've closed all tags, this is a guard against popping the xml
// tag off the stack
if($this->indent < 0)
throw new XMLDocumentException("No tags left to close.");
$tag =& array_pop($this->tags);
// did we expect to close a certain tag name and it's not the same one
// that the class expects?
if($expect !== NULL && $tag['name'] != $expect)
throw new XMLDocumentException("Malformed XML. Expected [{$expect}] "
."but found [{$tag['name']}].");
// this builds the output from the inside out by putting the content
// into the parent tag's content.
$prefix = $this->indent > 0 ? str_repeat("\t", $this->indent) : '';
$suffix = !$tag['closing'] ? ' /' : '';
$content = "\n{$prefix}<". $tag['name'] . $tag['attr'] . $suffix .">";
// do we need to build the contents of this tag?
if($tag['closing']) {
// write this tag over multiple lines
if($tag['content_num_lines'] > 1 || $tag['children'] > 0)
$content .= "{$prefix}". $tag['content'] ."\n{$prefix}";
// write this tag over a single line
else
$content .= $tag['content'];
$content .= '</'. $tag['name'] .">";
}
// add stuff into the parent and modify its content and children
// counter
$parent =& $this->tags[$this->indent];
$parent['content'] .= $content;
$parent['children'] += $tag['children'];
$this->indent--;
return $this;
}
/**
* Append an attribute to the top tag in the stack. This is really just a
* convenient shortcut.
*/
public function __call($attr, array $args) {
if(empty($attr))
throw new XMLDocumentException("XML tag attribute must has a name.");
$tag =& $this->tags[$this->indent+1];
$tag['attr'] .= " {$attr}=\"". preg_replace('~"~', '\"', (string)$args[0]) ."\"";
return $this;
}
/**
* Secondary function for attributes if an attribute, for example, has a
* colon in its name or has the name push, pop, or one of the other names
* of one of this class' functions.
*/
public function hasAttribute($name = '', $val = '') {
return $this->__call((string)$name, array((string)$val));
}
/**
* Do the header call.
*/
public function doHeader() {
header("Content-Type: text/xml");
}
/**
* Return the built XML string.
*/
public function __toString() {
// whoops, we're not done building the xml document yet!
if($this->indent > 0)
throw new XMLDocumentException("Cannot export malformed XML document.");
return (string)$this->tags[0]['content'];
}
}
$xml = new XMLDocument;
$xml->push('rss')->version("2.0")->encoding("UTF-8")
->push('channel')
->push('title')->hasContent("Feed title here")->pop()
->push('pubDate')->hasContent(date("D, d M Y H:i:s O"))->pop()
->push('link')->hasContent("http://ioreader.com")->pop()
->push('language')->hasContent("en-us")->pop();
// create the xml for each item
$xml->push('item')
->push('title')->hasContent("hello world")->pop()
->push('link')->hasContent("http://ioreader.com")->pop()
->push('description')->hasContent("this is a description")->pop()
->push('pubDate')->hasContent(date("D, d M Y H:i:s O"))->pop()
->push('guid')->hasContent(12345)->isPermaLink("false")->pop()
->pop('item');
// close remaining tags and output the xml
$xml->pop('channel')->pop('rss');
echo $xml;