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 * @throws ParseException 142 * @return array|object|string 143 */ 144 function expect($rootElementName, $input, $contextUri = null) { 145 146 if (is_resource($input)) { 147 // Unfortunately the XMLReader doesn't support streams. When it 148 // does, we can optimize this. 149 $input = stream_get_contents($input); 150 } 151 $r = $this->getReader(); 152 $r->contextUri = $contextUri; 153 $r->xml($input); 154 155 $rootElementName = (array)$rootElementName; 156 157 foreach ($rootElementName as &$rEl) { 158 if ($rEl[0] !== '{') $rEl = '{}' . $rEl; 159 } 160 161 $result = $r->parse(); 162 if (!in_array($result['name'], $rootElementName, true)) { 163 throw new ParseException('Expected ' . implode(' or ', (array)$rootElementName) . ' but received ' . $result['name'] . ' as the root element'); 164 } 165 return $result['value']; 166 167 } 168 169 /** 170 * Generates an XML document in one go. 171 * 172 * The $rootElement must be specified in clark notation. 173 * The value must be a string, an array or an object implementing 174 * XmlSerializable. Basically, anything that's supported by the Writer 175 * object. 176 * 177 * $contextUri can be used to specify a sort of 'root' of the PHP application, 178 * in case the xml document is used as a http response. 179 * 180 * This allows an implementor to easily create URI's relative to the root 181 * of the domain. 182 * 183 * @param string $rootElementName 184 * @param string|array|XmlSerializable $value 185 * @param string|null $contextUri 186 */ 187 function write($rootElementName, $value, $contextUri = null) { 188 189 $w = $this->getWriter(); 190 $w->openMemory(); 191 $w->contextUri = $contextUri; 192 $w->setIndent(true); 193 $w->startDocument(); 194 $w->writeElement($rootElementName, $value); 195 return $w->outputMemory(); 196 197 } 198 199 /** 200 * Map an xml element to a PHP class. 201 * 202 * Calling this function will automatically setup the Reader and Writer 203 * classes to turn a specific XML element to a PHP class. 204 * 205 * For example, given a class such as : 206 * 207 * class Author { 208 * public $firstName; 209 * public $lastName; 210 * } 211 * 212 * and an XML element such as: 213 * 214 * <author xmlns="http://example.org/ns"> 215 * <firstName>...</firstName> 216 * <lastName>...</lastName> 217 * </author> 218 * 219 * These can easily be mapped by calling: 220 * 221 * $service->mapValueObject('{http://example.org}author', 'Author'); 222 * 223 * @param string $elementName 224 * @param object $className 225 * @return void 226 */ 227 function mapValueObject($elementName, $className) { 228 list($namespace) = self::parseClarkNotation($elementName); 229 230 $this->elementMap[$elementName] = function(Reader $reader) use ($className, $namespace) { 231 return \Sabre\Xml\Deserializer\valueObject($reader, $className, $namespace); 232 }; 233 $this->classMap[$className] = function(Writer $writer, $valueObject) use ($namespace) { 234 return \Sabre\Xml\Serializer\valueObject($writer, $valueObject, $namespace); 235 }; 236 $this->valueObjectMap[$className] = $elementName; 237 } 238 239 /** 240 * Writes a value object. 241 * 242 * This function largely behaves similar to write(), except that it's 243 * intended specifically to serialize a Value Object into an XML document. 244 * 245 * The ValueObject must have been previously registered using 246 * mapValueObject(). 247 * 248 * @param object $object 249 * @param string $contextUri 250 * @return void 251 */ 252 function writeValueObject($object, $contextUri = null) { 253 254 if (!isset($this->valueObjectMap[get_class($object)])) { 255 throw new \InvalidArgumentException('"' . get_class($object) . '" is not a registered value object class. Register your class with mapValueObject.'); 256 } 257 return $this->write( 258 $this->valueObjectMap[get_class($object)], 259 $object, 260 $contextUri 261 ); 262 263 } 264 265 /** 266 * Parses a clark-notation string, and returns the namespace and element 267 * name components. 268 * 269 * If the string was invalid, it will throw an InvalidArgumentException. 270 * 271 * @param string $str 272 * @throws InvalidArgumentException 273 * @return array 274 */ 275 static function parseClarkNotation($str) { 276 static $cache = []; 277 278 if (!isset($cache[$str])) { 279 280 if (!preg_match('/^{([^}]*)}(.*)$/', $str, $matches)) { 281 throw new \InvalidArgumentException('\'' . $str . '\' is not a valid clark-notation formatted string'); 282 } 283 284 $cache[$str] = [ 285 $matches[1], 286 $matches[2] 287 ]; 288 } 289 290 return $cache[$str]; 291 } 292 293 /** 294 * A list of classes and which XML elements they map to. 295 */ 296 protected $valueObjectMap = []; 297 298} 299