root

Changeset 693

Show
Ignore:
Timestamp:
09/01/09 02:37:05 (1 year ago)
Author:
wbond
Message:

Added instance functionality to fXML for reading XML files

Files:

Legend:

Unmodified
Added
Removed
Modified
Copied
Moved
  • fXML.php

    r448 r693 Hide Line Numbers
    66 * http://flourishlib.com/docs/UTF-8 for more information. 
    77 *  
    8  * @copyright  Copyright (c) 2007-2008 Will Bond 
     8 * @copyright  Copyright (c) 2007-2009 Will Bond 
    99 * @author     Will Bond [wb] <will@flourishlib.com> 
    1010 * @license    http://flourishlib.com/license 
     
    1313 * @link       http://flourishlib.com/fXML 
    1414 *  
    15  * @version    1.0.0b 
    16  * @changes    1.0.0b  The initial implementation [wb, 2008-01-13] 
     15 * @version    1.0.0b2 
     16 * @changes    1.0.0b2  Added instance functionality for reading of XML files [wb, 2009-09-01] 
     17 * @changes    1.0.0b   The initial implementation [wb, 2008-01-13] 
    1718 */ 
    18 class fXML 
     19class fXML implements ArrayAccess 
    1920{ 
    2021    // The following constants allow for nice looking callbacks to static methods 
     
    4748     
    4849    /** 
    49      * Forces use as a static class 
    50      *  
     50     * Custom prefix => namespace URI mappings 
     51     *  
     52     * @var array 
     53     */ 
     54    protected $__custom_prefixes; 
     55     
     56    /** 
     57     * The dom element for this XML 
     58     *  
     59     * @var DOMElement 
     60     */ 
     61    protected $__dom; 
     62     
     63    /** 
     64     * An XPath object for performing xpath lookups 
     65     *  
     66     * @var DOMXPath 
     67     */ 
     68    protected $__xpath; 
     69     
     70    /** 
     71     * The XML string for serialization 
     72     *  
     73     * @var string 
     74     */ 
     75    protected $__xml; 
     76     
     77     
     78    /** 
     79     * Create the XML object from a string, fFile or URL 
     80     *  
     81     * The `$default_namespace` will be used for any sort of methods calls, 
     82     * member access or array access when the element or attribute name does 
     83     * not include a `:`. 
     84     *  
     85     * @throws fValidationException  When the source XML is invalid 
     86     *  
     87     * @param  fFile|string $source  The source of the XML, either an fFile object, a string of XML, a file path or a URL 
    5188     * @return fXML 
    5289     */ 
    53     private function __construct() { } 
     90    public function __construct($source) 
     91    { 
     92        // Prevent spitting out errors to we can throw exceptions 
     93        $old_setting = libxml_use_internal_errors(TRUE); 
     94         
     95        $exception_message = NULL; 
     96        try { 
     97            if ($source instanceof DOMElement) { 
     98                $this->__dom = $source; 
     99                $xml         = TRUE; 
     100                 
     101            } elseif ($source instanceof fFile) { 
     102                $xml = simplexml_load_file($source->getPath()); 
     103                 
     104            } else { 
     105                $is_path = $source && !preg_match('#^\s*<#', $source); 
     106                $xml     = new SimpleXMLElement($source, 0, $is_path); 
     107            } 
     108         
     109        } catch (Exception $e) { 
     110            $exception_message = $e->getMessage(); 
     111            $xml = FALSE; 
     112        } 
     113         
     114        // We want it to be clear when XML parsing issues occur 
     115        if ($xml === FALSE) { 
     116            $errors = libxml_get_errors(); 
     117            foreach ($errors as $error) { 
     118                $exception_message .= "\n" . $error->message;    
     119            } 
     120            // If internal errors were off before, turn them back off 
     121            if (!$old_setting) { 
     122                libxml_use_internal_errors(FALSE);   
     123            } 
     124            throw new fValidationException(str_replace('%', '%%', $exception_message)); 
     125        } 
     126         
     127        if (!$old_setting) { 
     128            libxml_use_internal_errors(FALSE);   
     129        } 
     130         
     131        if (!$this->__dom) { 
     132            $this->__dom = dom_import_simplexml($xml); 
     133        } 
     134    } 
     135     
     136     
     137    /** 
     138     * Allows access to the text content of a child tag 
     139     *  
     140     * The child element name (`$name`) may start with a namespace prefix and a 
     141     * `:` to indicate what namespace it is part of. A blank namespace prefix 
     142     * (i.e. an element name starting with `:`) is treated as the XML default 
     143     * namespace. 
     144     *  
     145     * @internal 
     146     *  
     147     * @param  string $name  The child element to retrieve 
     148     * @return fXML|NULL  The child element requested 
     149     */ 
     150    public function __get($name) 
     151    {    
     152        // Handle nice callback syntax 
     153        static $methods = array( 
     154            '__construct'     => TRUE, 
     155            '__get'           => TRUE, 
     156            '__isset'         => TRUE, 
     157            '__sleep'         => TRUE, 
     158            '__toString'      => TRUE, 
     159            '__wakeup'        => TRUE, 
     160            'addCustomPrefix' => TRUE,   
     161            'getName'         => TRUE, 
     162            'getNamespace'    => TRUE, 
     163            'getPrefix'       => TRUE, 
     164            'getText'         => TRUE,   
     165            'offsetExists'    => TRUE, 
     166            'offsetGet'       => TRUE,  
     167            'offsetSet'       => TRUE, 
     168            'offsetUnset'     => TRUE,  
     169            'toXML'           => TRUE, 
     170            'xpath'           => TRUE 
     171        ); 
     172         
     173        if (isset($methods[$name])) { 
     174            return array($this, $name); 
     175        } 
     176         
     177        $first_child = $this->query($name . '[1]'); 
     178        if ($first_child->length) { 
     179            return $first_child->item(0)->textContent; 
     180        } 
     181         
     182        return NULL; 
     183    } 
     184     
     185     
     186    /** 
     187     * The child element name (`$name`) may start with a namespace prefix and a 
     188     * `:` to indicate what namespace it is part of. A blank namespace prefix 
     189     * (i.e. an element name starting with `:`) is treated as the XML default 
     190     * namespace. 
     191     *  
     192     * @internal 
     193     *  
     194     * @param  string $name  The child element to check - see method description for details about namespaces 
     195     * @return boolean  If the child element is set 
     196     */ 
     197    public function __isset($name) 
     198    { 
     199        return (boolean) $this->query($name . '[1]')->length;    
     200    } 
     201     
     202     
     203    /** 
     204     * Prevents users from trying to set elements 
     205     *  
     206     * @internal 
     207     *  
     208     * @param  string $name   The element to set 
     209     * @param  mixed  $value  The value to set 
     210     * @return void 
     211     */  
     212    public function __set($name, $value) 
     213    { 
     214        throw new fProgrammerException('The %s class does not support modifying XML', __CLASS__); 
     215    } 
     216     
     217     
     218    /** 
     219     * The XML needs to be made into a string before being serialized 
     220     *  
     221     * @internal 
     222     *  
     223     * @return array  The members to serialize 
     224     */ 
     225    public function __sleep() 
     226    { 
     227        $this->__xml = $this->toXML(); 
     228        return array('__custom_prefixes', '__xml');  
     229    } 
     230     
     231     
     232    /** 
     233     * Gets the string inside the root XML element 
     234     *  
     235     * @return string  The text inside the root element 
     236     */ 
     237    public function __toString() 
     238    { 
     239        return (string) $this->__dom->textContent;   
     240    } 
     241     
     242     
     243    /** 
     244     * Prevents users from trying to unset elements 
     245     *  
     246     * @internal 
     247     *  
     248     * @param  string $name  The element to unset 
     249     * @return void 
     250     */  
     251    public function __unset($name) 
     252    { 
     253        throw new fProgrammerException('The %s class does not support modifying XML', __CLASS__); 
     254    } 
     255     
     256     
     257    /** 
     258     * The XML needs to be made into a DOMElement when woken up 
     259     *  
     260     * @internal 
     261     *  
     262     * @return void 
     263     */ 
     264    public function __wakeup() 
     265    { 
     266        $this->__dom = dom_import_simplexml(new SimpleXMLElement($this->__xml)); 
     267        $this->__xml = NULL; 
     268    } 
     269     
     270     
     271    /** 
     272     * Adds a custom namespace prefix to full namespace mapping 
     273     *  
     274     * This namespace prefix will be valid for any operation on this object, 
     275     * including calls to ::xpath(). 
     276     *  
     277     * @param  string $ns_prefix  The custom namespace prefix 
     278     * @param  string $namespace  The full namespace it maps to 
     279     * @return void              
     280     */ 
     281    public function addCustomPrefix($ns_prefix, $namespace) 
     282    { 
     283        if (!$this->__custom_prefixes) { 
     284            $this->__custom_prefixes = array();  
     285        } 
     286        $this->__custom_prefixes[$ns_prefix] = $namespace; 
     287        if ($this->__xpath) { 
     288            $this->__xpath->registerNamespace($ns_prefix, $namespace); 
     289        } 
     290    } 
     291     
     292     
     293    /** 
     294     * Returns the name of the current element 
     295     *  
     296     * @return string  The name of the current element 
     297     */ 
     298    public function getName() 
     299    { 
     300        return $this->__dom->localName; 
     301    } 
     302     
     303     
     304    /** 
     305     * Returns the namespace of the current element 
     306     *  
     307     * @return string  The namespace of the current element 
     308     */ 
     309    public function getNamespace() 
     310    { 
     311        return $this->__dom->namespaceURI; 
     312    } 
     313     
     314     
     315    /** 
     316     * Returns the namespace prefix of the current element 
     317     *  
     318     * @return string  The namespace prefix of the current element 
     319     */ 
     320    public function getPrefix() 
     321    { 
     322        return $this->__dom->prefix; 
     323    } 
     324     
     325     
     326    /** 
     327     * Returns the string text of the current element 
     328     *  
     329     * @return string  The string text of the current element 
     330     */ 
     331    public function getText() 
     332    { 
     333        return (string) $this->__dom->textContent; 
     334    } 
     335     
     336     
     337    /** 
     338     * Provides functionality for isset() and empty() (required by arrayaccess interface) 
     339     *  
     340     * Offsets refers to an attribute name. Attribute may start with a namespace 
     341     * prefix and a `:` to indicate what namespace the attribute is part of. A 
     342     * blank namespace prefix (i.e. an offset starting with `:`) is treated as 
     343     * the XML default namespace. 
     344     *  
     345     * @internal 
     346     *  
     347     * @param  string $offset  The offset to check 
     348     * @return boolean  If the offset exists 
     349     */ 
     350    public function offsetExists($offset) 
     351    { 
     352        return (boolean) $this->query('@' . $offset . '[1]')->length; 
     353    } 
     354     
     355     
     356    /** 
     357     * Provides functionality for get [index] syntax (required by ArrayAccess interface) 
     358     *  
     359     * Offsets refers to an attribute name. Attribute may start with a namespace 
     360     * prefix and a `:` to indicate what namespace the attribute is part of. A 
     361     * blank namespace prefix (i.e. an offset starting with `:`) is treated as 
     362     * the XML default namespace. 
     363     *  
     364     * @internal 
     365     *  
     366     * @param  string $offset  The attribute to retrieve the value for 
     367     * @return string  The value of the offset 
     368     */ 
     369    public function offsetGet($offset) 
     370    { 
     371        $attribute = $this->query('@' . $offset . '[1]'); 
     372        if ($attribute->length) { 
     373            return $attribute->item(0)->nodeValue; 
     374        } 
     375        return NULL; 
     376    } 
     377     
     378     
     379    /** 
     380     * Required by ArrayAccess interface 
     381     *  
     382     * @internal 
     383     *  
     384     * @param  integer|string $offset  The offset to set 
     385     * @return void 
     386     */ 
     387    public function offsetSet($offset, $value) 
     388    { 
     389        throw new fProgrammerException('The %s class does not support modifying XML', __CLASS__); 
     390    } 
     391     
     392     
     393    /** 
     394     * Required by ArrayAccess interface 
     395     *  
     396     * @internal 
     397     *  
     398     * @param  integer|string $offset  The offset to unset 
     399     * @return void 
     400     */  
     401    public function offsetUnset($offset) 
     402    { 
     403        throw new fProgrammerException('The %s class does not support modifying XML', __CLASS__); 
     404    } 
     405     
     406     
     407    /** 
     408     * Performs an XPath query on the current element, returning the raw results 
     409     *  
     410     * @param  string $path  The XPath path to query 
     411     * @return array  The matching elements 
     412     */ 
     413    protected function query($path) 
     414    { 
     415        if (!$this->__xpath) { 
     416            $this->__xpath = new DOMXPath($this->__dom->ownerDocument); 
     417            if ($this->__custom_prefixes) { 
     418                foreach ($this->__custom_prefixes as $prefix => $namespace) { 
     419                    $this->__xpath->registerNamespace($prefix, $namespace); 
     420                } 
     421            }    
     422        } 
     423         
     424        // Prevent spitting out errors to we can throw exceptions 
     425        $old_setting = libxml_use_internal_errors(TRUE); 
     426         
     427        $result = $this->__xpath->query($path, $this->__dom); 
     428         
     429        // We want it to be clear when XML parsing issues occur 
     430        if ($result === FALSE) { 
     431            $errors            = libxml_get_errors(); 
     432            $exception_message = ''; 
     433             
     434            foreach ($errors as $error) { 
     435                $exception_message .= "\n" . $error->message;    
     436            } 
     437             
     438            // If internal errors were off before, turn them back off 
     439            if (!$old_setting) { 
     440                libxml_use_internal_errors(FALSE);   
     441            } 
     442             
     443            throw new fProgrammerException(str_replace('%', '%%', trim($exception_message))); 
     444        } 
     445         
     446        if (!$old_setting) { 
     447            libxml_use_internal_errors(FALSE);   
     448        } 
     449         
     450        return $result;  
     451    } 
     452     
     453     
     454    /** 
     455     * Returns a well-formed XML string from the current element 
     456     *  
     457     * @return string  The XML 
     458     */ 
     459    public function toXML() 
     460    { 
     461        return $this->__dom->ownerDocument->saveXML($this->__dom->parentNode === $this->__dom->ownerDocument ? $this->__dom->parentNode : $this->__dom);     
     462    } 
     463     
     464     
     465    /** 
     466     * Executes an XPath query on the current element, returning an array of matching elements 
     467     *  
     468     * @param  string  $path        The XPath path to query 
     469     * @param  boolean $first_only  If only the first match should be returned 
     470     * @return array|string|fXML  An array of matching elements, or a string or fXML object if `$first_only` is `TRUE` 
     471     */ 
     472    public function xpath($path, $first_only=FALSE) 
     473    { 
     474        $result = $this->query($path); 
     475         
     476        if ($first_only) { 
     477            if (!$result->length) { return NULL; } 
     478            $result = array($result->item(0)); 
     479             
     480        } else { 
     481            if (!$result->length) { return array(); } 
     482        } 
     483         
     484        $keys_to_remove = array(); 
     485        $output         = array(); 
     486         
     487        foreach ($result as $element) { 
     488             
     489            if ($element instanceof DOMElement) { 
     490                $child = new fXML($element); 
     491                $child->__custom_prefixes = $this->__custom_prefixes; 
     492                $output[] = $child; 
     493             
     494            } elseif ($element instanceof DOMCharacterData) { 
     495                $output[] = $element->data; 
     496             
     497            } elseif ($element instanceof DOMAttr) { 
     498                 
     499                $key      = $element->name; 
     500                if ($element->prefix) { 
     501                    $key = $element->prefix . ':' . $key;    
     502                } 
     503                 
     504                // We will create an attrname and attrname[0] key for each 
     505                // attribute and if more than one is found we remove the 
     506                // key attrname. If only one is found we remove attrname[0]. 
     507                $key_1 = $key . '[1]'; 
     508                 
     509                if (isset($output[$key_1])) { 
     510                    $i = 1; 
     511                    while (isset($output[$key . '[' . $i . ']'])) { 
     512                        $i++; 
     513                    } 
     514                     
     515                    // This removes the key without the array index if more than one was found 
     516                    unset($output[$key]); 
     517                    unset($keys_to_remove[$key_1]); 
     518                     
     519                    $key = $key . '[' . $i . ']'; 
     520                 
     521                } else { 
     522                    $output[$key_1] = $element->nodeValue; 
     523                    $keys_to_remove[$key_1] = TRUE;      
     524                } 
     525                 
     526                $output[$key] = $element->nodeValue;     
     527            } 
     528        } 
     529         
     530        foreach ($keys_to_remove as $key => $trash) { 
     531            unset($output[$key]);    
     532        } 
     533         
     534        if ($first_only) { 
     535            return current($output);     
     536        } 
     537         
     538        return $output; 
     539    } 
    54540} 
    55541 
     
    57543 
    58544/** 
    59  * Copyright (c) 2007-2008 Will Bond <will@flourishlib.com> 
     545 * Copyright (c) 2007-2009 Will Bond <will@flourishlib.com> 
    60546 *  
    61547 * Permission is hereby granted, free of charge, to any person obtaining a copy