├── LICENSE ├── README.md ├── composer.json └── lib ├── ContextStackTrait.php ├── Deserializer └── functions.php ├── Element.php ├── Element ├── Base.php ├── Cdata.php ├── Elements.php ├── KeyValue.php ├── Uri.php └── XmlFragment.php ├── LibXMLException.php ├── ParseException.php ├── Reader.php ├── Serializer └── functions.php ├── Service.php ├── Version.php ├── Writer.php ├── XmlDeserializable.php └── XmlSerializable.php /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2009-2015 fruux GmbH (https://fruux.com/) 2 | 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without modification, 6 | are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright notice, 9 | this list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | * Neither the name Sabre nor the names of its contributors 14 | may be used to endorse or promote products derived from this software 15 | without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 18 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 19 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 20 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 21 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 22 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 23 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 24 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 25 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 26 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 27 | POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | sabre/xml 2 | ========= 3 | 4 | ![Build Status](https://github.com/sabre-io/xml/actions/workflows/ci.yml/badge.svg) 5 | 6 | The sabre/xml library is a specialized XML reader and writer. 7 | 8 | Documentation 9 | ------------- 10 | 11 | * [Introduction](http://sabre.io/xml/). 12 | * [Installation](http://sabre.io/xml/install/). 13 | * [Reading XML](http://sabre.io/xml/reading/). 14 | * [Writing XML](http://sabre.io/xml/writing/). 15 | 16 | Major version 3 implements type declarations for input parameters, function returns, variables etc. It supports PHP 7.4 and PHP 8. When you upgrade to major version 3, if you extend classes etc., then you will need to make similar type declarations in your code. 17 | 18 | Support 19 | ------- 20 | 21 | Head over to the [SabreDAV mailing list](http://groups.google.com/group/sabredav-discuss) for any questions. 22 | 23 | Made at fruux 24 | ------------- 25 | 26 | This library is being developed by [fruux](https://fruux.com/). Drop us a line for commercial services or enterprise support. 27 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sabre/xml", 3 | "description" : "sabre/xml is an XML library that you may not hate.", 4 | "keywords" : [ "XML", "XMLReader", "XMLWriter", "DOM" ], 5 | "homepage" : "https://sabre.io/xml/", 6 | "license" : "BSD-3-Clause", 7 | "require" : { 8 | "php" : "^7.4 || ^8.0", 9 | "ext-xmlwriter" : "*", 10 | "ext-xmlreader" : "*", 11 | "ext-dom" : "*", 12 | "lib-libxml" : ">=2.6.20", 13 | "sabre/uri" : ">=2.0,<4.0.0" 14 | }, 15 | "authors" : [ 16 | { 17 | "name" : "Evert Pot", 18 | "email" : "me@evertpot.com", 19 | "homepage" : "http://evertpot.com/", 20 | "role" : "Developer" 21 | }, 22 | { 23 | "name": "Markus Staab", 24 | "email": "markus.staab@redaxo.de", 25 | "role" : "Developer" 26 | } 27 | ], 28 | "support" : { 29 | "forum" : "https://groups.google.com/group/sabredav-discuss", 30 | "source" : "https://github.com/fruux/sabre-xml" 31 | }, 32 | "autoload" : { 33 | "psr-4" : { 34 | "Sabre\\Xml\\" : "lib/" 35 | }, 36 | "files": [ 37 | "lib/Deserializer/functions.php", 38 | "lib/Serializer/functions.php" 39 | ] 40 | }, 41 | "autoload-dev" : { 42 | "psr-4" : { 43 | "Sabre\\Xml\\" : "tests/Sabre/Xml/" 44 | } 45 | }, 46 | "require-dev": { 47 | "friendsofphp/php-cs-fixer": "^3.64", 48 | "phpstan/phpstan": "^1.12", 49 | "phpunit/phpunit" : "^9.6" 50 | }, 51 | "scripts": { 52 | "phpstan": [ 53 | "phpstan analyse" 54 | ], 55 | "phpstan-baseline": [ 56 | "phpstan analyse --generate-baseline phpstan-baseline.neon" 57 | ], 58 | "cs-fixer": [ 59 | "PHP_CS_FIXER_IGNORE_ENV=true php-cs-fixer fix" 60 | ], 61 | "phpunit": [ 62 | "phpunit --configuration tests/phpunit.xml" 63 | ], 64 | "test": [ 65 | "composer phpstan", 66 | "composer cs-fixer", 67 | "composer phpunit" 68 | ] 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /lib/ContextStackTrait.php: -------------------------------------------------------------------------------- 1 | 36 | */ 37 | public array $elementMap = []; 38 | 39 | /** 40 | * A contextUri pointing to the document being parsed / written. 41 | * This uri may be used to resolve relative urls that may appear in the 42 | * document. 43 | * 44 | * The reader and writer don't use this property, but as it's an extremely 45 | * common use-case for parsing XML documents, it's added here as a 46 | * convenience. 47 | */ 48 | public ?string $contextUri = null; 49 | 50 | /** 51 | * This is a list of namespaces that you want to give default prefixes. 52 | * 53 | * You must make sure you create this entire list before starting to write. 54 | * They should be registered on the root element. 55 | * 56 | * @phpstan-var array 57 | */ 58 | public array $namespaceMap = []; 59 | 60 | /** 61 | * This is a list of custom serializers for specific classes. 62 | * 63 | * The writer may use this if you attempt to serialize an object with a 64 | * class that does not implement XmlSerializable. 65 | * 66 | * Instead, it will look at this classmap to see if there is a custom 67 | * serializer here. This is useful if you don't want your value objects 68 | * to be responsible for serializing themselves. 69 | * 70 | * The keys in this classmap need to be fully qualified PHP class names, 71 | * the values must be callbacks. The callbacks take two arguments. The 72 | * writer class, and the value that must be written. 73 | * 74 | * function (Writer $writer, object $value) 75 | * 76 | * @phpstan-var array 77 | */ 78 | public array $classMap = []; 79 | 80 | /** 81 | * Backups of previous contexts. 82 | * 83 | * @var list 84 | */ 85 | protected array $contextStack = []; 86 | 87 | /** 88 | * Create a new "context". 89 | * 90 | * This allows you to safely modify the elementMap, contextUri or 91 | * namespaceMap. After you're done, you can restore the old data again 92 | * with popContext. 93 | */ 94 | public function pushContext(): void 95 | { 96 | $this->contextStack[] = [ 97 | $this->elementMap, 98 | $this->contextUri, 99 | $this->namespaceMap, 100 | $this->classMap, 101 | ]; 102 | } 103 | 104 | /** 105 | * Restore the previous "context". 106 | */ 107 | public function popContext(): void 108 | { 109 | list( 110 | $this->elementMap, 111 | $this->contextUri, 112 | $this->namespaceMap, 113 | $this->classMap, 114 | ) = array_pop($this->contextStack); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /lib/Deserializer/functions.php: -------------------------------------------------------------------------------- 1 | value" array. 22 | * 23 | * For example, keyvalue will parse: 24 | * 25 | * 26 | * 27 | * value1 28 | * value2 29 | * 30 | * 31 | * 32 | * Into: 33 | * 34 | * [ 35 | * "{http://sabredav.org/ns}elem1" => "value1", 36 | * "{http://sabredav.org/ns}elem2" => "value2", 37 | * "{http://sabredav.org/ns}elem3" => null, 38 | * ]; 39 | * 40 | * If you specify the 'namespace' argument, the deserializer will remove 41 | * the namespaces of the keys that match that namespace. 42 | * 43 | * For example, if you call keyValue like this: 44 | * 45 | * keyValue($reader, 'http://sabredav.org/ns') 46 | * 47 | * it's output will instead be: 48 | * 49 | * [ 50 | * "elem1" => "value1", 51 | * "elem2" => "value2", 52 | * "elem3" => null, 53 | * ]; 54 | * 55 | * Attributes will be removed from the top-level elements. If elements with 56 | * the same name appear twice in the list, only the last one will be kept. 57 | * 58 | * @phpstan-return array 59 | */ 60 | function keyValue(Reader $reader, ?string $namespace = null): array 61 | { 62 | // If there's no children, we don't do anything. 63 | if ($reader->isEmptyElement) { 64 | $reader->next(); 65 | 66 | return []; 67 | } 68 | 69 | if (!$reader->read()) { 70 | $reader->next(); 71 | 72 | return []; 73 | } 74 | 75 | if (Reader::END_ELEMENT === $reader->nodeType) { 76 | $reader->next(); 77 | 78 | return []; 79 | } 80 | 81 | $values = []; 82 | 83 | do { 84 | if (Reader::ELEMENT === $reader->nodeType) { 85 | if (null !== $namespace && $reader->namespaceURI === $namespace) { 86 | $values[$reader->localName] = $reader->parseCurrentElement()['value']; 87 | } else { 88 | $clark = $reader->getClark(); 89 | $values[$clark] = $reader->parseCurrentElement()['value']; 90 | } 91 | } else { 92 | if (!$reader->read()) { 93 | break; 94 | } 95 | } 96 | } while (Reader::END_ELEMENT !== $reader->nodeType); 97 | 98 | $reader->read(); 99 | 100 | return $values; 101 | } 102 | 103 | /** 104 | * The 'enum' deserializer parses elements into a simple list 105 | * without values or attributes. 106 | * 107 | * For example, Elements will parse: 108 | * 109 | * 110 | * 111 | * 112 | * 113 | * 114 | * content 115 | * 116 | * 117 | * 118 | * Into: 119 | * 120 | * [ 121 | * "{http://sabredav.org/ns}elem1", 122 | * "{http://sabredav.org/ns}elem2", 123 | * "{http://sabredav.org/ns}elem3", 124 | * "{http://sabredav.org/ns}elem4", 125 | * "{http://sabredav.org/ns}elem5", 126 | * ]; 127 | * 128 | * This is useful for 'enum'-like structures. 129 | * 130 | * If the $namespace argument is specified, it will strip the namespace 131 | * for all elements that match that. 132 | * 133 | * For example, 134 | * 135 | * enum($reader, 'http://sabredav.org/ns') 136 | * 137 | * would return: 138 | * 139 | * [ 140 | * "elem1", 141 | * "elem2", 142 | * "elem3", 143 | * "elem4", 144 | * "elem5", 145 | * ]; 146 | * 147 | * @return string[] 148 | * 149 | * @phpstan-return list 150 | */ 151 | function enum(Reader $reader, ?string $namespace = null): array 152 | { 153 | // If there's no children, we don't do anything. 154 | if ($reader->isEmptyElement) { 155 | $reader->next(); 156 | 157 | return []; 158 | } 159 | if (!$reader->read()) { 160 | $reader->next(); 161 | 162 | return []; 163 | } 164 | 165 | if (Reader::END_ELEMENT === $reader->nodeType) { 166 | $reader->next(); 167 | 168 | return []; 169 | } 170 | $currentDepth = $reader->depth; 171 | 172 | $values = []; 173 | do { 174 | if (Reader::ELEMENT !== $reader->nodeType) { 175 | continue; 176 | } 177 | if (!is_null($namespace) && $namespace === $reader->namespaceURI) { 178 | $values[] = $reader->localName; 179 | } else { 180 | $values[] = (string) $reader->getClark(); 181 | } 182 | } while ($reader->depth >= $currentDepth && $reader->next()); 183 | 184 | $reader->next(); 185 | 186 | return $values; 187 | } 188 | 189 | /** 190 | * The valueObject deserializer turns an XML element into a PHP object of 191 | * a specific class. 192 | * 193 | * This is primarily used by the mapValueObject function from the Service 194 | * class, but it can also easily be used for more specific situations. 195 | * 196 | * @template C of object 197 | * 198 | * @param class-string $className 199 | * 200 | * @phpstan-return C 201 | */ 202 | function valueObject(Reader $reader, string $className, string $namespace): object 203 | { 204 | $valueObject = new $className(); 205 | if ($reader->isEmptyElement) { 206 | $reader->next(); 207 | 208 | return $valueObject; 209 | } 210 | 211 | $defaultProperties = get_class_vars($className); 212 | 213 | $reader->read(); 214 | do { 215 | if (Reader::ELEMENT === $reader->nodeType && $reader->namespaceURI == $namespace) { 216 | if (property_exists($valueObject, $reader->localName)) { 217 | if (is_array($defaultProperties[$reader->localName])) { 218 | $valueObject->{$reader->localName}[] = $reader->parseCurrentElement()['value']; 219 | } else { 220 | $valueObject->{$reader->localName} = $reader->parseCurrentElement()['value']; 221 | } 222 | } else { 223 | // Ignore property 224 | $reader->next(); 225 | } 226 | } elseif (Reader::ELEMENT === $reader->nodeType) { 227 | // Skipping element from different namespace 228 | $reader->next(); 229 | } else { 230 | if (Reader::END_ELEMENT !== $reader->nodeType && !$reader->read()) { 231 | break; 232 | } 233 | } 234 | } while (Reader::END_ELEMENT !== $reader->nodeType); 235 | 236 | $reader->read(); 237 | 238 | return $valueObject; 239 | } 240 | 241 | /** 242 | * This deserializer helps you deserialize xml structures that look like 243 | * this:. 244 | * 245 | * 246 | * ... 247 | * ... 248 | * ... 249 | * 250 | * 251 | * Many XML documents use patterns like that, and this deserializer 252 | * allow you to get all the 'items' as an array. 253 | * 254 | * In that previous example, you would register the deserializer as such: 255 | * 256 | * $reader->elementMap['{}collection'] = function($reader) { 257 | * return repeatingElements($reader, '{}item'); 258 | * } 259 | * 260 | * The repeatingElements deserializer simply returns everything as an array. 261 | * 262 | * $childElementName must either be a clark-notation element name, or if no 263 | * namespace is used, the bare element name. 264 | * 265 | * @phpstan-return list 266 | */ 267 | function repeatingElements(Reader $reader, string $childElementName): array 268 | { 269 | if ('{' !== $childElementName[0]) { 270 | $childElementName = '{}'.$childElementName; 271 | } 272 | $result = []; 273 | 274 | foreach ($reader->parseGetElements() as $element) { 275 | if ($element['name'] === $childElementName) { 276 | $result[] = $element['value']; 277 | } 278 | } 279 | 280 | return $result; 281 | } 282 | 283 | /** 284 | * This deserializer helps you to deserialize structures which contain mixed content. 285 | * 286 | *

some text and an inline tagand even more text

287 | * 288 | * The above example will return 289 | * 290 | * [ 291 | * 'some text', 292 | * [ 293 | * 'name' => '{}extref', 294 | * 'value' => 'and an inline tag', 295 | * 'attributes' => [] 296 | * ], 297 | * 'and even more text' 298 | * ] 299 | * 300 | * In strict XML documents you won't find this kind of markup but in html this is a quite common pattern. 301 | * 302 | * @return array 303 | */ 304 | function mixedContent(Reader $reader): array 305 | { 306 | // If there's no children, we don't do anything. 307 | if ($reader->isEmptyElement) { 308 | $reader->next(); 309 | 310 | return []; 311 | } 312 | 313 | $previousDepth = $reader->depth; 314 | 315 | $content = []; 316 | $reader->read(); 317 | while (true) { 318 | if (Reader::ELEMENT == $reader->nodeType) { 319 | $content[] = $reader->parseCurrentElement(); 320 | } elseif ($reader->depth >= $previousDepth && in_array($reader->nodeType, [Reader::TEXT, Reader::CDATA, Reader::WHITESPACE])) { 321 | $content[] = $reader->value; 322 | $reader->read(); 323 | } elseif (Reader::END_ELEMENT == $reader->nodeType) { 324 | // Ensuring we are moving the cursor after the end element. 325 | $reader->read(); 326 | break; 327 | } else { 328 | $reader->read(); 329 | } 330 | } 331 | 332 | return $content; 333 | } 334 | 335 | /** 336 | * The functionCaller deserializer turns an XML element into whatever your callable returns. 337 | * 338 | * You can use, e.g., a named constructor (factory method) to create an object using 339 | * this function. 340 | * 341 | * @return mixed whatever the 'func' callable returns 342 | * 343 | * @throws \InvalidArgumentException|\ReflectionException 344 | */ 345 | function functionCaller(Reader $reader, callable $func, string $namespace) 346 | { 347 | if ($reader->isEmptyElement) { 348 | $reader->next(); 349 | 350 | return null; 351 | } 352 | 353 | $funcArgs = []; 354 | if (is_array($func)) { 355 | $ref = new \ReflectionMethod($func[0], $func[1]); 356 | } elseif (is_string($func) && false !== strpos($func, '::')) { 357 | // We have a string that should refer to a method that exists, like "MyClass::someMethod" 358 | // ReflectionMethod knows how to handle that as-is 359 | $ref = new \ReflectionMethod($func); 360 | } elseif ($func instanceof \Closure || is_string($func)) { 361 | // We have an actual Closure (a real function) or a string that is the name of a function 362 | // ReflectionFunction can take either of those 363 | $ref = new \ReflectionFunction($func); 364 | } else { 365 | throw new \InvalidArgumentException(__METHOD__.' unable to use func parameter with ReflectionMethod or ReflectionFunction.'); 366 | } 367 | 368 | foreach ($ref->getParameters() as $parameter) { 369 | $funcArgs[$parameter->getName()] = null; 370 | } 371 | 372 | $reader->read(); 373 | do { 374 | if (Reader::ELEMENT === $reader->nodeType && $reader->namespaceURI == $namespace) { 375 | if (array_key_exists($reader->localName, $funcArgs)) { 376 | $funcArgs[$reader->localName] = $reader->parseCurrentElement()['value']; 377 | } else { 378 | // Ignore property 379 | $reader->next(); 380 | } 381 | } else { 382 | $reader->read(); 383 | } 384 | } while (Reader::END_ELEMENT !== $reader->nodeType); 385 | $reader->read(); 386 | 387 | return $func(...array_values($funcArgs)); 388 | } 389 | -------------------------------------------------------------------------------- /lib/Element.php: -------------------------------------------------------------------------------- 1 | value = $value; 35 | } 36 | 37 | /** 38 | * The xmlSerialize method is called during xml writing. 39 | * 40 | * Use the $writer argument to write its own xml serialization. 41 | * 42 | * An important note: do _not_ create a parent element. Any element 43 | * implementing XmlSerializable should only ever write what's considered 44 | * its 'inner xml'. 45 | * 46 | * The parent of the current element is responsible for writing a 47 | * containing element. 48 | * 49 | * This allows serializers to be re-used for different element names. 50 | * 51 | * If you are opening new elements, you must also close them again. 52 | */ 53 | public function xmlSerialize(Xml\Writer $writer): void 54 | { 55 | $writer->write($this->value); 56 | } 57 | 58 | /** 59 | * The deserialize method is called during xml parsing. 60 | * 61 | * This method is called statically, this is because in theory this method 62 | * may be used as a type of constructor, or factory method. 63 | * 64 | * Often you want to return an instance of the current class, but you are 65 | * free to return other data as well. 66 | * 67 | * Important note 2: You are responsible for advancing the reader to the 68 | * next element. Not doing anything will result in a never-ending loop. 69 | * 70 | * If you just want to skip parsing for this element altogether, you can 71 | * just call $reader->next(); 72 | * 73 | * $reader->parseInnerTree() will parse the entire sub-tree, and advance to 74 | * the next element. 75 | * 76 | * @return array>|string|null 77 | */ 78 | public static function xmlDeserialize(Xml\Reader $reader) 79 | { 80 | $subTree = $reader->parseInnerTree(); 81 | 82 | return $subTree; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /lib/Element/Cdata.php: -------------------------------------------------------------------------------- 1 | value = $value; 35 | } 36 | 37 | /** 38 | * The xmlSerialize method is called during xml writing. 39 | * 40 | * Use the $writer argument to write its own xml serialization. 41 | * 42 | * An important note: do _not_ create a parent element. Any element 43 | * implementing XmlSerializable should only ever write what's considered 44 | * its 'inner xml'. 45 | * 46 | * The parent of the current element is responsible for writing a 47 | * containing element. 48 | * 49 | * This allows serializers to be re-used for different element names. 50 | * 51 | * If you are opening new elements, you must also close them again. 52 | */ 53 | public function xmlSerialize(Xml\Writer $writer): void 54 | { 55 | $writer->writeCData($this->value); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /lib/Element/Elements.php: -------------------------------------------------------------------------------- 1 | 16 | * 17 | * 18 | * 19 | * 20 | * content 21 | * 22 | * 23 | * 24 | * Into: 25 | * 26 | * [ 27 | * "{http://sabredav.org/ns}elem1", 28 | * "{http://sabredav.org/ns}elem2", 29 | * "{http://sabredav.org/ns}elem3", 30 | * "{http://sabredav.org/ns}elem4", 31 | * "{http://sabredav.org/ns}elem5", 32 | * ]; 33 | * 34 | * @copyright Copyright (C) 2009-2015 fruux GmbH (https://fruux.com/). 35 | * @author Evert Pot (http://evertpot.com/) 36 | * @license http://sabre.io/license/ Modified BSD License 37 | */ 38 | class Elements implements Xml\Element 39 | { 40 | /** 41 | * Value to serialize. 42 | * 43 | * @var array 44 | */ 45 | protected array $value; 46 | 47 | /** 48 | * Constructor. 49 | * 50 | * @param array $value 51 | */ 52 | public function __construct(array $value = []) 53 | { 54 | $this->value = $value; 55 | } 56 | 57 | /** 58 | * The xmlSerialize method is called during xml writing. 59 | * 60 | * Use the $writer argument to write its own xml serialization. 61 | * 62 | * An important note: do _not_ create a parent element. Any element 63 | * implementing XmlSerializable should only ever write what's considered 64 | * its 'inner xml'. 65 | * 66 | * The parent of the current element is responsible for writing a 67 | * containing element. 68 | * 69 | * This allows serializers to be re-used for different element names. 70 | * 71 | * If you are opening new elements, you must also close them again. 72 | */ 73 | public function xmlSerialize(Xml\Writer $writer): void 74 | { 75 | Serializer\enum($writer, $this->value); 76 | } 77 | 78 | /** 79 | * The deserialize method is called during xml parsing. 80 | * 81 | * This method is called statically, this is because in theory this method 82 | * may be used as a type of constructor, or factory method. 83 | * 84 | * Often you want to return an instance of the current class, but you are 85 | * free to return other data as well. 86 | * 87 | * Important note 2: You are responsible for advancing the reader to the 88 | * next element. Not doing anything will result in a never-ending loop. 89 | * 90 | * If you just want to skip parsing for this element altogether, you can 91 | * just call $reader->next(); 92 | * 93 | * $reader->parseSubTree() will parse the entire sub-tree, and advance to 94 | * the next element. 95 | * 96 | * @return string[] 97 | */ 98 | public static function xmlDeserialize(Xml\Reader $reader) 99 | { 100 | return Deserializer\enum($reader); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /lib/Element/KeyValue.php: -------------------------------------------------------------------------------- 1 | value struct. 13 | * 14 | * Attributes will be removed, and duplicate child elements are discarded. 15 | * Complex values within the elements will be parsed by the 'standard' parser. 16 | * 17 | * For example, KeyValue will parse: 18 | * 19 | * 20 | * 21 | * value1 22 | * value2 23 | * 24 | * 25 | * 26 | * Into: 27 | * 28 | * [ 29 | * "{http://sabredav.org/ns}elem1" => "value1", 30 | * "{http://sabredav.org/ns}elem2" => "value2", 31 | * "{http://sabredav.org/ns}elem3" => null, 32 | * ]; 33 | * 34 | * @copyright Copyright (C) 2009-2015 fruux GmbH (https://fruux.com/). 35 | * @author Evert Pot (http://evertpot.com/) 36 | * @license http://sabre.io/license/ Modified BSD License 37 | */ 38 | class KeyValue implements Xml\Element 39 | { 40 | /** 41 | * Value to serialize. 42 | * 43 | * @var array 44 | */ 45 | protected array $value; 46 | 47 | /** 48 | * Constructor. 49 | * 50 | * @param array $value 51 | */ 52 | public function __construct(array $value = []) 53 | { 54 | $this->value = $value; 55 | } 56 | 57 | /** 58 | * The xmlSerialize method is called during xml writing. 59 | * 60 | * Use the $writer argument to write its own xml serialization. 61 | * 62 | * An important note: do _not_ create a parent element. Any element 63 | * implementing XmlSerializable should only ever write what's considered 64 | * its 'inner xml'. 65 | * 66 | * The parent of the current element is responsible for writing a 67 | * containing element. 68 | * 69 | * This allows serializers to be re-used for different element names. 70 | * 71 | * If you are opening new elements, you must also close them again. 72 | */ 73 | public function xmlSerialize(Xml\Writer $writer): void 74 | { 75 | $writer->write($this->value); 76 | } 77 | 78 | /** 79 | * The deserialize method is called during xml parsing. 80 | * 81 | * This method is called statically, this is because in theory this method 82 | * may be used as a type of constructor, or factory method. 83 | * 84 | * Often you want to return an instance of the current class, but you are 85 | * free to return other data as well. 86 | * 87 | * Important note 2: You are responsible for advancing the reader to the 88 | * next element. Not doing anything will result in a never-ending loop. 89 | * 90 | * If you just want to skip parsing for this element altogether, you can 91 | * just call $reader->next(); 92 | * 93 | * $reader->parseInnerTree() will parse the entire sub-tree, and advance to 94 | * the next element. 95 | * 96 | * @return array 97 | */ 98 | public static function xmlDeserialize(Xml\Reader $reader) 99 | { 100 | return Deserializer\keyValue($reader); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /lib/Element/Uri.php: -------------------------------------------------------------------------------- 1 | /foo/bar 17 | * http://example.org/hi 18 | * 19 | * If the uri is relative, it will be automatically expanded to an absolute 20 | * url during writing and reading, if the contextUri property is set on the 21 | * reader and/or writer. 22 | * 23 | * @copyright Copyright (C) 2009-2015 fruux GmbH (https://fruux.com/). 24 | * @author Evert Pot (http://evertpot.com/) 25 | * @license http://sabre.io/license/ Modified BSD License 26 | */ 27 | class Uri implements Xml\Element 28 | { 29 | /** 30 | * Uri element value. 31 | */ 32 | protected string $value; 33 | 34 | /** 35 | * Constructor. 36 | */ 37 | public function __construct(string $value) 38 | { 39 | $this->value = $value; 40 | } 41 | 42 | /** 43 | * The xmlSerialize method is called during xml writing. 44 | * 45 | * Use the $writer argument to write its own xml serialization. 46 | * 47 | * An important note: do _not_ create a parent element. Any element 48 | * implementing XmlSerializable should only ever write what's considered 49 | * its 'inner xml'. 50 | * 51 | * The parent of the current element is responsible for writing a 52 | * containing element. 53 | * 54 | * This allows serializers to be re-used for different element names. 55 | * 56 | * If you are opening new elements, you must also close them again. 57 | */ 58 | public function xmlSerialize(Xml\Writer $writer): void 59 | { 60 | $writer->text( 61 | resolve( 62 | $writer->contextUri ?? '', 63 | $this->value 64 | ) 65 | ); 66 | } 67 | 68 | /** 69 | * This method is called during xml parsing. 70 | * 71 | * This method is called statically, this is because in theory this method 72 | * may be used as a type of constructor, or factory method. 73 | * 74 | * Often you want to return an instance of the current class, but you are 75 | * free to return other data as well. 76 | * 77 | * Important note 2: You are responsible for advancing the reader to the 78 | * next element. Not doing anything will result in a never-ending loop. 79 | * 80 | * If you just want to skip parsing for this element altogether, you can 81 | * just call $reader->next(); 82 | * 83 | * $reader->parseSubTree() will parse the entire sub-tree, and advance to 84 | * the next element. 85 | */ 86 | public static function xmlDeserialize(Xml\Reader $reader) 87 | { 88 | return new self( 89 | resolve( 90 | (string) $reader->contextUri, 91 | $reader->readText() 92 | ) 93 | ); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /lib/Element/XmlFragment.php: -------------------------------------------------------------------------------- 1 | xml = $xml; 38 | } 39 | 40 | /** 41 | * Returns the inner XML document. 42 | */ 43 | public function getXml(): string 44 | { 45 | return $this->xml; 46 | } 47 | 48 | /** 49 | * The xmlSerialize method is called during xml writing. 50 | * 51 | * Use the $writer argument to write its own xml serialization. 52 | * 53 | * An important note: do _not_ create a parent element. Any element 54 | * implementing XmlSerializable should only ever write what's considered 55 | * its 'inner xml'. 56 | * 57 | * The parent of the current element is responsible for writing a 58 | * containing element. 59 | * 60 | * This allows serializers to be re-used for different element names. 61 | * 62 | * If you are opening new elements, you must also close them again. 63 | */ 64 | public function xmlSerialize(Writer $writer): void 65 | { 66 | $reader = new Reader(); 67 | 68 | // Wrapping the xml in a container, so root-less values can still be 69 | // parsed. 70 | $xml = << 72 | {$this->getXml()} 73 | XML; 74 | 75 | $reader->xml($xml); 76 | 77 | while ($reader->read()) { 78 | if ($reader->depth < 1) { 79 | // Skipping the root node. 80 | continue; 81 | } 82 | 83 | switch ($reader->nodeType) { 84 | case Reader::ELEMENT: 85 | $writer->startElement( 86 | (string) $reader->getClark() 87 | ); 88 | $empty = $reader->isEmptyElement; 89 | while ($reader->moveToNextAttribute()) { 90 | switch ($reader->namespaceURI) { 91 | case '': 92 | $writer->writeAttribute($reader->localName, $reader->value); 93 | break; 94 | case 'http://www.w3.org/2000/xmlns/': 95 | // Skip namespace declarations 96 | break; 97 | default: 98 | $writer->writeAttribute((string) $reader->getClark(), $reader->value); 99 | break; 100 | } 101 | } 102 | if ($empty) { 103 | $writer->endElement(); 104 | } 105 | break; 106 | case Reader::CDATA: 107 | case Reader::TEXT: 108 | $writer->text( 109 | $reader->value 110 | ); 111 | break; 112 | case Reader::END_ELEMENT: 113 | $writer->endElement(); 114 | break; 115 | } 116 | } 117 | } 118 | 119 | /** 120 | * The deserialize method is called during xml parsing. 121 | * 122 | * This method is called statically, this is because in theory this method 123 | * may be used as a type of constructor, or factory method. 124 | * 125 | * Often you want to return an instance of the current class, but you are 126 | * free to return other data as well. 127 | * 128 | * You are responsible for advancing the reader to the next element. Not 129 | * doing anything will result in a never-ending loop. 130 | * 131 | * If you just want to skip parsing for this element altogether, you can 132 | * just call $reader->next(); 133 | * 134 | * $reader->parseInnerTree() will parse the entire sub-tree, and advance to 135 | * the next element. 136 | */ 137 | public static function xmlDeserialize(Reader $reader) 138 | { 139 | $result = new self($reader->readInnerXml()); 140 | $reader->next(); 141 | 142 | return $result; 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /lib/LibXMLException.php: -------------------------------------------------------------------------------- 1 | errors = $errors; 37 | parent::__construct($errors[0]->message.' on line '.$errors[0]->line.', column '.$errors[0]->column, $code, $previousException); 38 | } 39 | 40 | /** 41 | * Returns the LibXML errors. 42 | * 43 | * @return \LibXMLError[] 44 | */ 45 | public function getErrors(): array 46 | { 47 | return $this->errors; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /lib/ParseException.php: -------------------------------------------------------------------------------- 1 | localName) { 37 | return null; 38 | } 39 | 40 | return '{'.$this->namespaceURI.'}'.$this->localName; 41 | } 42 | 43 | /** 44 | * Reads the entire document. 45 | * 46 | * This function returns an array with the following three elements: 47 | * * name - The root element name. 48 | * * value - The value for the root element. 49 | * * attributes - An array of attributes. 50 | * 51 | * This function will also disable the standard libxml error handler (which 52 | * usually just results in PHP errors), and throw exceptions instead. 53 | * 54 | * @return array 55 | */ 56 | public function parse(): array 57 | { 58 | $previousEntityState = null; 59 | $shouldCallLibxmlDisableEntityLoader = (\LIBXML_VERSION < 20900); 60 | if ($shouldCallLibxmlDisableEntityLoader) { 61 | $previousEntityState = libxml_disable_entity_loader(true); 62 | } 63 | $previousSetting = libxml_use_internal_errors(true); 64 | 65 | try { 66 | while (self::ELEMENT !== $this->nodeType) { 67 | if (!$this->read()) { 68 | $errors = libxml_get_errors(); 69 | libxml_clear_errors(); 70 | if ($errors) { 71 | throw new LibXMLException($errors); 72 | } 73 | } 74 | } 75 | $result = $this->parseCurrentElement(); 76 | 77 | // last line of defense in case errors did occur above 78 | $errors = libxml_get_errors(); 79 | libxml_clear_errors(); 80 | if ($errors) { 81 | throw new LibXMLException($errors); 82 | } 83 | } finally { 84 | libxml_use_internal_errors($previousSetting); 85 | if ($shouldCallLibxmlDisableEntityLoader) { 86 | libxml_disable_entity_loader($previousEntityState); 87 | } 88 | } 89 | 90 | return $result; 91 | } 92 | 93 | /** 94 | * parseGetElements parses everything in the current sub-tree, 95 | * and returns an array of elements. 96 | * 97 | * Each element has a 'name', 'value' and 'attributes' key. 98 | * 99 | * If the element didn't contain sub-elements, an empty array is always 100 | * returned. If there was any text inside the element, it will be 101 | * discarded. 102 | * 103 | * If the $elementMap argument is specified, the existing elementMap will 104 | * be overridden while parsing the tree, and restored after this process. 105 | * 106 | * @param array|null $elementMap 107 | * 108 | * @return array> 109 | */ 110 | public function parseGetElements(?array $elementMap = null): array 111 | { 112 | $result = $this->parseInnerTree($elementMap); 113 | if (!is_array($result)) { 114 | return []; 115 | } 116 | 117 | return $result; 118 | } 119 | 120 | /** 121 | * Parses all elements below the current element. 122 | * 123 | * This method will return a string if this was a text-node, or an array if 124 | * there were sub-elements. 125 | * 126 | * If there's both text and sub-elements, the text will be discarded. 127 | * 128 | * If the $elementMap argument is specified, the existing elementMap will 129 | * be overridden while parsing the tree, and restored after this process. 130 | * 131 | * @param array|null $elementMap 132 | * 133 | * @return array>|string|null 134 | */ 135 | public function parseInnerTree(?array $elementMap = null) 136 | { 137 | $text = null; 138 | $elements = []; 139 | 140 | if (self::ELEMENT === $this->nodeType && $this->isEmptyElement) { 141 | // Easy! 142 | $this->next(); 143 | 144 | return null; 145 | } 146 | 147 | if (!is_null($elementMap)) { 148 | $this->pushContext(); 149 | $this->elementMap = $elementMap; 150 | } 151 | 152 | try { 153 | if (!$this->read()) { 154 | $errors = libxml_get_errors(); 155 | libxml_clear_errors(); 156 | if ($errors) { 157 | throw new LibXMLException($errors); 158 | } 159 | throw new ParseException('This should never happen (famous last words)'); 160 | } 161 | 162 | $keepOnParsing = true; 163 | 164 | while ($keepOnParsing) { 165 | if (!$this->isValid()) { 166 | $errors = libxml_get_errors(); 167 | 168 | if ($errors) { 169 | libxml_clear_errors(); 170 | throw new LibXMLException($errors); 171 | } 172 | } 173 | 174 | switch ($this->nodeType) { 175 | case self::ELEMENT: 176 | $elements[] = $this->parseCurrentElement(); 177 | break; 178 | case self::TEXT: 179 | case self::CDATA: 180 | $text .= $this->value; 181 | $this->read(); 182 | break; 183 | case self::END_ELEMENT: 184 | // Ensuring we are moving the cursor after the end element. 185 | $this->read(); 186 | $keepOnParsing = false; 187 | break; 188 | case self::NONE: 189 | throw new ParseException('We hit the end of the document prematurely. This likely means that some parser "eats" too many elements. Do not attempt to continue parsing.'); 190 | default: 191 | // Advance to the next element 192 | $this->read(); 193 | break; 194 | } 195 | } 196 | } finally { 197 | if (!is_null($elementMap)) { 198 | $this->popContext(); 199 | } 200 | } 201 | 202 | return $elements ?: $text; 203 | } 204 | 205 | /** 206 | * Reads all text below the current element, and returns this as a string. 207 | */ 208 | public function readText(): string 209 | { 210 | $result = ''; 211 | $previousDepth = $this->depth; 212 | 213 | while ($this->read() && $this->depth != $previousDepth) { 214 | if (in_array($this->nodeType, [\XMLReader::TEXT, \XMLReader::CDATA, \XMLReader::WHITESPACE])) { 215 | $result .= $this->value; 216 | } 217 | } 218 | 219 | return $result; 220 | } 221 | 222 | /** 223 | * Parses the current XML element. 224 | * 225 | * This method returns arn array with 3 properties: 226 | * * name - A clark-notation XML element name. 227 | * * value - The parsed value. 228 | * * attributes - A key-value list of attributes. 229 | * 230 | * @return array 231 | */ 232 | public function parseCurrentElement(): array 233 | { 234 | $name = $this->getClark(); 235 | 236 | $attributes = []; 237 | 238 | if ($this->hasAttributes) { 239 | $attributes = $this->parseAttributes(); 240 | } 241 | 242 | $value = call_user_func( 243 | $this->getDeserializerForElementName((string) $name), 244 | $this 245 | ); 246 | 247 | return [ 248 | 'name' => $name, 249 | 'value' => $value, 250 | 'attributes' => $attributes, 251 | ]; 252 | } 253 | 254 | /** 255 | * Grabs all the attributes from the current element, and returns them as a 256 | * key-value array. 257 | * 258 | * If the attributes are part of the same namespace, they will simply be 259 | * short keys. If they are defined on a different namespace, the attribute 260 | * name will be returned in clark-notation. 261 | * 262 | * @return array 263 | */ 264 | public function parseAttributes(): array 265 | { 266 | $attributes = []; 267 | 268 | while ($this->moveToNextAttribute()) { 269 | if ($this->namespaceURI) { 270 | // Ignoring 'xmlns', it doesn't make any sense. 271 | if ('http://www.w3.org/2000/xmlns/' === $this->namespaceURI) { 272 | continue; 273 | } 274 | 275 | $name = $this->getClark(); 276 | $attributes[$name] = $this->value; 277 | } else { 278 | $attributes[$this->localName] = $this->value; 279 | } 280 | } 281 | $this->moveToElement(); 282 | 283 | return $attributes; 284 | } 285 | 286 | /** 287 | * Returns the function that should be used to parse the element identified 288 | * by its clark-notation name. 289 | */ 290 | public function getDeserializerForElementName(string $name): callable 291 | { 292 | if (!array_key_exists($name, $this->elementMap)) { 293 | if ('{}' == substr($name, 0, 2) && array_key_exists(substr($name, 2), $this->elementMap)) { 294 | $name = substr($name, 2); 295 | } else { 296 | return ['Sabre\\Xml\\Element\\Base', 'xmlDeserialize']; 297 | } 298 | } 299 | 300 | $deserializer = $this->elementMap[$name]; 301 | if (is_callable($deserializer)) { 302 | return $deserializer; 303 | } 304 | 305 | if (is_subclass_of($deserializer, 'Sabre\\Xml\\XmlDeserializable')) { 306 | return [$deserializer, 'xmlDeserialize']; 307 | } 308 | 309 | $type = gettype($deserializer); 310 | if (is_string($deserializer)) { 311 | $type .= ' ('.$deserializer.')'; 312 | } elseif (is_object($deserializer)) { 313 | $type .= ' ('.get_class($deserializer).')'; 314 | } 315 | throw new \LogicException('Could not use this type as a deserializer: '.$type.' for element: '.$name); 316 | } 317 | } 318 | -------------------------------------------------------------------------------- /lib/Serializer/functions.php: -------------------------------------------------------------------------------- 1 | 33 | * 34 | * 35 | * content 36 | * 37 | * 38 | * @param string[] $values 39 | */ 40 | function enum(Writer $writer, array $values): void 41 | { 42 | foreach ($values as $value) { 43 | $writer->writeElement($value); 44 | } 45 | } 46 | 47 | /** 48 | * The valueObject serializer turns a simple PHP object into a classname. 49 | * 50 | * Every public property will be encoded as an XML element with the same 51 | * name, in the XML namespace as specified. 52 | * 53 | * Values that are set to null or an empty array are not serialized. To 54 | * serialize empty properties, you must specify them as an empty string. 55 | */ 56 | function valueObject(Writer $writer, object $valueObject, string $namespace): void 57 | { 58 | foreach (get_object_vars($valueObject) as $key => $val) { 59 | if (is_array($val)) { 60 | // If $val is an array, it has a special meaning. We need to 61 | // generate one child element for each item in $val 62 | foreach ($val as $child) { 63 | $writer->writeElement('{'.$namespace.'}'.$key, $child); 64 | } 65 | } elseif (null !== $val) { 66 | $writer->writeElement('{'.$namespace.'}'.$key, $val); 67 | } 68 | } 69 | } 70 | 71 | /** 72 | * This serializer helps you serialize xml structures that look like 73 | * this:. 74 | * 75 | * 76 | * ... 77 | * ... 78 | * ... 79 | * 80 | * 81 | * In that previous example, this serializer just serializes the item element, 82 | * and this could be called like this: 83 | * 84 | * repeatingElements($writer, $items, '{}item'); 85 | * 86 | * @param array $items 87 | */ 88 | function repeatingElements(Writer $writer, array $items, string $childElementName): void 89 | { 90 | foreach ($items as $item) { 91 | $writer->writeElement($childElementName, $item); 92 | } 93 | } 94 | 95 | /** 96 | * This function is the 'default' serializer that is able to serialize most 97 | * things, and delegates to other serializers if needed. 98 | * 99 | * The standardSerializer supports a wide-array of values. 100 | * 101 | * $value may be a string or integer, it will just write out the string as text. 102 | * $value may be an instance of XmlSerializable or Element, in which case it 103 | * calls it's xmlSerialize() method. 104 | * $value may be a PHP callback/function/closure, in case we call the callback 105 | * and give it the Writer as an argument. 106 | * $value may be an object, and if it's in the classMap we automatically call 107 | * the correct serializer for it. 108 | * $value may be null, in which case we do nothing. 109 | * 110 | * If $value is an array, the array must look like this: 111 | * 112 | * [ 113 | * [ 114 | * 'name' => '{namespaceUri}element-name', 115 | * 'value' => '...', 116 | * 'attributes' => [ 'attName' => 'attValue' ] 117 | * ] 118 | * [, 119 | * 'name' => '{namespaceUri}element-name2', 120 | * 'value' => '...', 121 | * ] 122 | * ] 123 | * 124 | * This would result in xml like: 125 | * 126 | * 127 | * ... 128 | * 129 | * 130 | * ... 131 | * 132 | * 133 | * The value property may be any value standardSerializer supports, so you can 134 | * nest data-structures this way. Both value and attributes are optional. 135 | * 136 | * Alternatively, you can also specify the array using this syntax: 137 | * 138 | * [ 139 | * [ 140 | * '{namespaceUri}element-name' => '...', 141 | * '{namespaceUri}element-name2' => '...', 142 | * ] 143 | * ] 144 | * 145 | * This is excellent for simple key->value structures, and here you can also 146 | * specify anything for the value. 147 | * 148 | * You can even mix the two array syntaxes. 149 | * 150 | * @param string|int|float|bool|array|object $value 151 | */ 152 | function standardSerializer(Writer $writer, $value): void 153 | { 154 | if (is_scalar($value)) { 155 | // String, integer, float, boolean 156 | $writer->text((string) $value); 157 | } elseif ($value instanceof XmlSerializable) { 158 | // XmlSerializable classes or Element classes. 159 | $value->xmlSerialize($writer); 160 | } elseif (is_object($value) && isset($writer->classMap[get_class($value)])) { 161 | // It's an object which class appears in the classmap. 162 | $writer->classMap[get_class($value)]($writer, $value); 163 | } elseif (is_callable($value)) { 164 | // A callback 165 | $value($writer); 166 | } elseif (is_array($value) && array_key_exists('name', $value)) { 167 | // if the array had a 'name' element, we assume that this array 168 | // describes a 'name' and optionally 'attributes' and 'value'. 169 | 170 | $name = $value['name']; 171 | $attributes = isset($value['attributes']) ? $value['attributes'] : []; 172 | $value = isset($value['value']) ? $value['value'] : null; 173 | 174 | $writer->startElement($name); 175 | $writer->writeAttributes($attributes); 176 | $writer->write($value); 177 | $writer->endElement(); 178 | } elseif (is_array($value)) { 179 | foreach ($value as $name => $item) { 180 | if (is_int($name)) { 181 | // This item has a numeric index. We just loop through the 182 | // array and throw it back in the writer. 183 | standardSerializer($writer, $item); 184 | } elseif (is_string($name) && is_array($item) && isset($item['attributes'])) { 185 | // The key is used for a name, but $item has 'attributes' and 186 | // possibly 'value' 187 | $writer->startElement($name); 188 | $writer->writeAttributes($item['attributes']); 189 | if (isset($item['value'])) { 190 | $writer->write($item['value']); 191 | } 192 | $writer->endElement(); 193 | } elseif (is_string($name)) { 194 | // This was a plain key-value array. 195 | $writer->startElement($name); 196 | $writer->write($item); 197 | $writer->endElement(); 198 | } else { 199 | throw new \InvalidArgumentException('The writer does not know how to serialize arrays with keys of type: '.gettype($name)); 200 | } 201 | } 202 | } elseif (is_object($value)) { 203 | throw new \InvalidArgumentException('The writer cannot serialize objects of class: '.get_class($value)); 204 | } elseif (!is_null($value)) { 205 | throw new \InvalidArgumentException('The writer cannot serialize values of type: '.gettype($value)); 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /lib/Service.php: -------------------------------------------------------------------------------- 1 | 30 | */ 31 | public array $elementMap = []; 32 | 33 | /** 34 | * This is a list of namespaces that you want to give default prefixes. 35 | * 36 | * You must make sure you create this entire list before starting to write. 37 | * They should be registered on the root element. 38 | * 39 | * @phpstan-var array 40 | */ 41 | public array $namespaceMap = []; 42 | 43 | /** 44 | * This is a list of custom serializers for specific classes. 45 | * 46 | * The writer may use this if you attempt to serialize an object with a 47 | * class that does not implement XmlSerializable. 48 | * 49 | * Instead, it will look at this classmap to see if there is a custom 50 | * serializer here. This is useful if you don't want your value objects 51 | * to be responsible for serializing themselves. 52 | * 53 | * The keys in this classmap need to be fully qualified PHP class names, 54 | * the values must be callbacks. The callbacks take two arguments. The 55 | * writer class, and the value that must be written. 56 | * 57 | * function (Writer $writer, object $value) 58 | * 59 | * @phpstan-var array 60 | */ 61 | public array $classMap = []; 62 | 63 | /** 64 | * A bitmask of the LIBXML_* constants. 65 | */ 66 | public int $options = 0; 67 | 68 | /** 69 | * Returns a fresh XML Reader. 70 | */ 71 | public function getReader(): Reader 72 | { 73 | $r = new Reader(); 74 | $r->elementMap = $this->elementMap; 75 | 76 | return $r; 77 | } 78 | 79 | /** 80 | * Returns a fresh xml writer. 81 | */ 82 | public function getWriter(): Writer 83 | { 84 | $w = new Writer(); 85 | $w->namespaceMap = $this->namespaceMap; 86 | $w->classMap = $this->classMap; 87 | 88 | return $w; 89 | } 90 | 91 | /** 92 | * Parses a document in full. 93 | * 94 | * Input may be specified as a string or readable stream resource. 95 | * The returned value is the value of the root document. 96 | * 97 | * Specifying the $contextUri allows the parser to figure out what the URI 98 | * of the document was. This allows relative URIs within the document to be 99 | * expanded easily. 100 | * 101 | * The $rootElementName is specified by reference and will be populated 102 | * with the root element name of the document. 103 | * 104 | * @param string|resource $input 105 | * 106 | * @return array|object|string 107 | * 108 | * @throws ParseException 109 | */ 110 | public function parse($input, ?string $contextUri = null, ?string &$rootElementName = null) 111 | { 112 | if (!is_string($input)) { 113 | // Unfortunately the XMLReader doesn't support streams. When it 114 | // does, we can optimize this. 115 | if (is_resource($input)) { 116 | $input = (string) stream_get_contents($input); 117 | } else { 118 | // Input is not a string and not a resource. 119 | // Therefore, it has to be a closed resource. 120 | // Effectively empty input has been passed in. 121 | $input = ''; 122 | } 123 | } 124 | 125 | // If input is empty, then it's safe to throw an exception 126 | if (empty($input)) { 127 | throw new ParseException('The input element to parse is empty. Do not attempt to parse'); 128 | } 129 | 130 | $r = $this->getReader(); 131 | $r->contextUri = $contextUri; 132 | $r->XML($input, null, $this->options); 133 | 134 | $result = $r->parse(); 135 | $rootElementName = $result['name']; 136 | 137 | return $result['value']; 138 | } 139 | 140 | /** 141 | * Parses a document in full, and specify what the expected root element 142 | * name is. 143 | * 144 | * This function works similar to parse, but the difference is that the 145 | * user can specify what the expected name of the root element should be, 146 | * in clark notation. 147 | * 148 | * This is useful in cases where you expected a specific document to be 149 | * passed, and reduces the amount of if statements. 150 | * 151 | * It's also possible to pass an array of expected rootElements if your 152 | * code may expect more than one document type. 153 | * 154 | * @param string|string[] $rootElementName 155 | * @param string|resource $input 156 | * 157 | * @return array|object|string 158 | * 159 | * @throws ParseException 160 | */ 161 | public function expect($rootElementName, $input, ?string $contextUri = null) 162 | { 163 | if (!is_string($input)) { 164 | // Unfortunately the XMLReader doesn't support streams. When it 165 | // does, we can optimize this. 166 | if (is_resource($input)) { 167 | $input = (string) stream_get_contents($input); 168 | } else { 169 | // Input is not a string and not a resource. 170 | // Therefore, it has to be a closed resource. 171 | // Effectively empty input has been passed in. 172 | $input = ''; 173 | } 174 | } 175 | 176 | // If input is empty, then it's safe to throw an exception 177 | if (empty($input)) { 178 | throw new ParseException('The input element to parse is empty. Do not attempt to parse'); 179 | } 180 | 181 | $r = $this->getReader(); 182 | $r->contextUri = $contextUri; 183 | $r->XML($input, null, $this->options); 184 | 185 | $rootElementName = (array) $rootElementName; 186 | 187 | foreach ($rootElementName as &$rEl) { 188 | if ('{' !== $rEl[0]) { 189 | $rEl = '{}'.$rEl; 190 | } 191 | } 192 | 193 | $result = $r->parse(); 194 | if (!in_array($result['name'], $rootElementName, true)) { 195 | throw new ParseException('Expected '.implode(' or ', $rootElementName).' but received '.$result['name'].' as the root element'); 196 | } 197 | 198 | return $result['value']; 199 | } 200 | 201 | /** 202 | * Generates an XML document in one go. 203 | * 204 | * The $rootElement must be specified in clark notation. 205 | * The value must be a string, an array or an object implementing 206 | * XmlSerializable. Basically, anything that's supported by the Writer 207 | * object. 208 | * 209 | * $contextUri can be used to specify a sort of 'root' of the PHP application, 210 | * in case the xml document is used as a http response. 211 | * 212 | * This allows an implementor to easily create URI's relative to the root 213 | * of the domain. 214 | * 215 | * @param string|array|object|XmlSerializable $value 216 | */ 217 | public function write(string $rootElementName, $value, ?string $contextUri = null): string 218 | { 219 | $w = $this->getWriter(); 220 | $w->openMemory(); 221 | $w->contextUri = $contextUri; 222 | $w->setIndent(true); 223 | $w->startDocument(); 224 | $w->writeElement($rootElementName, $value); 225 | 226 | return $w->outputMemory(); 227 | } 228 | 229 | /** 230 | * Map an XML element to a PHP class. 231 | * 232 | * Calling this function will automatically set up the Reader and Writer 233 | * classes to turn a specific XML element to a PHP class. 234 | * 235 | * For example, given a class such as : 236 | * 237 | * class Author { 238 | * public $firstName; 239 | * public $lastName; 240 | * } 241 | * 242 | * and an XML element such as: 243 | * 244 | * 245 | * ... 246 | * ... 247 | * 248 | * 249 | * These can easily be mapped by calling: 250 | * 251 | * $service->mapValueObject('{http://example.org}author', 'Author'); 252 | * 253 | * @param class-string $className 254 | */ 255 | public function mapValueObject(string $elementName, string $className): void 256 | { 257 | list($namespace) = self::parseClarkNotation($elementName); 258 | 259 | $this->elementMap[$elementName] = function (Reader $reader) use ($className, $namespace) { 260 | return \Sabre\Xml\Deserializer\valueObject($reader, $className, $namespace); 261 | }; 262 | $this->classMap[$className] = function (Writer $writer, $valueObject) use ($namespace) { 263 | \Sabre\Xml\Serializer\valueObject($writer, $valueObject, $namespace); 264 | }; 265 | $this->valueObjectMap[$className] = $elementName; 266 | } 267 | 268 | /** 269 | * Writes a value object. 270 | * 271 | * This function largely behaves similar to write(), except that it's 272 | * intended specifically to serialize a Value Object into an XML document. 273 | * 274 | * The ValueObject must have been previously registered using 275 | * mapValueObject(). 276 | * 277 | * @throws \InvalidArgumentException 278 | */ 279 | public function writeValueObject(object $object, ?string $contextUri = null): string 280 | { 281 | if (!isset($this->valueObjectMap[get_class($object)])) { 282 | throw new \InvalidArgumentException('"'.get_class($object).'" is not a registered value object class. Register your class with mapValueObject.'); 283 | } 284 | 285 | return $this->write( 286 | $this->valueObjectMap[get_class($object)], 287 | $object, 288 | $contextUri 289 | ); 290 | } 291 | 292 | /** 293 | * Parses a clark-notation string, and returns the namespace and element 294 | * name components. 295 | * 296 | * If the string was invalid, it will throw an InvalidArgumentException. 297 | * 298 | * @return array{string, string} 299 | * 300 | * @throws \InvalidArgumentException 301 | */ 302 | public static function parseClarkNotation(string $str): array 303 | { 304 | static $cache = []; 305 | 306 | if (!isset($cache[$str])) { 307 | if (!preg_match('/^{([^}]*)}(.*)$/', $str, $matches)) { 308 | throw new \InvalidArgumentException('\''.$str.'\' is not a valid clark-notation formatted string'); 309 | } 310 | 311 | $cache[$str] = [ 312 | $matches[1], 313 | $matches[2], 314 | ]; 315 | } 316 | 317 | return $cache[$str]; 318 | } 319 | 320 | /** 321 | * A list of classes and which XML elements they map to. 322 | * 323 | * @var array 324 | */ 325 | protected array $valueObjectMap = []; 326 | } 327 | -------------------------------------------------------------------------------- /lib/Version.php: -------------------------------------------------------------------------------- 1 | 45 | */ 46 | protected array $adhocNamespaces = []; 47 | 48 | /** 49 | * When the first element is written, this flag is set to true. 50 | * 51 | * This ensures that the namespaces in the namespaces map are only written 52 | * once. 53 | */ 54 | protected bool $namespacesWritten = false; 55 | 56 | /** 57 | * Writes a value to the output stream. 58 | * 59 | * The following values are supported: 60 | * 1. Scalar values will be written as-is, as text. 61 | * 2. Null values will be skipped (resulting in a short xml tag). 62 | * 3. If a value is an instance of an Element class, writing will be 63 | * delegated to the object. 64 | * 4. If a value is an array, two formats are supported. 65 | * 66 | * Array format 1: 67 | * [ 68 | * "{namespace}name1" => "..", 69 | * "{namespace}name2" => "..", 70 | * ] 71 | * 72 | * One element will be created for each key in this array. The values of 73 | * this array support any format this method supports (this method is 74 | * called recursively). 75 | * 76 | * Array format 2: 77 | * 78 | * [ 79 | * [ 80 | * "name" => "{namespace}name1" 81 | * "value" => "..", 82 | * "attributes" => [ 83 | * "attr" => "attribute value", 84 | * ] 85 | * ], 86 | * [ 87 | * "name" => "{namespace}name1" 88 | * "value" => "..", 89 | * "attributes" => [ 90 | * "attr" => "attribute value", 91 | * ] 92 | * ] 93 | * ] 94 | * 95 | * @param mixed $value PHP value to be written 96 | */ 97 | public function write($value): void 98 | { 99 | Serializer\standardSerializer($this, $value); 100 | } 101 | 102 | /** 103 | * Opens a new element. 104 | * 105 | * You can either just use a local element name, or you can use clark- 106 | * notation to start a new element. 107 | * 108 | * Example: 109 | * 110 | * $writer->startElement('{http://www.w3.org/2005/Atom}entry'); 111 | * 112 | * Would result in something like: 113 | * 114 | * 115 | * 116 | * Note: this function doesn't have the string typehint, because PHP's 117 | * XMLWriter::startElement doesn't either. 118 | * From PHP 8.0 the typehint exists, so it can be added here after PHP 7.4 is dropped. 119 | * 120 | * @param string $name 121 | */ 122 | public function startElement($name): bool 123 | { 124 | if ('{' === $name[0]) { 125 | list($namespace, $localName) = 126 | Service::parseClarkNotation($name); 127 | 128 | if (array_key_exists($namespace, $this->namespaceMap)) { 129 | $result = $this->startElementNS( 130 | '' === $this->namespaceMap[$namespace] ? null : $this->namespaceMap[$namespace], 131 | $localName, 132 | null 133 | ); 134 | } else { 135 | // An empty namespace means it's the global namespace. This is 136 | // allowed, but it mustn't get a prefix. 137 | if ('' === $namespace) { 138 | $result = $this->startElement($localName); 139 | $this->writeAttribute('xmlns', ''); 140 | } else { 141 | if (!isset($this->adhocNamespaces[$namespace])) { 142 | $this->adhocNamespaces[$namespace] = 'x'.(count($this->adhocNamespaces) + 1); 143 | } 144 | $result = $this->startElementNS($this->adhocNamespaces[$namespace], $localName, $namespace); 145 | } 146 | } 147 | } else { 148 | $result = parent::startElement($name); 149 | } 150 | 151 | if (!$this->namespacesWritten) { 152 | foreach ($this->namespaceMap as $namespace => $prefix) { 153 | $this->writeAttribute($prefix ? 'xmlns:'.$prefix : 'xmlns', $namespace); 154 | } 155 | $this->namespacesWritten = true; 156 | } 157 | 158 | return $result; 159 | } 160 | 161 | /** 162 | * Write a full element tag and it's contents. 163 | * 164 | * This method automatically closes the element as well. 165 | * 166 | * The element name may be specified in clark-notation. 167 | * 168 | * Examples: 169 | * 170 | * $writer->writeElement('{http://www.w3.org/2005/Atom}author',null); 171 | * becomes: 172 | * 173 | * 174 | * $writer->writeElement('{http://www.w3.org/2005/Atom}author', [ 175 | * '{http://www.w3.org/2005/Atom}name' => 'Evert Pot', 176 | * ]); 177 | * becomes: 178 | * Evert Pot 179 | * 180 | * Note: this function doesn't have the string typehint, because PHP's 181 | * XMLWriter::startElement doesn't either. 182 | * From PHP 8.0 the typehint exists, so it can be added here after PHP 7.4 is dropped. 183 | * 184 | * @param string $name 185 | * @param array|string|object|null $content 186 | */ 187 | public function writeElement($name, $content = null): bool 188 | { 189 | $this->startElement($name); 190 | if (!is_null($content)) { 191 | $this->write($content); 192 | } 193 | $this->endElement(); 194 | 195 | return true; 196 | } 197 | 198 | /** 199 | * Writes a list of attributes. 200 | * 201 | * Attributes are specified as a key->value array. 202 | * 203 | * The key is an attribute name. If the key is a 'localName', the current 204 | * xml namespace is assumed. If it's a 'clark notation key', this namespace 205 | * will be used instead. 206 | * 207 | * @param array $attributes 208 | */ 209 | public function writeAttributes(array $attributes): void 210 | { 211 | foreach ($attributes as $name => $value) { 212 | $this->writeAttribute($name, $value); 213 | } 214 | } 215 | 216 | /** 217 | * Writes a new attribute. 218 | * 219 | * The name may be specified in clark-notation. 220 | * 221 | * Returns true when successful. 222 | * 223 | * Note: this function doesn't have typehints, because for some reason 224 | * PHP's XMLWriter::writeAttribute doesn't either. 225 | * From PHP 8.0 the typehint exists, so it can be added here after PHP 7.4 is dropped. 226 | * 227 | * @param string $name 228 | * @param string $value 229 | */ 230 | public function writeAttribute($name, $value): bool 231 | { 232 | if ('{' !== $name[0]) { 233 | return parent::writeAttribute($name, $value); 234 | } 235 | 236 | list( 237 | $namespace, 238 | $localName, 239 | ) = Service::parseClarkNotation($name); 240 | 241 | if (array_key_exists($namespace, $this->namespaceMap)) { 242 | // It's an attribute with a namespace we know 243 | return $this->writeAttribute( 244 | $this->namespaceMap[$namespace].':'.$localName, 245 | $value 246 | ); 247 | } 248 | 249 | // We don't know the namespace, we must add it in-line 250 | if (!isset($this->adhocNamespaces[$namespace])) { 251 | $this->adhocNamespaces[$namespace] = 'x'.(count($this->adhocNamespaces) + 1); 252 | } 253 | 254 | return $this->writeAttributeNS( 255 | $this->adhocNamespaces[$namespace], 256 | $localName, 257 | $namespace, 258 | $value 259 | ); 260 | } 261 | } 262 | -------------------------------------------------------------------------------- /lib/XmlDeserializable.php: -------------------------------------------------------------------------------- 1 | next(); 31 | * 32 | * $reader->parseInnerTree() will parse the entire sub-tree, and advance to 33 | * the next element. 34 | * 35 | * @return mixed see comments above 36 | */ 37 | public static function xmlDeserialize(Reader $reader); 38 | } 39 | -------------------------------------------------------------------------------- /lib/XmlSerializable.php: -------------------------------------------------------------------------------- 1 |