├── .editorconfig ├── .gitattributes ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── composer.json ├── phpunit.xml.dist ├── src ├── xml.php └── xml │ ├── DOMElement.php │ ├── NodeList.php │ ├── NodeListTrait.php │ ├── Parser.php │ ├── Proxy.php │ ├── RawXML.php │ ├── SimpleXMLElement.php │ └── Writer.php └── tests └── xml.Test.php /.editorconfig: -------------------------------------------------------------------------------- 1 | # This file is for unifying the coding style for different editors and IDEs 2 | # editorconfig.org 3 | 4 | root = true 5 | 6 | [*] 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [**.php] 13 | indent_style = space 14 | indent_size = 4 15 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | *.php text 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | tests/composer.lock 2 | tests/vendor/ 3 | *~ 4 | /vendor/ 5 | /*.lock 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | php: 3 | - '7.3' 4 | - '7.4' 5 | - '8.0' 6 | 7 | before_script: 8 | - composer install 9 | 10 | script: vendor/bin/phpunit --coverage-clover=coverage.clover 11 | 12 | notifications: 13 | email: false 14 | irc: 15 | channels: 16 | secure: "oPoQ3znLse98X3go3m1IVu7nGvXGXXu90U3/kooLeJ3wCJnk/H7ZHzLXu5RIm/2mqEd+CPWFdpyLVa95Ldy+qJ6qcoO6FzC6ID9vw1t781nnxfNHe1tb3pSb4GMKrpPgumSNqY10TIRYBNem9N7rGHtG6MUOtXVsLX20YpBgSAU=" 17 | on_success: change 18 | on_failure: always 19 | 20 | after_script: 21 | - wget https://scrutinizer-ci.com/ocular.phar 22 | - php ocular.phar code-coverage:upload --format=php-clover coverage.clover 23 | 24 | 25 | sudo: false 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012, Muze ( http://www.muze.nl/ ) 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the name of Muze nor the names of its contributors may be 12 | used to endorse or promote products derived from this software 13 | without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY 19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ARC: Ariadne Component Library 2 | ============================== 3 | 4 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/Ariadne-CMS/arc-xml/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/Ariadne-CMS/arc-xml/?branch=master) 5 | [![Code Coverage](https://scrutinizer-ci.com/g/Ariadne-CMS/arc-xml/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/Ariadne-CMS/arc-xml/) 6 | [![Latest Stable Version](https://poser.pugx.org/arc/xml/v/stable.svg)](https://packagist.org/packages/arc/xml) 7 | [![Total Downloads](https://poser.pugx.org/arc/xml/downloads.svg)](https://packagist.org/packages/arc/xml) 8 | [![Latest Unstable Version](https://poser.pugx.org/arc/xml/v/unstable.svg)](https://packagist.org/packages/arc/xml) 9 | [![License](https://poser.pugx.org/arc/xml/license.svg)](https://packagist.org/packages/arc/xml) 10 | 11 | 12 | A flexible component library for PHP 13 | ------------------------------------ 14 | 15 | The Ariadne Component Library is a spinoff from the Ariadne Web 16 | Application Framework and Content Management System 17 | [ http://www.ariadne-cms.org/ ] 18 | 19 | arc/xml 20 | ======= 21 | 22 | This component provides a unified xml parser and writer. The writer allows for readable and always correct xml in code, without using templates. The parser is a wrapper around both DOMDocument and SimpleXML. 23 | 24 | The parser and writer also work on fragments of XML. The parser also makes sure that the output is identical to the input. 25 | When converting a node to a string, \arc\xml will return the full xml string, including tags. If you don't want that, you 26 | can always access the 'nodeValue' property to get the original SimpleXMLElement. 27 | 28 | Finally the parser also adds the ability to use basic CSS selectors to find elements in the XML. 29 | 30 | Example code: 31 | ```php 32 | use \arc\xml as x; 33 | $xmlString = 34 | x::preamble() 35 | .x::rss(['version'=>'2.0'], 36 | x::channel( 37 | x::title('Wikipedia'), 38 | x::link('http://www.wikipedia.org'), 39 | x::description('This feed notifies you of new articles on Wikipedia.') 40 | ) 41 | ); 42 | ``` 43 | 44 | And parsing it: 45 | 46 | ```php 47 | $xml = \arc\xml::parse($xmlString); 48 | $title = $xml->channel->title->nodeValue; // SimpleXMLElement 'Wikipedia' 49 | $titleTag = $xml->channel->title; // Wikipedia 50 | echo $title; 51 | ``` 52 | 53 | Installation 54 | ------------ 55 | 56 | This library requires PHP 7.1 or higher. It is installable and autoloadable via Composer as arc/xml. 57 | 58 | ``` 59 | composer require arc/xml 60 | ``` 61 | 62 | Parsing XML 63 | =========== 64 | 65 | Examples 66 | -------- 67 | 68 | For these examples we'll use the following XML 69 | 70 | ```xml 71 | 74 | 75 | Slashdot 76 | http://slashdot.org/ 77 | News for nerds, stuff that matters 78 | en-us 79 | 2016-01-30T20:38:08+00:00 80 | 81 | 82 | Drone Races To Be Broadcast To VR Headsets 83 | http://hardware.slashdot.org/story/1757209/ 84 | 85 | 86 | FTDI Driver Breaks Hardware Again 87 | http://it.slashdot.org/story/1720259/ 88 | 89 | 90 | ``` 91 | 92 | ### Getting the title 93 | ```php 94 | $xml = \arc\xml::parse( $xmlString ); 95 | $title = $xml->channel->title; 96 | echo $title; 97 | ``` 98 | 99 | result: 100 | ``` 101 | Slashdot 102 | ``` 103 | 104 | The parser returns the full XML element by default. If you just want the contents, you must be explicit: 105 | 106 | ```php 107 | $title = $xml->channel->title->nodeValue; 108 | echo $title; 109 | ``` 110 | 111 | result: 112 | ``` 113 | Slashdot 114 | ``` 115 | 116 | Instead of the default in SimpleXML, arc\xml must be explicitly told to get the value of the node using the `nodeValue` property. 117 | 118 | ### Setting the title 119 | ```php 120 | $xml->channel->title = 'Update title'; 121 | ``` 122 | 123 | As you can see, there is no need to mention the nodeValue here, the name 'title' is enough to select the correct element. It would not make sense to turn the title into another tag entirely here. You can still use the `nodeValue` if you prefer though. 124 | 125 | ### Getting attributes 126 | ```php 127 | $about = $xml->channel['rdf:about']; 128 | ``` 129 | 130 | result 131 | ``` 132 | http://slashdot.org/ 133 | ``` 134 | 135 | Just what you would expect, even though there is a namespace in there. When you use a namespace that the parser hasn't been told about before, it will simply look it up in the document and re-use it. 136 | 137 | Since attributes aren't XML nodes, there is no nodeValue. Attributes are always returned as just a string. 138 | 139 | ### Setting attributes 140 | ```php 141 | $xml->channel['title-attribute'] = 'This is a title attribute'; 142 | ``` 143 | 144 | This adds the `title-attribute` if it wasn't there before, or updates it if it was. 145 | 146 | ### Removing attributes 147 | ```php 148 | unset($xml->channel['title-attribute']); 149 | ``` 150 | 151 | ### Searching the document 152 | ```php 153 | $items = $xml->find('item'); 154 | echo implode($items); 155 | ``` 156 | 157 | result: 158 | ```xml 159 | 160 | Drone Races To Be Broadcast To VR Headsets 161 | http://hardware.slashdot.org/story/1757209/ 162 | 163 | 164 | FTDI Driver Breaks Hardware Again 165 | http://it.slashdot.org/story/1720259/ 166 | 167 | ``` 168 | 169 | Again, you get the full XML of the result and it is just an array. (Its been joined here using `implode` for clarity). 170 | 171 | The `find()` method accepts most CSS2.0 selectors. For now you can't enter more than one selector, so you can't select 'item, channel' for instance. 172 | Either use the SimpleXML `xpath()` method or run multiple queries. 173 | 174 | ### Searching using namespaces 175 | ```php 176 | $xml->registerNamespace('dublincore','http://purl.org/dc/elements/1.1/'); 177 | $date = current($xml->find('dublincore|date)); 178 | echo $date; 179 | ``` 180 | 181 | result: 182 | ```xml 183 | 2016-01-30T20:38:08+00:00 184 | ``` 185 | 186 | Again, you get the full XML by default. But in addition, though you've used a namespace alias not known in the document ( `dublincore` ), `find()` returns the `` element for you. The alias is different, but the namespace is the same and that is what matters. 187 | 188 | The find() method always returns an array, which may be empty. By using current() you get the first element found, or null if nothing was found. 189 | 190 | ### Supported CSS Selectors 191 | 192 | The following CSS selectors are supported: 193 | 194 | - `tag1 tag2`
195 | This matches `tag2` which is a descendant of `tag1`. 196 | - `tag1 > tag2`
197 | This matches `tag2` which is a direct child of `tag1`. 198 | - `tag:first-child`
199 | This matches `tag` only if its the first child. 200 | - `tag1 + tag2`
201 | This matches `tag2` only if its immediately preceded by `tag1`. 202 | - `tag1 ~ tag2`
203 | This matches `tag2` only if it has a previous sibling `tag1`. 204 | - `tag[attr]`
205 | This matches `tag` if it has the attribute `attr`. 206 | - `tag[attr="foo"]`
207 | This matches `tag` if it has the attribute `attr` with the value `foo` in its value list. 208 | - `tag#id`
209 | This matches any `tag` with id `id`. 210 | - `#id`
211 | This matches any element with id `id`. 212 | - `ns|tag`
213 | This matches `ns:tag` or more generally `tag` in the namespace indicated by the alias `ns` 214 | 215 | SimpleXML 216 | --------- 217 | 218 | The parsed XML behaves almost identical to a SimpleXMLElement, with the exceptions noted above. So you can access attributes just like SimpleXMLElement allows: 219 | 220 | ```php 221 | $version = $xml['version']; 222 | $version = $xml->attributes('version'); 223 | ``` 224 | 225 | You can walk through the node tree: 226 | 227 | ```php 228 | $title = $xml->channel->title; 229 | ``` 230 | 231 | Any method or property available in SimpleXMLElement is included in \arc\xml parsed data. 232 | 233 | ### DOMElement methods 234 | 235 | In addition to SimpleXMLElement methods, you can also call any method that is available in DOMElement. 236 | 237 | ```php 238 | $version = $xml->getAttributes('version'); 239 | $title = $xml->getElementsByTagName('channel')[0] 240 | ->getElementsByTagName('title')[0]; 241 | ``` 242 | 243 | ### Parsing fragments 244 | 245 | The arc\xml parser accepts partial XML content. It doesn't require a single root element. 246 | 247 | ```php 248 | $xmlString = <<< EOF 249 | 250 | An item 251 | 252 | 253 | Another item 254 | 255 | EOF; 256 | $xml = \arc\xml::parse($xmlString); 257 | echo $xml; 258 | ``` 259 | 260 | result: 261 | ```xml 262 | 263 | An item 264 | 265 | 266 | Another item 267 | 268 | ``` 269 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "arc/xml", 3 | "type": "library", 4 | "description": "Ariadne Component Library: xml writer and parser Component", 5 | "keywords": ["component","components"], 6 | "homepage": "https://github.com/Ariadne-CMS/arc/wiki", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "Auke van Slooten", 11 | "email": "auke@muze.nl" 12 | } 13 | ], 14 | "require": { 15 | "php": ">=7.3", 16 | "arc/base": "~3.0" 17 | }, 18 | "autoload": { 19 | "psr-4": { 20 | "arc\\": "src/" 21 | } 22 | }, 23 | "require-dev": { 24 | "phpunit/phpunit": "7|8|~9" 25 | }, 26 | "scripts": { 27 | "test": "phpunit --testdox" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | tests/ 6 | 7 | 8 | 9 | 10 | src/ 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/xml.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace arc; 13 | 14 | /** 15 | * Any method statically called on this class 16 | * will reroute the call to the XML writer instance at 17 | * \arc\xml::$writer. Except for the methods: 18 | * parse, css2XPath, name, value, attribute, comment, cdata and preamble 19 | * If you need those call the Writer instance directly 20 | */ 21 | class xml 22 | { 23 | /** 24 | * @var xml\Writer The writer instance to use by default 25 | */ 26 | public static $writer = null; 27 | 28 | public static function __callStatic( $name, $args ) 29 | { 30 | if ( !isset(static::$writer) ) { 31 | static::$writer = new xml\Writer(); 32 | } 33 | return call_user_func_array( [ static::$writer, $name ], $args ); 34 | } 35 | 36 | /** 37 | * This parses an XML string and returns a Proxy 38 | * @param string|Proxy $xml 39 | * @return Proxy 40 | * @throws \arc\UnknownError 41 | */ 42 | public static function parse( $xml=null, $encoding = null ) 43 | { 44 | $parser = new xml\Parser(); 45 | return $parser->parse( $xml, $encoding ); 46 | } 47 | 48 | /** 49 | * This method turns a single CSS 2 selector into an XPath query 50 | * @param string $cssSelector 51 | * @return string 52 | */ 53 | public static function css2XPath( $cssSelector ) 54 | { 55 | $tag = '(?:\w+\|)?\w+'; 56 | /* based on work by Tijs Verkoyen - http://blog.verkoyen.eu/blog/p/detail/css-selector-to-xpath-query/ */ 57 | $translateList = array( 58 | // E F: Matches any F element that is a descendant of an E element 59 | '/('.$tag.')\s+(?=(?:[^"]*"[^"]*")*[^"]*$)('.$tag.')/' 60 | => '\1//\2', 61 | // E > F: Matches any F element that is a child of an element E 62 | '/('.$tag.')\s*>\s*('.$tag.')/' 63 | => '\1/\2', 64 | // E:first-child: Matches element E when E is the first child of its parent 65 | '/('.$tag.'|(\w+\|)?\*):first-child/' 66 | => '*[1]/self::\1', 67 | // Matches E:checked, E:disabled or E:selected (and just for scrutinizer: this is not code!) 68 | '/('.$tag.'|(\w+\|)?\*):(checked|disabled|selected)/' 69 | => '\1 [ @\3 ]', 70 | // E + F: Matches any F element immediately preceded by an element 71 | '/('.$tag.')\s*\+\s*('.$tag.')/' 72 | => '\1/following-sibling::*[1]/self::\2', 73 | // E ~ F: Matches any F element preceded by an element 74 | '/('.$tag.')\s*\~\s*('.$tag.')/' 75 | => '\1/following-sibling::*/self::\2', 76 | // E[foo]: Matches any E element with the "foo" attribute set (whatever the value) 77 | '/('.$tag.')\[([\w\-]+)]/' 78 | => '\1 [ @\2 ]', 79 | // E[foo="warning"]: Matches any E element whose "foo" attribute value is exactly equal to "warning" 80 | '/('.$tag.')\[([\w\-]+)\=\"(.*)\"]/' 81 | => '\1[ contains( concat( " ", normalize-space(@\2), " " ), concat( " ", "\3", " " ) ) ]', 82 | // .warning: HTML only. The same as *[class~="warning"] 83 | '/(^|\s)\.([\w\-]+)+/' 84 | => '*[ contains( concat( " ", normalize-space(@class), " " ), concat( " ", "\2", " " ) ) ]', 85 | // div.warning: HTML only. The same as DIV[class~="warning"] 86 | '/('.$tag.'|(\w+\|)?\*)\.([\w\-]+)+/' 87 | => '\1[ contains( concat( " ", normalize-space(@class), " " ), concat( " ", "\3", " " ) ) ]', 88 | // E#myid: Matches any E element with id-attribute equal to "myid" 89 | '/('.$tag.')\#([\w\-]+)/' 90 | => "\\1[@id='\\2']", 91 | // #myid: Matches any E element with id-attribute equal to "myid" 92 | '/\#([\w\-]+)/' 93 | => "*[@id='\\1']", 94 | // namespace| 95 | '/(\w+)\|/' 96 | => "\\1:" 97 | ); 98 | 99 | $cssSelectors = array_keys($translateList); 100 | $xPathQueries = array_values($translateList); 101 | do { 102 | $continue = false; 103 | $cssSelector = (string) preg_replace($cssSelectors, $xPathQueries, $cssSelector); 104 | foreach ( $cssSelectors as $selector ) { 105 | if ( preg_match($selector, $cssSelector) ) { 106 | $continue = true; 107 | break; 108 | } 109 | } 110 | } while ( $continue ); 111 | return '//'.$cssSelector; 112 | } 113 | 114 | /** 115 | * Returns a guaranteed valid XML name. Removes illegal characters from the name. 116 | * @param string $name 117 | * @return string 118 | */ 119 | public static function name( $name) 120 | { 121 | return preg_replace( '/^[^:a-z_]*/isU', '', 122 | preg_replace( '/[^-.0-9:a-z_]/isU', '', $name 123 | ) ); 124 | } 125 | 126 | /** 127 | * Returns a guaranteed valid XML attribute value. Removes illegal characters. 128 | * @param string|array|bool $value 129 | * @return string 130 | */ 131 | public static function value( $value) 132 | { 133 | if (is_array( $value )) { 134 | $content = array_reduce( $value, function( $result, $value) 135 | { 136 | return $result . ' ' . static::value( $value ); 137 | } ); 138 | } else if (is_bool( $value )) { 139 | $content = $value ? 'true' : 'false'; 140 | } else { 141 | $value = (string) $value; 142 | if (preg_match( '/^\s*'); 170 | } 171 | 172 | /** 173 | * Returns a guaranteed valid XML CDATA string. Removes illegal characters. 174 | * @param string $content 175 | * @return string 176 | */ 177 | public static function cdata( $content) 178 | { 179 | return static::raw('', ']]>', $content ) . ']]>'); 180 | } 181 | 182 | /** 183 | * Returns an XML preamble. 184 | * @param string $version Defaults to '1.0' 185 | * @param string $encoding Defaults to null 186 | * @param string $standalone Defaults to null 187 | * @return string 188 | */ 189 | public static function preamble( $version = '1.0', $encoding = null, $standalone = null) 190 | { 191 | if (isset($standalone)) { 192 | if ($standalone === 'false') { 193 | $standalone = 'no'; 194 | } else if ($standalone !== 'no') { 195 | $standalone = ( $standalone ? 'yes' : 'no' ); 196 | } 197 | $standalone = static::attribute( 'standalone', $standalone ); 198 | } else { 199 | $standalone = ''; 200 | } 201 | $preamble = ''; 206 | return $preamble; 207 | } 208 | 209 | public static function raw( $contents='' ) { 210 | return new xml\RawXML($contents); 211 | } 212 | 213 | } -------------------------------------------------------------------------------- /src/xml/DOMElement.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace arc\xml; 13 | 14 | /** 15 | * This adds docblock information for DOMElement methods and properties 16 | * that are available on the \arc\xml\Proxy class. 17 | * 18 | * @property string $tagName 19 | * @property int $nodeType 20 | * @property Proxy $parentNode 21 | * @property Proxy $firstChild 22 | * @property Proxy $lastChild 23 | * @property Proxy $previousSibling 24 | * @property Proxy $nextSibling 25 | * @property \DOMDocument $ownerDocument 26 | * @property string $namespaceURI 27 | * @property string $prefix 28 | * @property string $localName 29 | * @property string $baseURI 30 | * @property string $textContent 31 | * 32 | * @method string getAttribute( string $name ) 33 | * @method string getAttributeNS( string $namespaceURI, string $localName ) 34 | * @method array getElementsByTagName( string $name ) 35 | * @method array getElementsByTagNameNS( string $namespaceURI, string $localName) 36 | * @method bool hasAttribute( string $name ) 37 | * @method bool hasAttributeNS( string $namespaceURI, string $localname ) 38 | * @method bool removeAttribute( string $name ) 39 | * @method bool removeAttributeNS( string $namespaceURI, string $localName ) 40 | * @method DOMAttr setAttribute( string $name, string $value ) 41 | * @method void setAttributeNS( string $namespaceURI, string $qualifiedName, string $value ) 42 | * @method void setIdAttribute( string $name, bool $isId ) 43 | * @method void setIdAttributeNS( string $namespaceURI, string $localName, bool $isId ) 44 | * @method Proxy appendChild( Proxy $child ) 45 | * @method Proxy cloneNode( bool $deep ) 46 | * @method int getLineNo() 47 | * @method string getNodePath() 48 | * @method bool hasAttributes() 49 | * @method bool hasChildNodes() 50 | * @method Proxy insertBefore( Proxy $newnode, Proxy $refnode ) 51 | * @method bool isDefaultNamespace( string $namespaceURI ) 52 | * @method bool isSameNode( Proxy $node ) 53 | * @method bool isSupported( string $feature, string $version ) 54 | * @method string lookupNamespaceURI( string $prefix ) 55 | * @method string lookupPrefix( string $namespaceURI ) 56 | * @method void normalize() 57 | * @method Proxy removeChild( Proxy $oldnode ) 58 | * @method Proxy replaceChild( Proxy $newnode, Proxy $oldnode ) 59 | */ 60 | interface DOMElement { 61 | 62 | } -------------------------------------------------------------------------------- /src/xml/NodeList.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace arc\xml; 12 | 13 | /** 14 | * This class is used by Writer to represent child nodes. 15 | */ 16 | class NodeList extends \ArrayObject { 17 | use NodeListTrait; 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/xml/NodeListTrait.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace arc\xml; 12 | 13 | /** 14 | * This trait is used by the XML Writer to represent a list of child nodes 15 | */ 16 | trait NodeListTrait { 17 | 18 | protected $writer = null; 19 | protected $invalidChars = []; 20 | 21 | /** 22 | * @param array $list 23 | * @param Writer $writer 24 | */ 25 | public function __construct( $list = null, $writer = null ) 26 | { 27 | parent::__construct( $list ); 28 | $this->writer = $writer; 29 | } 30 | 31 | public function __call( $name, $args ) 32 | { 33 | $tagName = $name; 34 | list( $attributes, $content ) = $this->parseArgs( $args ); 35 | parent::offsetSet( null, $this->element( $tagName, $attributes, $content ) ); 36 | return $this; 37 | } 38 | 39 | public function __toString() 40 | { 41 | $indent = ''; 42 | if (!is_object( $this->writer ) || $this->writer->indent ) { 43 | $indent = "\r\n"; // element() will indent each line with whatever indent string is in the writer 44 | } 45 | return join( $indent, (array) $this ); 46 | } 47 | 48 | protected static function indent( $content, $indent="\t", $newLine="\r\n" ) 49 | { 50 | if ($indent && ( strpos( $content, '<' ) !== false )) { 51 | $indent = ( is_string( $indent ) ? $indent : "\t" ); 52 | return $newLine . preg_replace( '/^(\s*[^\<]*)escape($arg); 70 | } else if (is_array( $arg )) { 71 | foreach( $arg as $key => $subArg ) { 72 | if (is_numeric( $key )) { 73 | list( $subattributes, $subcontent ) = $this->parseArgs( $subArg ); 74 | $attributes = array_merge( $attributes, $subattributes); 75 | $content = \arc\xml::raw( $content . $subcontent ); 76 | } else { 77 | $attributes[ $key ] = $subArg; 78 | } 79 | } 80 | } else { 81 | $content .= $arg; 82 | } 83 | } 84 | return [ $attributes, $content ]; 85 | } 86 | 87 | protected function element( $tagName, $attributes, $content ) 88 | { 89 | $tagName = \arc\xml::name( $tagName ); 90 | $el = '<' . $tagName; 91 | $el .= $this->getAttributes( $attributes ); 92 | if ($this->hasContent( $content )) { 93 | $el .= '>' . self::indent( $content, $this->writer->indent, $this->writer->newLine ); 94 | $el .= ''; 95 | } else { 96 | $el .= '/>'; 97 | } 98 | return $el; 99 | } 100 | 101 | protected function getAttributes( $attributes ) 102 | { 103 | $result = ''; 104 | if (count( $attributes )) { 105 | foreach ($attributes as $name => $value ) { 106 | $result .= \arc\xml::attribute( $name, $value ); 107 | } 108 | } 109 | return $result; 110 | } 111 | 112 | protected function hasContent( $content ) 113 | { 114 | return ( trim( $content ) != '' ); 115 | } 116 | 117 | } 118 | -------------------------------------------------------------------------------- /src/xml/Parser.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace arc\xml; 12 | 13 | /** 14 | * This class implements a XML parser based on DOMDocument->loadXML() 15 | * But it returns a Proxy for both SimpleXMLElement and DOMElement. 16 | * It also allows parsing of partial XML content. 17 | */ 18 | class Parser 19 | { 20 | 21 | /** 22 | * A list of namespaces to use when importing partial xml 23 | * @var string[] $namespaces 24 | */ 25 | public $namespaces = array(); 26 | 27 | /** 28 | * @param array $options Allows you to set the namespaces property immediately 29 | */ 30 | public function __construct( $options = array() ) 31 | { 32 | $optionList = array( 'namespaces' ); 33 | foreach( $options as $option => $optionValue) { 34 | if (in_array( $option, $optionList )) { 35 | $this->{$option} = $optionValue; 36 | } 37 | } 38 | } 39 | 40 | /** 41 | * Parses an XML string and returns a Proxy for it. 42 | * @param string|Proxy|null $xml 43 | * @param string $encoding The character set to use, defaults to UTF-8 44 | * @return Proxy 45 | */ 46 | public function parse( $xml=null, $encoding = null ) 47 | { 48 | if (!$xml) { 49 | return Proxy( null ); 50 | } 51 | if ($xml instanceof Proxy) { // already parsed 52 | return $xml->cloneNode(); 53 | } 54 | $xml = (string) $xml; 55 | try { 56 | return $this->parseFull( $xml, $encoding ); 57 | } catch( \arc\UnknownError $e) { 58 | return $this->parsePartial( $xml, $encoding ); 59 | } 60 | } 61 | 62 | private function parsePartial( $xml, $encoding ) 63 | { 64 | // add a known (single) root element with all declared namespaces 65 | // libxml will barf on multiple root elements 66 | // and it will silently drop namespace prefixes not defined in the document 67 | $root = 'namespaces as $name => $uri) { 69 | if ($name === 0) { 70 | $root .= ' xmlns="'; 71 | } else { 72 | $root .= ' xmlns:'.$name.'="'; 73 | } 74 | $root .= htmlspecialchars( $uri ) . '"'; 75 | } 76 | $root .= '>'; 77 | $result = $this->parseFull( $root.$xml.'', $encoding ); 78 | $result = $result->firstChild->childNodes; 79 | return $result; 80 | } 81 | 82 | private function parseFull( $xml, $encoding = null ) 83 | { 84 | $dom = new \DomDocument(); 85 | if ($encoding) { 86 | $xml = '' . $xml; 87 | } 88 | libxml_disable_entity_loader(); // prevents XXE attacks 89 | $prevErrorSetting = libxml_use_internal_errors(true); 90 | if ($dom->loadXML( $xml )) { 91 | if ($encoding) { 92 | foreach( $dom->childNodes as $item) { 93 | if ($item->nodeType == XML_PI_NODE) { 94 | $dom->removeChild( $item ); 95 | break; 96 | } 97 | } 98 | $dom->encoding = $encoding; 99 | } 100 | libxml_use_internal_errors( $prevErrorSetting ); 101 | return new Proxy( simplexml_import_dom( $dom ), $this ); 102 | } 103 | $errors = libxml_get_errors(); 104 | libxml_clear_errors(); 105 | libxml_use_internal_errors( $prevErrorSetting ); 106 | $message = 'Incorrect xml passed.'; 107 | foreach ($errors as $error) { 108 | $message .= '\nline: '.$error->line.'; column: '.$error->column.'; '.$error->message; 109 | } 110 | throw new \arc\UnknownError( $message, \arc\exceptions::ILLEGAL_ARGUMENT ); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/xml/Proxy.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace arc\xml; 12 | 13 | /** 14 | * This class is a proxy for both the SimpleXMLElement and DOMElement 15 | * properties and methods. 16 | * @property \SimpleXMLElement nodeValue 17 | */ 18 | class Proxy extends \ArrayObject implements DOMElement, SimpleXMLElement { 19 | 20 | use \arc\traits\Proxy { 21 | \arc\traits\Proxy::__construct as private ProxyConstruct; 22 | \arc\traits\Proxy::__call as private ProxyCall; 23 | } 24 | 25 | private $parser = null; 26 | 27 | public function __construct( $node, $parser) { 28 | $this->ProxyConstruct( $node ); 29 | $this->parser = $parser; 30 | } 31 | 32 | public function __toString() { 33 | return isset($this->target) ? (string) $this->target->asXML() : ''; 34 | } 35 | 36 | private function _isDomProperty( $name ) { 37 | $domProperties = [ 38 | 'tagName', 'nodeType', 'parentNode', 39 | 'firstChild', 'lastChild', 'previousSibling', 'nextSibling', 40 | 'ownerDocument', 'namespaceURI', 'prefix', 41 | 'localName', 'baseURI', 'textContent' 42 | ]; 43 | return in_array( $name, $domProperties ); 44 | } 45 | 46 | private function _parseName( $name ) { 47 | $ns = ''; 48 | $name = trim($name); 49 | $prefix = false; 50 | if ( $name[0] == '{' ) { 51 | list($ns, $name) = explode('}', $name); 52 | $ns = substr($ns, 1); 53 | } else if ( strpos($name, ':') !== false ) { 54 | list ($ns, $name) = explode(':', $name); 55 | if ( isset($this->parser->namespaces[$ns]) ) { 56 | $prefix = $ns; 57 | $ns = $this->parser->namespaces[$ns]; 58 | } else { 59 | $prefix = $this->lookupPrefix($ns); 60 | } 61 | } 62 | return [ $ns, $name, $prefix ]; 63 | } 64 | 65 | private function _getTargetProperty($name) { 66 | $value = null; 67 | list( $uri, $name, $prefix ) 68 | = $this->_parseName($name); 69 | if ( $uri ) { 70 | $value = $this->target->children($uri)->{$name}; 71 | } else if ( !$this->_isDomProperty($name) ) { 72 | $value = $this->target->{$name}; 73 | } else { 74 | $dom = dom_import_simplexml($this->target); 75 | if ( isset($dom) ) { 76 | $value = $dom->{$name}; 77 | } 78 | } 79 | return $value; 80 | } 81 | 82 | private function _proxyResult( $value ) { 83 | if ( $value instanceof \DOMElement ) { 84 | $value = simplexml_import_dom($value); 85 | } else if ( $value instanceof \DOMNodeList ) { 86 | $array = []; 87 | for ( $i=0, $l=$value->length; $i<$l; $i ++ ) { 88 | $array[$i] = $value[$i]; 89 | } 90 | $value = $array; 91 | } 92 | if ( $value instanceof \SimpleXMLElement ) { 93 | $value = new static( $value, $this->parser ); 94 | } else if ( is_array($value) ) { 95 | foreach ( $value as $key => $subvalue ) { 96 | $value[$key] = $this->_proxyResult( $subvalue ); 97 | } 98 | } 99 | return $value; 100 | } 101 | 102 | public function __get( $name) { 103 | if ($name == 'nodeValue') { 104 | return $this->target; 105 | } 106 | return $this->_proxyResult( $this->_getTargetProperty($name) ); 107 | } 108 | 109 | public function __set( $name, $value ) { 110 | if ($name == 'nodeValue') { 111 | $this->target = $value; 112 | } else { 113 | list($uri, $name, $prefix) = $this->_parseName($name); 114 | if ( $uri && !$this->isDefaultNamespace($uri) ) { 115 | $el = $this->ownerDocument->createElementNS($uri, $prefix.':'.$name, $value); 116 | $this->appendChild($el); 117 | } else { 118 | $this->target->{$name} = $value; 119 | } 120 | } 121 | } 122 | 123 | private function _domCall( $name, $args ) { 124 | $dom = dom_import_simplexml($this->target); 125 | foreach ( $args as $index => $arg ) { 126 | if ( $arg instanceof \arc\xml\Proxy ) { 127 | $args[$index] = dom_import_simplexml( $arg->nodeValue ); 128 | } else if ( $arg instanceof \SimpleXMLElement ) { 129 | $args[$index] = dom_import_simplexml( $arg ); 130 | } 131 | } 132 | $importMethods = [ 133 | 'appendChild', 'insertBefore', 'replaceChild' 134 | ]; 135 | if ( in_array( $name, $importMethods ) ) { 136 | if ( isset($args[0]) && $args[0] instanceof \DOMNode ) { 137 | if ( $args[0]->ownerDocument !== $this->ownerDocument ) { 138 | $args[0] = $this->ownerDocument->importNode( $args[0], true); 139 | } 140 | } 141 | } 142 | return call_user_func_array( [ $dom, $name], $args ); 143 | } 144 | 145 | public function __call( $name, $args ) { 146 | if ( !method_exists( $this->target, $name ) ) { 147 | return $this->_proxyResult( $this->_domCall( $name, $args ) ); 148 | } else { 149 | return $this->_proxyResult( $this->ProxyCall( $name, $args ) ); 150 | } 151 | } 152 | 153 | /** 154 | * Search through the XML DOM with a single CSS selector 155 | * @param string $query the CSS selector, most CSS 2 selectors work 156 | * @return Proxy 157 | */ 158 | public function find( $query) { 159 | $xpath = \arc\xml::css2Xpath( $query ); 160 | return $this->_proxyResult( $this->target->xpath( $xpath ) ); 161 | } 162 | 163 | /** 164 | * Searches through the subtree for an element with the given id and returns it 165 | * @param string $id 166 | * @return Proxy 167 | */ 168 | public function getElementById( $id ) { 169 | return current($this->find('#'.$id)); 170 | } 171 | 172 | /** 173 | * Register a namespace alias and URI to use in xpath and find 174 | * @param string $prefix the alias for this namespace 175 | * @param string $ns the URI for this namespace 176 | */ 177 | public function registerNamespace( $prefix, $ns ) { 178 | if ( $this->target && $this->target instanceof \SimpleXMLElement ) { 179 | $this->target->registerXPathNamespace($prefix, $ns); 180 | } 181 | $this->parser->namespaces[$prefix] = $ns; 182 | } 183 | 184 | public function offsetGet( $offset ) 185 | { 186 | list( $uri, $name, $prefix ) = $this->_parseName($offset); 187 | if ( $uri ) { 188 | return (string) $this->attributes($uri)[$name]; 189 | } else { 190 | return (string) $this->target[$offset]; 191 | } 192 | } 193 | 194 | public function offsetSet( $offset, $value ) 195 | { 196 | list( $uri, $name, $prefix ) = $this->_parseName($offset); 197 | if ( $uri && !$this->isDefaultNamespace($uri) ) { 198 | $this->setAttributeNS($uri, $prefix.':'.$name, $value); 199 | } else { 200 | $this->target[$name] = $value; 201 | } 202 | } 203 | 204 | public function offsetUnset( $offset ) 205 | { 206 | list( $uri, $name, $prefix ) = $this->_parseName($offset); 207 | if ( $uri ) { 208 | unset( $this->target->attributes($uri)->{$name} ); 209 | } else { 210 | unset( $this->target[$offset] ); 211 | } 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /src/xml/RawXML.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace arc\xml; 12 | 13 | /** 14 | * This is a container for raw xml content, which will not get escaped 15 | * when included by the \arc\xml\Writer or NodeList. 16 | * @property string $contents 17 | */ 18 | class RawXML { 19 | public $contents = ''; 20 | 21 | public function __construct($contents) 22 | { 23 | $this->contents = $contents; 24 | } 25 | 26 | public function __toString() 27 | { 28 | return (string) $this->contents; 29 | } 30 | } -------------------------------------------------------------------------------- /src/xml/SimpleXMLElement.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace arc\xml; 13 | 14 | /** 15 | * This adds docblock information for DOMElement methods that are 16 | * available on the \arc\xml\Proxy class. 17 | * 18 | * @method void addAttribute( string $name, string $value, string $namespace ) 19 | * @method Proxy addChild( string $name, string $value, string $namespace ) 20 | * @method string asXML( strinf $filename ) 21 | * @method Proxy attributes( string $ns, bool $is_prefix ) 22 | * @method Proxy children( string $ns, bool $is_prefix ) 23 | * @method int count() 24 | * @method array getDocNamespaces( bool $recursive, bool $from_root ) 25 | * @method string getName() 26 | * @method array getNamespaces( bool $recursive ) 27 | * @method bool registerXPathNamespace( string $prefix, string $ns ) 28 | * @method array spath( string $path ) 29 | */ 30 | interface SimpleXMLElement { 31 | 32 | } -------------------------------------------------------------------------------- /src/xml/Writer.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace arc\xml; 12 | 13 | /** 14 | * This class allows you to create valid and nicely indented XML strings 15 | * Any method called on it is interpreted as a new XML element to create. 16 | */ 17 | class Writer { 18 | 19 | /** 20 | * @var string $indent The string to ident each level with. Default is a tab. 21 | */ 22 | public $indent = "\t"; 23 | 24 | /** 25 | * @var string $newLine The string to use as a new line or linebreak. Defaults to \r\n. 26 | */ 27 | public $newLine = "\r\n"; 28 | 29 | /** 30 | * @param array $options allows you to set the indent and newLine 31 | * options immediately upon construction 32 | */ 33 | public function __construct( $options = []) 34 | { 35 | $optionList = [ 'indent', 'newLine' ]; 36 | foreach( $options as $option => $optionValue) { 37 | if (in_array( $option, $optionList )) { 38 | $this->{$option} = $optionValue; 39 | } 40 | } 41 | } 42 | 43 | public function __call( $name, $args) 44 | { 45 | return call_user_func_array( [ new NodeList( [], $this ), $name ], $args ); 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /tests/xml.Test.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | 13 | class TestXML extends PHPUnit\Framework\TestCase 14 | { 15 | var $RSSXML = ' 16 | 17 | 18 | Wikipedia 19 | http://www.wikipedia.org 20 | This feed notifies you of new articles on Wikipedia. 21 | 22 | 23 | '; 24 | var $incorrectXML = ""; 25 | var $namespacedXML = "\n\nsomethingsomething elsesomething else again\n"; 26 | 27 | function testXMLBasics() 28 | { 29 | $preamble = \arc\xml::preamble(); 30 | $cdata = \arc\xml::cdata('Some " value'); 31 | $this->assertEquals( (string) $preamble, '' ); 32 | $this->assertEquals( (string) $cdata, '' ); 33 | $comment = \arc\xml::comment('A comment'); 34 | $this->assertEquals( (string) $comment, '' ); 35 | } 36 | 37 | function testXMLWriter() 38 | { 39 | $xml = \arc\xml::ul( [ 'class' => 'menu' ], 40 | \arc\xml::li('menu 1') 41 | ->li('menu 2 ', 42 | \arc\xml::em('emphasized') 43 | ) 44 | ); 45 | $this->assertEquals( "
    \r\n\t
  • menu 1
  • \r\n\t
  • \r\n\t\tmenu 2 emphasized\r\n\t
  • \r\n
