1<?php 2 3namespace Sabre\Xml; 4 5use XMLWriter; 6 7/** 8 * The XML Writer class. 9 * 10 * This class works exactly as PHP's built-in XMLWriter, with a few additions. 11 * 12 * Namespaces can be registered beforehand, globally. When the first element is 13 * written, namespaces will automatically be declared. 14 * 15 * The writeAttribute, startElement and writeElement can now take a 16 * clark-notation element name (example: {http://www.w3.org/2005/Atom}link). 17 * 18 * If, when writing the namespace is a known one a prefix will automatically be 19 * selected, otherwise a random prefix will be generated. 20 * 21 * Instead of standard string values, the writer can take Element classes (as 22 * defined by this library) to delegate the serialization. 23 * 24 * The write() method can take array structures to quickly write out simple xml 25 * trees. 26 * 27 * @copyright Copyright (C) 2009-2015 fruux GmbH (https://fruux.com/). 28 * @author Evert Pot (http://evertpot.com/) 29 * @license http://sabre.io/license/ Modified BSD License 30 */ 31class Writer extends XMLWriter { 32 33 use ContextStackTrait; 34 35 /** 36 * Any namespace that the writer is asked to write, will be added here. 37 * 38 * Any of these elements will get a new namespace definition *every single 39 * time* they are used, but this array allows the writer to make sure that 40 * the prefixes are consistent anyway. 41 * 42 * @var array 43 */ 44 protected $adhocNamespaces = []; 45 46 /** 47 * When the first element is written, this flag is set to true. 48 * 49 * This ensures that the namespaces in the namespaces map are only written 50 * once. 51 * 52 * @var bool 53 */ 54 protected $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 96 * @return void 97 */ 98 function write($value) { 99 100 Serializer\standardSerializer($this, $value); 101 102 } 103 104 /** 105 * Opens a new element. 106 * 107 * You can either just use a local elementname, or you can use clark- 108 * notation to start a new element. 109 * 110 * Example: 111 * 112 * $writer->startElement('{http://www.w3.org/2005/Atom}entry'); 113 * 114 * Would result in something like: 115 * 116 * <entry xmlns="http://w3.org/2005/Atom"> 117 * 118 * @param string $name 119 * @return bool 120 */ 121 function startElement($name) { 122 123 if ($name[0] === '{') { 124 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 136 // An empty namespace means it's the global namespace. This is 137 // allowed, but it mustn't get a prefix. 138 if ($namespace === "" || $namespace === null) { 139 $result = $this->startElement($localName); 140 $this->writeAttribute('xmlns', ''); 141 } else { 142 if (!isset($this->adhocNamespaces[$namespace])) { 143 $this->adhocNamespaces[$namespace] = 'x' . (count($this->adhocNamespaces) + 1); 144 } 145 $result = $this->startElementNS($this->adhocNamespaces[$namespace], $localName, $namespace); 146 } 147 } 148 149 } else { 150 $result = parent::startElement($name); 151 } 152 153 if (!$this->namespacesWritten) { 154 155 foreach ($this->namespaceMap as $namespace => $prefix) { 156 $this->writeAttribute(($prefix ? 'xmlns:' . $prefix : 'xmlns'), $namespace); 157 } 158 $this->namespacesWritten = true; 159 160 } 161 162 return $result; 163 164 } 165 166 /** 167 * Write a full element tag and it's contents. 168 * 169 * This method automatically closes the element as well. 170 * 171 * The element name may be specified in clark-notation. 172 * 173 * Examples: 174 * 175 * $writer->writeElement('{http://www.w3.org/2005/Atom}author',null); 176 * becomes: 177 * <author xmlns="http://www.w3.org/2005" /> 178 * 179 * $writer->writeElement('{http://www.w3.org/2005/Atom}author', [ 180 * '{http://www.w3.org/2005/Atom}name' => 'Evert Pot', 181 * ]); 182 * becomes: 183 * <author xmlns="http://www.w3.org/2005" /><name>Evert Pot</name></author> 184 * 185 * @param string $name 186 * @param string $content 187 * @return bool 188 */ 189 function writeElement($name, $content = null) { 190 191 $this->startElement($name); 192 if (!is_null($content)) { 193 $this->write($content); 194 } 195 $this->endElement(); 196 197 } 198 199 /** 200 * Writes a list of attributes. 201 * 202 * Attributes are specified as a key->value array. 203 * 204 * The key is an attribute name. If the key is a 'localName', the current 205 * xml namespace is assumed. If it's a 'clark notation key', this namespace 206 * will be used instead. 207 * 208 * @param array $attributes 209 * @return void 210 */ 211 function writeAttributes(array $attributes) { 212 213 foreach ($attributes as $name => $value) { 214 $this->writeAttribute($name, $value); 215 } 216 217 } 218 219 /** 220 * Writes a new attribute. 221 * 222 * The name may be specified in clark-notation. 223 * 224 * Returns true when successful. 225 * 226 * @param string $name 227 * @param string $value 228 * @return bool 229 */ 230 function writeAttribute($name, $value) { 231 232 if ($name[0] === '{') { 233 234 list( 235 $namespace, 236 $localName 237 ) = Service::parseClarkNotation($name); 238 239 if (array_key_exists($namespace, $this->namespaceMap)) { 240 // It's an attribute with a namespace we know 241 $this->writeAttribute( 242 $this->namespaceMap[$namespace] . ':' . $localName, 243 $value 244 ); 245 } else { 246 247 // We don't know the namespace, we must add it in-line 248 if (!isset($this->adhocNamespaces[$namespace])) { 249 $this->adhocNamespaces[$namespace] = 'x' . (count($this->adhocNamespaces) + 1); 250 } 251 $this->writeAttributeNS( 252 $this->adhocNamespaces[$namespace], 253 $localName, 254 $namespace, 255 $value 256 ); 257 258 } 259 260 } else { 261 return parent::writeAttribute($name, $value); 262 } 263 264 } 265 266} 267