1<?php 2 3namespace Sabre\Xml; 4 5use XMLWriter; 6use InvalidArgumentException; 7 8/** 9 * The XML Writer class. 10 * 11 * This class works exactly as PHP's built-in XMLWriter, with a few additions. 12 * 13 * Namespaces can be registered beforehand, globally. When the first element is 14 * written, namespaces will automatically be declared. 15 * 16 * The writeAttribute, startElement and writeElement can now take a 17 * clark-notation element name (example: {http://www.w3.org/2005/Atom}link). 18 * 19 * If, when writing the namespace is a known one a prefix will automatically be 20 * selected, otherwise a random prefix will be generated. 21 * 22 * Instead of standard string values, the writer can take Element classes (as 23 * defined by this library) to delegate the serialization. 24 * 25 * The write() method can take array structures to quickly write out simple xml 26 * trees. 27 * 28 * @copyright Copyright (C) 2009-2015 fruux GmbH (https://fruux.com/). 29 * @author Evert Pot (http://evertpot.com/) 30 * @license http://sabre.io/license/ Modified BSD License 31 */ 32class Writer extends XMLWriter { 33 34 use ContextStackTrait; 35 36 /** 37 * Any namespace that the writer is asked to write, will be added here. 38 * 39 * Any of these elements will get a new namespace definition *every single 40 * time* they are used, but this array allows the writer to make sure that 41 * the prefixes are consistent anyway. 42 * 43 * @var array 44 */ 45 protected $adhocNamespaces = []; 46 47 /** 48 * When the first element is written, this flag is set to true. 49 * 50 * This ensures that the namespaces in the namespaces map are only written 51 * once. 52 * 53 * @var bool 54 */ 55 protected $namespacesWritten = false; 56 57 /** 58 * Writes a value to the output stream. 59 * 60 * The following values are supported: 61 * 1. Scalar values will be written as-is, as text. 62 * 2. Null values will be skipped (resulting in a short xml tag). 63 * 3. If a value is an instance of an Element class, writing will be 64 * delegated to the object. 65 * 4. If a value is an array, two formats are supported. 66 * 67 * Array format 1: 68 * [ 69 * "{namespace}name1" => "..", 70 * "{namespace}name2" => "..", 71 * ] 72 * 73 * One element will be created for each key in this array. The values of 74 * this array support any format this method supports (this method is 75 * called recursively). 76 * 77 * Array format 2: 78 * 79 * [ 80 * [ 81 * "name" => "{namespace}name1" 82 * "value" => "..", 83 * "attributes" => [ 84 * "attr" => "attribute value", 85 * ] 86 * ], 87 * [ 88 * "name" => "{namespace}name1" 89 * "value" => "..", 90 * "attributes" => [ 91 * "attr" => "attribute value", 92 * ] 93 * ] 94 * ] 95 * 96 * @param mixed $value 97 * @return void 98 */ 99 function write($value) { 100 101 if (is_scalar($value)) { 102 $this->text($value); 103 } elseif ($value instanceof XmlSerializable) { 104 $value->xmlSerialize($this); 105 } elseif (is_null($value)) { 106 // noop 107 } elseif (is_array($value)) { 108 109 reset($value); 110 foreach ($value as $name => $item) { 111 112 if (is_int($name)) { 113 114 // This item has a numeric index. We expect to be an array with a name and a value. 115 if (!is_array($item) || !array_key_exists('name', $item) || !array_key_exists('value', $item)) { 116 throw new InvalidArgumentException('When passing an array to ->write with numeric indices, every item must be an array containing the "name" and "value" key'); 117 } 118 119 $attributes = isset($item['attributes']) ? $item['attributes'] : []; 120 $name = $item['name']; 121 $item = $item['value']; 122 123 } elseif (is_array($item) && array_key_exists('value', $item)) { 124 125 // This item has a text index. We expect to be an array with a value and optional attributes. 126 $attributes = isset($item['attributes']) ? $item['attributes'] : []; 127 $item = $item['value']; 128 129 } else { 130 // If it's an array with text-indices, we expect every item's 131 // key to be an xml element name in clark notation. 132 // No attributes can be passed. 133 $attributes = []; 134 } 135 136 $this->startElement($name); 137 $this->writeAttributes($attributes); 138 $this->write($item); 139 $this->endElement(); 140 141 } 142 143 } elseif (is_object($value)) { 144 145 throw new InvalidArgumentException('The writer cannot serialize objects of type: ' . get_class($value)); 146 147 } 148 149 } 150 151 /** 152 * Starts an element. 153 * 154 * @param string $name 155 * @return bool 156 */ 157 function startElement($name) { 158 159 if ($name[0] === '{') { 160 161 list($namespace, $localName) = 162 Service::parseClarkNotation($name); 163 164 if (array_key_exists($namespace, $this->namespaceMap)) { 165 $result = $this->startElementNS($this->namespaceMap[$namespace], $localName, null); 166 } else { 167 168 // An empty namespace means it's the global namespace. This is 169 // allowed, but it mustn't get a prefix. 170 if ($namespace === "") { 171 $result = $this->startElement($localName); 172 $this->writeAttribute('xmlns', ''); 173 } else { 174 if (!isset($this->adhocNamespaces[$namespace])) { 175 $this->adhocNamespaces[$namespace] = 'x' . (count($this->adhocNamespaces) + 1); 176 } 177 $result = $this->startElementNS($this->adhocNamespaces[$namespace], $localName, $namespace); 178 } 179 } 180 181 } else { 182 $result = parent::startElement($name); 183 } 184 185 if (!$this->namespacesWritten) { 186 187 foreach ($this->namespaceMap as $namespace => $prefix) { 188 $this->writeAttribute(($prefix ? 'xmlns:' . $prefix : 'xmlns'), $namespace); 189 } 190 $this->namespacesWritten = true; 191 192 } 193 194 return $result; 195 196 } 197 198 /** 199 * Write a full element tag. 200 * 201 * This method automatically closes the element as well. 202 * 203 * @param string $name 204 * @param string $content 205 * @return bool 206 */ 207 function writeElement($name, $content = null) { 208 209 $this->startElement($name); 210 if (!is_null($content)) { 211 $this->write($content); 212 } 213 $this->endElement(); 214 215 } 216 217 /** 218 * Writes a list of attributes. 219 * 220 * Attributes are specified as a key->value array. 221 * 222 * The key is an attribute name. If the key is a 'localName', the current 223 * xml namespace is assumed. If it's a 'clark notation key', this namespace 224 * will be used instead. 225 * 226 * @param array $attributes 227 * @return void 228 */ 229 function writeAttributes(array $attributes) { 230 231 foreach ($attributes as $name => $value) { 232 $this->writeAttribute($name, $value); 233 } 234 235 } 236 237 /** 238 * Writes a new attribute. 239 * 240 * The name may be specified in clark-notation. 241 * 242 * Returns true when successful. 243 * 244 * @param string $name 245 * @param string $value 246 * @return bool 247 */ 248 function writeAttribute($name, $value) { 249 250 if ($name[0] === '{') { 251 252 list( 253 $namespace, 254 $localName 255 ) = Service::parseClarkNotation($name); 256 257 if (array_key_exists($namespace, $this->namespaceMap)) { 258 // It's an attribute with a namespace we know 259 $this->writeAttribute( 260 $this->namespaceMap[$namespace] . ':' . $localName, 261 $value 262 ); 263 } else { 264 265 // We don't know the namespace, we must add it in-line 266 if (!isset($this->adhocNamespaces[$namespace])) { 267 $this->adhocNamespaces[$namespace] = 'x' . (count($this->adhocNamespaces) + 1); 268 } 269 $this->writeAttributeNS( 270 $this->adhocNamespaces[$namespace], 271 $localName, 272 $namespace, 273 $value 274 ); 275 276 } 277 278 } else { 279 return parent::writeAttribute($name, $value); 280 } 281 282 } 283 284} 285