1<?php 2 3namespace Sabre\Xml; 4 5/** 6 * XML parsing and writing service. 7 * 8 * You are encouraged to make a instance of this for your application and 9 * potentially extend it, as a central API point for dealing with xml and 10 * configuring the reader and writer. 11 * 12 * @copyright Copyright (C) 2009-2015 fruux GmbH (https://fruux.com/). 13 * @author Evert Pot (http://evertpot.com/) 14 * @license http://sabre.io/license/ Modified BSD License 15 */ 16class Service { 17 18 /** 19 * This is the element map. It contains a list of XML elements (in clark 20 * notation) as keys and PHP class names as values. 21 * 22 * The PHP class names must implement Sabre\Xml\Element. 23 * 24 * Values may also be a callable. In that case the function will be called 25 * directly. 26 * 27 * @var array 28 */ 29 public $elementMap = []; 30 31 /** 32 * This is a list of namespaces that you want to give default prefixes. 33 * 34 * You must make sure you create this entire list before starting to write. 35 * They should be registered on the root element. 36 * 37 * @var array 38 */ 39 public $namespaceMap = []; 40 41 /** 42 * This is a list of custom serializers for specific classes. 43 * 44 * The writer may use this if you attempt to serialize an object with a 45 * class that does not implement XmlSerializable. 46 * 47 * Instead it will look at this classmap to see if there is a custom 48 * serializer here. This is useful if you don't want your value objects 49 * to be responsible for serializing themselves. 50 * 51 * The keys in this classmap need to be fully qualified PHP class names, 52 * the values must be callbacks. The callbacks take two arguments. The 53 * writer class, and the value that must be written. 54 * 55 * function (Writer $writer, object $value) 56 * 57 * @var array 58 */ 59 public $classMap = []; 60 61 /** 62 * Returns a fresh XML Reader 63 * 64 * @return Reader 65 */ 66 function getReader() { 67 68 $r = new Reader(); 69 $r->elementMap = $this->elementMap; 70 return $r; 71 72 } 73 74 /** 75 * Returns a fresh xml writer 76 * 77 * @return Writer 78 */ 79 function getWriter() { 80 81 $w = new Writer(); 82 $w->namespaceMap = $this->namespaceMap; 83 $w->classMap = $this->classMap; 84 return $w; 85 86 } 87 88 /** 89 * Parses a document in full. 90 * 91 * Input may be specified as a string or readable stream resource. 92 * The returned value is the value of the root document. 93 * 94 * Specifying the $contextUri allows the parser to figure out what the URI 95 * of the document was. This allows relative URIs within the document to be 96 * expanded easily. 97 * 98 * The $rootElementName is specified by reference and will be populated 99 * with the root element name of the document. 100 * 101 * @param string|resource $input 102 * @param string|null $contextUri 103 * @param string|null $rootElementName 104 * @throws ParseException 105 * @return array|object|string 106 */ 107 function parse($input, $contextUri = null, &$rootElementName = null) { 108 109 if (is_resource($input)) { 110 // Unfortunately the XMLReader doesn't support streams. When it 111 // does, we can optimize this. 112 $input = stream_get_contents($input); 113 } 114 $r = $this->getReader(); 115 $r->contextUri = $contextUri; 116 $r->xml($input); 117 118 $result = $r->parse(); 119 $rootElementName = $result['name']; 120 return $result['value']; 121 122 } 123 124 /** 125 * Parses a document in full, and specify what the expected root element 126 * name is. 127 * 128 * This function works similar to parse, but the difference is that the 129 * user can specify what the expected name of the root element should be, 130 * in clark notation. 131 * 132 * This is useful in cases where you expected a specific document to be 133 * passed, and reduces the amount of if statements. 134 * 135 * It's also possible to pass an array of expected rootElements if your 136 * code may expect more than one document type. 137 * 138 * @param string|string[] $rootElementName 139 * @param string|resource $input 140 * @param string|null $contextUri 141 * @return void 142 */ 143 function expect($rootElementName, $input, $contextUri = null) { 144 145 if (is_resource($input)) { 146 // Unfortunately the XMLReader doesn't support streams. When it 147 // does, we can optimize this. 148 $input = stream_get_contents($input); 149 } 150 $r = $this->getReader(); 151 $r->contextUri = $contextUri; 152 $r->xml($input); 153 154 $rootElementName = (array)$rootElementName; 155 156 foreach ($rootElementName as &$rEl) { 157 if ($rEl[0] !== '{') $rEl = '{}' . $rEl; 158 } 159 160 $result = $r->parse(); 161 if (!in_array($result['name'], $rootElementName, true)) { 162 throw new ParseException('Expected ' . implode(' or ', (array)$rootElementName) . ' but received ' . $result['name'] . ' as the root element'); 163 } 164 return $result['value']; 165 166 } 167 168 /** 169 * Generates an XML document in one go. 170 * 171 * The $rootElement must be specified in clark notation. 172 * The value must be a string, an array or an object implementing 173 * XmlSerializable. Basically, anything that's supported by the Writer 174 * object. 175 * 176 * $contextUri can be used to specify a sort of 'root' of the PHP application, 177 * in case the xml document is used as a http response. 178 * 179 * This allows an implementor to easily create URI's relative to the root 180 * of the domain. 181 * 182 * @param string $rootElementName 183 * @param string|array|XmlSerializable $value 184 * @param string|null $contextUri 185 */ 186 function write($rootElementName, $value, $contextUri = null) { 187 188 $w = $this->getWriter(); 189 $w->openMemory(); 190 $w->contextUri = $contextUri; 191 $w->setIndent(true); 192 $w->startDocument(); 193 $w->writeElement($rootElementName, $value); 194 return $w->outputMemory(); 195 196 } 197 198 /** 199 * Map an xml element to a PHP class. 200 * 201 * Calling this function will automatically setup the Reader and Writer 202 * classes to turn a specific XML element to a PHP class. 203 * 204 * For example, given a class such as : 205 * 206 * class Author { 207 * public $firstName; 208 * public $lastName; 209 * } 210 * 211 * and an XML element such as: 212 * 213 * <author xmlns="http://example.org/ns"> 214 * <firstName>...</firstName> 215 * <lastName>...</lastName> 216 * </author> 217 * 218 * These can easily be mapped by calling: 219 * 220 * $service->mapValueObject('{http://example.org}author', 'Author'); 221 * 222 * @param string $elementName 223 * @param object $className 224 * @return void 225 */ 226 function mapValueObject($elementName, $className) { 227 list($namespace) = self::parseClarkNotation($elementName); 228 229 $this->elementMap[$elementName] = function(Reader $reader) use ($className, $namespace) { 230 return \Sabre\Xml\Deserializer\valueObject($reader, $className, $namespace); 231 }; 232 $this->classMap[$className] = function(Writer $writer, $valueObject) use ($namespace) { 233 return \Sabre\Xml\Serializer\valueObject($writer, $valueObject, $namespace); 234 }; 235 $this->valueObjectMap[$className] = $elementName; 236 } 237 238 /** 239 * Writes a value object. 240 * 241 * This function largely behaves similar to write(), except that it's 242 * intended specifically to serialize a Value Object into an XML document. 243 * 244 * The ValueObject must have been previously registered using 245 * mapValueObject(). 246 * 247 * @param object $object 248 * @param string $contextUri 249 * @return void 250 */ 251 function writeValueObject($object, $contextUri = null) { 252 253 if (!isset($this->valueObjectMap[get_class($object)])) { 254 throw new \InvalidArgumentException('"' . get_class($object) . '" is not a registered value object class. Register your class with mapValueObject.'); 255 } 256 return $this->write( 257 $this->valueObjectMap[get_class($object)], 258 $object, 259 $contextUri 260 ); 261 262 } 263 264 /** 265 * Parses a clark-notation string, and returns the namespace and element 266 * name components. 267 * 268 * If the string was invalid, it will throw an InvalidArgumentException. 269 * 270 * @param string $str 271 * @throws InvalidArgumentException 272 * @return array 273 */ 274 static function parseClarkNotation($str) { 275 static $cache = []; 276 277 if (!isset($cache[$str])) { 278 279 if (!preg_match('/^{([^}]*)}(.*)$/', $str, $matches)) { 280 throw new \InvalidArgumentException('\'' . $str . '\' is not a valid clark-notation formatted string'); 281 } 282 283 $cache[$str] = [ 284 $matches[1], 285 $matches[2] 286 ]; 287 } 288 289 return $cache[$str]; 290 } 291 292 /** 293 * A list of classes and which XML elements they map to. 294 */ 295 protected $valueObjectMap = []; 296 297} 298