Location: PHPKode > scripts > DOMi > domi.class.php
<?php

/**
 * Improved DOM object - a merger of DOMDocument, DOMXpath and XSLTProcessor, 
 * with extended functionality for converting PHP data types into an XML 
 * structure and convert the XML into the desired output format
 *
 * version 1.1 is a retooling of the object by Steve Phillips
 * 1.1.0 released on 15 Jul 2009
 *
 * merger of Steve Phillips' AttachToXml and Chris Webb's XmlBuilder. 
 * AttachToXml is an extension of Steve Howe's ConvertObjectList.
 *
 * more information can be found at http://domi.sourceforge.net/
 *
 * @author Steve Phillips <hide@address.com>
 * @author Chris Webb <hide@address.com>
 * @author Phillip Brown (like the color) <hide@address.com>
 * @author Steve Howe
 * @version 1.1.0
 */

class DOMi {
    var $listSuffix = "-list"; ///< @var string suffix used when a list is made
    var $xslt;                 ///< @var XSLTProcessor internal XSLTProcessor 
    var $dom;                  ///< @var DOMDocument internal DOMDocument
    var $xpath;                ///< @var DOMXpath internal DOMXpath
    var $mainNode;             ///< @var DOMNode DOMDocument root node
    var $encoding;             ///< @var string character encoding (ie, UTF-8)
    
    /// @var const regular expression matching a valid node name
    const REGEX_PREFIX = '/^[a-zA-Z][-a-zA-Z0-9_.]*$/'; 
    
    const RENDER_VIEW = 1;
    const RENDER_HTML = 2;
    const RENDER_XML  = 4;
    const RENDER_JSON = 8;
    
    const DT_ARRAY       = 'array';
    const DT_ATTR_ARRAY  = 'attr-array';
    const DT_STRING      = 'string';
    const DT_DOMI        = 'domi';
    const DT_DOMNODE     = 'domnode';
    const DT_DOMDOCUMENT = 'domdocument';
    const DT_OBJECT      = 'object';
    const DT_BOOL        = 'bool';
    
    /**
     *  create an instance of the DOMi object
     *  @param string the name that will be used on the root node
     *  @param string character encoding for the DOMi object, ie UTF-8
     *  @retval DOMi the created DOMi object
     */
    public function __construct($mainNodeName='root', $encoding='UTF-8') {
        if(self::isValidPrefix($mainNodeName)) {
            $this->encoding = $encoding;
            $this->dom = new DOMDocument('1.0', $this->encoding);
            $this->mainNode = $this->createElement($mainNodeName);
            $this->appendChild($this->mainNode);
            $this->xpath = new DOMXpath($this->dom);
            $this->xslt = new XSLTProcessor();
        } else {
            throw new exception("invalid prefix '$mainNodeName'");
        }
    }
    
    /**
     *  the heart of DOMi - take a complex data tree and build it into an
     *  XML tree with the specified prefix and attach it to either the
     *  specified node or the root node
     *
     *  @param mixed any PHP data type that is being converted to XML
     *  @param string the name of the node that the data will be built onto
     *  @param DOMNode node to attach the newly created onto
     *  @retval DOMNode the newly created node
     */
    public function attachToXml($data, $prefix, &$parentNode = false) {
        if(!$parentNode) {
            $parentNode = &$this->mainNode;
        }
        // i don't like how this is done, but i can't see an easy alternative
        // that is clean. if the prefix is attributes, instead of creating
        // a node, just put all of the data onto the parent node as attributes
        if(strtolower($prefix) == 'attributes') {
            // set all of the attributes onto the node
            foreach($data as $key=>$val)
                $parentNode->setAttribute($key, $val);
            
            $node = &$parentNode;
        } else {
            $node = $this->convertToXml($data, $prefix);
            if($node instanceof DOMNode)
                $parentNode->appendChild($node);
        }
        return $node;
    }
    
