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