", (string) $xml ); 46 | $xml = \arc\xml::ul( 47 | \arc\xml::comment('A comment'), 48 | \arc\xml::cdata('Some < value >') 49 | ); 50 | $this->assertEquals( "
    \r\n\t]]>\r\n
", (string) $xml ); 51 | } 52 | 53 | function testXMLWriterEscaping() 54 | { 55 | $xml = \arc\xml::item( 56 | [ 57 | 'class' => 'in"valid', 58 | 'inva"lid' => "this is \n just wrong" 59 | ], 60 | "Escape me\" < really ©".chr(8), 61 | \arc\xml::subitem( 62 | \arc\xml::raw("this should work") 63 | ) 64 | ); 65 | $expectedValue = <<< EOF 66 | \r 68 | \tEscape me" < really &copy;\r 69 | \t\tthis should work\r 70 | \t\r 71 | 72 | EOF; 73 | $this->assertEquals( $expectedValue, (string) $xml ); 74 | } 75 | 76 | function testXMLParsing() 77 | { 78 | $xml = \arc\xml::parse( $this->RSSXML ); 79 | $error = null; 80 | $xmlString = ''.$xml; 81 | $this->assertEquals( $this->RSSXML, $xmlString ); 82 | $this->assertTrue( $xml->channel->title == 'Wikipedia' ); 83 | $this->assertTrue( $xml->channel->title->nodeValue == 'Wikipedia' ); 84 | 85 | try { 86 | $result = \arc\xml::parse( $this->incorrectXML ); 87 | } catch( \arc\UnknownError $error ) { 88 | } 89 | $this->assertTrue( $error instanceof \arc\UnknownError ); 90 | } 91 | 92 | function testXMLFind() 93 | { 94 | $xml = \arc\xml::parse( $this->RSSXML ); 95 | $title = $xml->find('channel title')[0]; 96 | $this->assertTrue( $title->nodeValue == 'Wikipedia' ); 97 | } 98 | 99 | function testProxyForAttributes() 100 | { 101 | $xml = \arc\xml::parse( $this->RSSXML ); 102 | $this->assertEquals( '2.0', $xml['version'] ); 103 | $attributes = $xml->attributes(); 104 | $this->assertEquals( '2.0', $attributes['version']); 105 | $version = $xml->getAttribute('version'); 106 | $this->assertEquals( '2.0', $version ); 107 | } 108 | 109 | function testCSSSelectors() 110 | { 111 | $xmlString = \arc\xml::{'list'}( 112 | \arc\xml::item(['class' => 'first item', 'id' => 'special'], 'item1', 113 | \arc\xml::input(['type' => 'radio', 'checked' => 'checked' ], 'a radio') 114 | ), 115 | \arc\xml::item(['class' => 'item special-class'], 'item2', 116 | \arc\xml::item(['class' => 'item last', 'data' => 'extra data'], 'item3') 117 | ) 118 | ); 119 | $xml = \arc\xml::parse($xmlString); 120 | $selectors = [ 121 | 'list item' => ['item1','item2','item3'], 122 | 'list > item' => ['item1','item2'], 123 | 'item:first-child' => ['item1','item3'], 124 | 'input:checked' => ['a radio'], 125 | 'item + item' => ['item2'], 126 | 'item ~ item' => ['item2'], 127 | 'item[data]' => ['item3'], 128 | 'item[data="extra data"]' => ['item3'], 129 | 'item[data="extra"]' => ['item3'], 130 | 'item#special' => ['item1'], 131 | '#special' => ['item1'], 132 | 'item.first' => ['item1'], 133 | 'item.last' => ['item3'], 134 | 'item.item' => ['item1', 'item2', 'item3'], 135 | '.first' => ['item1'], 136 | '.last' => ['item3'], 137 | '.item' => ['item1', 'item2', 'item3'], 138 | 'item.special-class' => ['item2'], 139 | 'list > item.item' => ['item1','item2'], 140 | 'list > item > item.last' => ['item3'], 141 | 'list > item ~ item' => ['item2'] 142 | ]; 143 | 144 | foreach ( $selectors as $css => $expectedValues ) { 145 | $result = $xml->find($css); 146 | foreach ( $result as $index => $value ) { 147 | $result[$index] = (string) trim($value->nodeValue); 148 | } 149 | $this->assertEquals( $expectedValues, $result, 'selector: '.$css ); 150 | } 151 | } 152 | 153 | function testXMLUpdates() 154 | { 155 | $xml = \arc\xml::parse( $this->RSSXML ); 156 | $xml->channel->addChild('language','en-us'); 157 | $language = $xml->channel->language->nodeValue; 158 | $this->assertEquals('en-us', (string) $language); 159 | 160 | $xml->channel->language = 'nl-NL'; 161 | $this->assertEquals('nl-NL', (string) $xml->channel->language->nodeValue ); 162 | 163 | $xml['version'] = '1.0'; 164 | $this->assertEquals('1.0', $xml['version']); 165 | 166 | $category = \arc\xml::parse('Encyclopedia'); 167 | $xml->channel->appendChild( $category ); 168 | $cat = $xml->channel->category->nodeValue; 169 | $this->assertEquals('Encyclopedia', (string) $cat); 170 | } 171 | 172 | function testGetElemById() 173 | { 174 | $xmlString = 'item 1item 2'; 175 | $xml = \arc\xml::parse($xmlString); 176 | $item1 = $xml->getElementById('item1'); 177 | $this->assertEquals('item 1', (string) $item1->nodeValue ); 178 | } 179 | 180 | function testNotFound() 181 | { 182 | $xmlString = 'bar'; 183 | $xml = \arc\xml::parse($xmlString); 184 | $baz = $xml->foo->baz; 185 | $this->assertEquals('', (string) $baz ); 186 | $this->assertFalse( isset($baz->nodeValue) ); 187 | } 188 | 189 | function testNamespacedElements() 190 | { 191 | $xmlString = << 193 | 194 | Slashdot 195 | http://slashdot.org/ 196 | News for nerds, stuff that matters 197 | en-us 198 | Copyright 1997-2016, SlashdotMedia. All Rights Reserved. 199 | 2016-01-30T20:38:08+00:00 200 | Dice 201 | help@slashdot.org 202 | Technology 203 | 204 | 205 | EOF; 206 | $xml = \arc\xml::parse($xmlString); 207 | $xml->registerNamespace('dc','http://purl.org/dc/elements/1.1/'); 208 | $xml->registerNamespace('foo','http://purl.org/dc/elements/1.1/'); 209 | $xml->registerNamespace('rdf', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#'); 210 | 211 | $date = current($xml->find("dc|date")); 212 | $this->assertEquals('2016-01-30T20:38:08+00:00', (string) $date->nodeValue); 213 | 214 | $date = current($xml->find("channel > dc|date")); 215 | $this->assertEquals('2016-01-30T20:38:08+00:00', (string) $date->nodeValue); 216 | 217 | $date = current($xml->channel->find("dc|date")); 218 | $this->assertEquals('2016-01-30T20:38:08+00:00', (string) $date->nodeValue); 219 | 220 | $date = $xml->channel->{'dc:date'}; 221 | $this->assertEquals('2016-01-30T20:38:08+00:00', (string) $date->nodeValue); 222 | 223 | $date = $xml->channel->{'{http://purl.org/dc/elements/1.1/}date'}; 224 | $this->assertEquals('2016-01-30T20:38:08+00:00', (string) $date->nodeValue); 225 | 226 | $date = current($xml->find('foo|date')); 227 | $this->assertEquals('2016-01-30T20:38:08+00:00', (string) $date->nodeValue); 228 | 229 | $date = $xml->channel->{'foo:date'}; 230 | $this->assertEquals('2016-01-30T20:38:08+00:00', (string) $date->nodeValue); 231 | 232 | $xml->registerNamespace('bar', 'http://arc.muze.nl/'); 233 | $xml->channel->{'bar:foo'} = 'Bar Foo'; 234 | $this->assertEquals('Bar Foo', (string) $xml->channel->{'bar:foo'}->nodeValue); 235 | } 236 | 237 | function testNamespacedAttributes() 238 | { 239 | $xmlString = << 241 | 242 | Slashdot 243 | http://slashdot.org/ 244 | News for nerds, stuff that matters 245 | en-us 246 | Copyright 1997-2016, SlashdotMedia. All Rights Reserved. 247 | 2016-01-30T20:38:08+00:00 248 | Dice 249 | help@slashdot.org 250 | Technology 251 | 252 | 253 | EOF; 254 | $xml = \arc\xml::parse($xmlString); 255 | $xml->registerNamespace('dc', 'http://purl.org/dc/elements/1.1/'); 256 | $xml->registerNamespace('foo', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#'); 257 | $xml->registerNamespace('rdf', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#'); 258 | 259 | $about = $xml->channel['rdf:about']; 260 | $this->assertEquals('http://slashdot.org/', $about); 261 | 262 | $xml->channel['rdf:about'] = 'About tests'; 263 | $this->assertEquals('About tests', $xml->channel['rdf:about']); 264 | 265 | unset($xml->channel['rdf:about']); 266 | $this->assertEquals('', $xml->channel['rdf:about']); 267 | 268 | $xml->channel['foo:about'] = 'About foo'; 269 | $this->assertEquals('About foo', $xml->channel['foo:about']); 270 | $this->assertEquals('About foo', $xml->channel['rdf:about']); 271 | 272 | $xml->registerNamespace('bar', 'http://arc.muze.nl/'); 273 | $xml->channel['bar:foo'] = 'Bar Foo'; 274 | $this->assertEquals('Bar Foo', $xml->channel['bar:foo']); 275 | } 276 | 277 | } 278 | --------------------------------------------------------------------------------