    /**
     *  convert a data tree to an XML tree with the name specified as the
     *  prefix
     *
     *  @param mixed any PHP data type that is being converted to XML
     *  @param string the name of the node that the data will be built onto
     *  @retval DOMNode the newly created node
     */
    public function convertToXml($data, $prefix) {
        $nodeName = $prefix;
        // figure out the prefix
        if(!self::isValidPrefix($prefix))
            throw new exception("invalid prefix '$prefix'");
        // if the data needs a list node, change the name to use the list-suffix
        if(self::isListNode($data))
            $nodeName = $prefix . $this->listSuffix;
        
        switch(self::getDataType($data)) {
            // if this array has attributes, do some additional work
            case self::DT_ATTR_ARRAY:
                // create the node, with the optionally specified value
                $node = $this->createElement(
                    $nodeName, 
                    isset($data['values']) ? $data['values'] : null
                );
                $data['attributes'] = 
                    isset($data['attributes']) ? 
                    $data['attributes'] : 
                    array();
                
                // set all of the attributes onto the node
                foreach($data['attributes'] as $key=>$val)
                    $node->setAttribute($key, $val);
                
                // remove the attributes and value so they aren't repeated
                // as children of the element
                unset($data['attributes']);
                unset($data['values']);
            case self::DT_ARRAY:
                // in the case of DT_ATTR_ARRAY, the node is already created
                if(!isset($node))
                    $node = $this->createElement($nodeName);
                // attach each child as a subnode
                foreach($data as $k=>$d) {
                    // figure out the child prefix
                    $childPrefix = self::isValidPrefix($k) ? $k : $prefix;
                    // recurse and attach
                    $node->appendChild($this->convertToXml($d, $childPrefix));
                }
                break;
            
            // when converting DOMi or DOMDocuments, just get the root node
            case self::DT_DOMI: 
            case self::DT_DOMDOCUMENT:
                $data = $data->childNodes->item(0);
            case self::DT_DOMNODE:
                // the node must be imported to be usable in this DOMDocument
                $domNode = $this->importNode($data, true);
                // only create a new node if the prefix and current root
                // node name aren't the same
                if($prefix == $domNode->nodeName) {
                    $node = $domNode;
                } else {
                    $node = $this->createElement($prefix);
                    $node->appendChild($domNode);
                }
                break;
            
            case self::DT_OBJECT:
                $node = $this->convertToXml(
                    $this->convertObjectToArray($data), 
                    $prefix
                );
                break;
            
            case self::DT_BOOL:
                $data = $data ? 'TRUE' : 'FALSE';
            default:
                $node = $this->createElement(
                    $nodeName, 
                    htmlspecialchars((string)$data)
                );
                break;
        }
        
        return $node;
    }
    
    /**
     *  process and return the output that will be sent to screen during
     *  the display process
     *
     *  @param mixed a string or array listing the XSL stylesheets to be used
     *      for the rendering process
     *  @param int a flag indicating the rendering type. acceptable values
     *      are DOMi::RENDER_HTML and DOMi::RENDER_XML
     *
     *  @retval string the result of the processing based on the stylesheets
     *      and the rendering mode
     */
    public function render($stylesheets=false, $mode=self::RENDER_HTML) {
        $this->xslt->importStylesheet($this->generateXsl($stylesheets));
        return $this->generateOutput($mode);
    }
    
    private function isListNode($data) {
        // if there are any invalid prefixes, a list must be used
        return 
            is_array($data) && 
            count(
                array_filter(
                    array_keys($data), 
                    array($this, 'isValidPrefix')
                )
            ) != count($data);
    }
    
    private function getDataType($data) {
        $dataType = self::DT_STRING;
        
        if(is_array($data)) {
            $dataType = 
                isset($data['attributes']) || isset($data['values']) ? 
                self::DT_ATTR_ARRAY : 
                self::DT_ARRAY;
        } elseif($data INSTANCEOF DOMi) {
            $dataType = self::DT_DOMI;
        } elseif($data INSTANCEOF DOMDocument) {
            $dataType = self::DT_DOMDOCUMENT;
        } elseif($data INSTANCEOF DOMNode) {
            $dataType = self::DT_DOMNODE;
        } elseif(is_object($data)) {
            $dataType = self::DT_OBJECT;
        } elseif(is_bool($data)) {
            $dataType = self::DT_BOOL;
        }
        
        return $dataType;
    }
    
