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