    private function generateXsl($stylesheets) {
        // create a DOMDocument that will include every specified stylesheet
        $dom = new DOMDocument('1.0', $this->encoding);
        $stylesheetNode = $dom->createElementNS(
            'http://www.w3.org/1999/XSL/Transform', 
            'xsl:stylesheet'
        );
        $stylesheetNode->setAttribute('version', '1.0');
        $stylesheetNode->setAttribute(
            'xmlns:xsl', 
            'http://www.w3.org/1999/XSL/Transform'
        );
        $dom->appendChild($stylesheetNode);
        
        if(!is_array($stylesheets)) {
            $stylesheets = array($stylesheets);
        }
        
        // create an include node for each non-false stylesheet specified
        foreach(array_filter($stylesheets) as $stylesheet) {
            $includeNode = $dom->createElementNS(
                'http://www.w3.org/1999/XSL/Transform', 
                'xsl:include'
            );
            $includeNode->setAttribute('href', (string)$stylesheet);
            $stylesheetNode->appendChild($includeNode);
        }
        
        return $dom;
    }
    
    private function generateOutput($mode) {
        switch($mode) {
            case self::RENDER_XML:
                $output = $this->saveXml();
                break;
            
            case self::RENDER_HTML:
                $output = $this->transformToXML($this->dom);
                break;
        }
        
        return $output;
    }
    
    private function convertObjectToArray(&$element) {
        // the recursive call can't operate through objects, so they
        // must be handled specially
        if(is_object($element)) {
            // typecast the array to an object, and clean up private and
            // protected keys
            $element = $this->keyCleanup((array)$element);
            // begin the recursion again to go through this object-turned-array
            // this is not strictly necesary, as removing it will cause the
            // recursion to happen in convertToXml, but putting it here makes it
            // more readable and ever so slightly faster.
            array_walk_recursive(
                $element, 
                array($this, 'convertObjectToArray')
            );
        }
        return $element;
    }
    
    private function keyCleanup($array) {
        // find every invalid key (private and protected member properties)
        foreach(array_filter(array_keys($array), array($this, 'isInvalidKey')) 
            as $invalidKey) {
            // change the key name by copy / delete / create
            $data = $array[$invalidKey];
            unset($array[$invalidKey]);
            // find out the correct key name by getting the last chunk that
            // is only ascii 32 - 126, the standard set of printable characters
            // User�Types => Types
            $key = preg_replace(
                '/^.*[^\x20-\xFE]([\x20-\xFE]*)$/', 
                '\\1', 
                $invalidKey
            );
            $array[$key] = $data;
        }
        
        return $array;
    }
    
    private function isInvalidKey($key) {
        // a key is invalid if it has any characters that are outside
        // of the ascii range 32 - 126, which is the standard set of printable
        // characters
        return preg_match('/[^\x20-\xFE]/', $key);
    }
    
    public function __call($method, $parameters) {
        // find out if the requested method exists inside any of the internal
        // DOMDocument, XSLTProcessor or DOMXpath objects
        if(method_exists($this->dom, $method)) {
            $obj = 'dom';
        } elseif (method_exists($this->xslt, $method)) {
            $obj = 'xslt';
        } elseif (method_exists($this->xpath, $method)) {
            $obj = 'xpath';
        } else {
            $backtrace = debug_backtrace();
            $exception = 
                "Call to undefined method DOMi::$method()"
                ." in {$backtrace[1]['file']}"
                ." on line {$backtrace[1]['line']}";
            throw new exception($exception);
        }
        
        // if it exists, transparently call and return that function
        return call_user_func_array(array($this->$obj, $method), $parameters);
    }
    
    public function __get($property) {
        $get = null;
        
        // if the requested property is inside any of the internal DOMDocument
        // XSLTProcessor or DOMXpath objects, return that property
        if(isset($this->dom->$property)) {
            $get = $this->dom->$property;
        } elseif(isset($this->xslt->$property)) {
            $get = $this->xslt->$property;
        } elseif(isset($this->xpath->$property)) {
            $get = $this->xpath->$property;
        } elseif (strtolower($property) == 'dom') {
            // legacy support for older versions of DOMi that used 
            // upper camel case for variable names
            $get = $this->dom;
        }
        
        return $get;
    }
    
    /**
     *  indicates whether a prefix is acceptable for XML node names
     *  
     *  @param string the prefix to be checked
     *  @retval bool whether or not the prefix is acceptable
     */
    static public function isValidPrefix($prefix) {
        return preg_match(self::REGEX_PREFIX, $prefix);
    }
}

?>
Return current item: DOMi