aboutsummaryrefslogtreecommitdiffstats
path: root/vendor/sabre/xml/lib/Service.php
blob: 9a2c47794e83f93c3815fd1cf2d8c8261bf5677e (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
<?php

declare(strict_types=1);

namespace Sabre\Xml;

/**
 * XML parsing and writing service.
 *
 * You are encouraged to make a instance of this for your application and
 * potentially extend it, as a central API point for dealing with xml and
 * configuring the reader and writer.
 *
 * @copyright Copyright (C) 2009-2015 fruux GmbH (https://fruux.com/).
 * @author Evert Pot (http://evertpot.com/)
 * @license http://sabre.io/license/ Modified BSD License
 */
class Service
{
    /**
     * This is the element map. It contains a list of XML elements (in clark
     * notation) as keys and PHP class names as values.
     *
     * The PHP class names must implement Sabre\Xml\Element.
     *
     * Values may also be a callable. In that case the function will be called
     * directly.
     *
     * @var array
     */
    public $elementMap = [];

    /**
     * This is a list of namespaces that you want to give default prefixes.
     *
     * You must make sure you create this entire list before starting to write.
     * They should be registered on the root element.
     *
     * @var array
     */
    public $namespaceMap = [];

    /**
     * This is a list of custom serializers for specific classes.
     *
     * The writer may use this if you attempt to serialize an object with a
     * class that does not implement XmlSerializable.
     *
     * Instead it will look at this classmap to see if there is a custom
     * serializer here. This is useful if you don't want your value objects
     * to be responsible for serializing themselves.
     *
     * The keys in this classmap need to be fully qualified PHP class names,
     * the values must be callbacks. The callbacks take two arguments. The
     * writer class, and the value that must be written.
     *
     * function (Writer $writer, object $value)
     *
     * @var array
     */
    public $classMap = [];

    /**
     * A bitmask of the LIBXML_* constants.
     *
     * @var int
     */
    public $options = 0;

    /**
     * Returns a fresh XML Reader.
     */
    public function getReader(): Reader
    {
        $r = new Reader();
        $r->elementMap = $this->elementMap;

        return $r;
    }

    /**
     * Returns a fresh xml writer.
     */
    public function getWriter(): Writer
    {
        $w = new Writer();
        $w->namespaceMap = $this->namespaceMap;
        $w->classMap = $this->classMap;

        return $w;
    }

    /**
     * Parses a document in full.
     *
     * Input may be specified as a string or readable stream resource.
     * The returned value is the value of the root document.
     *
     * Specifying the $contextUri allows the parser to figure out what the URI
     * of the document was. This allows relative URIs within the document to be
     * expanded easily.
     *
     * The $rootElementName is specified by reference and will be populated
     * with the root element name of the document.
     *
     * @param string|resource $input
     *
     * @throws ParseException
     *
     * @return array|object|string
     */
    public function parse($input, string $contextUri = null, string &$rootElementName = null)
    {
        if (is_resource($input)) {
            // Unfortunately the XMLReader doesn't support streams. When it
            // does, we can optimize this.
            $input = (string) stream_get_contents($input);

            // If input is an empty string, then its safe to throw exception
            if ('' === $input) {
                throw new ParseException('The input element to parse is empty. Do not attempt to parse');
            }
        }
        $r = $this->getReader();
        $r->contextUri = $contextUri;
        $r->XML($input, null, $this->options);

        $result = $r->parse();
        $rootElementName = $result['name'];

        return $result['value'];
    }

    /**
     * Parses a document in full, and specify what the expected root element
     * name is.
     *
     * This function works similar to parse, but the difference is that the
     * user can specify what the expected name of the root element should be,
     * in clark notation.
     *
     * This is useful in cases where you expected a specific document to be
     * passed, and reduces the amount of if statements.
     *
     * It's also possible to pass an array of expected rootElements if your
     * code may expect more than one document type.
     *
     * @param string|string[] $rootElementName
     * @param string|resource $input
     *
     * @throws ParseException
     *
     * @return array|object|string
     */
    public function expect($rootElementName, $input, string $contextUri = null)
    {
        if (is_resource($input)) {
            // Unfortunately the XMLReader doesn't support streams. When it
            // does, we can optimize this.
            $input = (string) stream_get_contents($input);

            // If input is empty string, then its safe to throw exception
            if ('' === $input) {
                throw new ParseException('The input element to parse is empty. Do not attempt to parse');
            }
        }
        $r = $this->getReader();
        $r->contextUri = $contextUri;
        $r->XML($input, null, $this->options);

        $rootElementName = (array) $rootElementName;

        foreach ($rootElementName as &$rEl) {
            if ('{' !== $rEl[0]) {
                $rEl = '{}'.$rEl;
            }
        }

        $result = $r->parse();
        if (!in_array($result['name'], $rootElementName, true)) {
            throw new ParseException('Expected '.implode(' or ', $rootElementName).' but received '.$result['name'].' as the root element');
        }

        return $result['value'];
    }

    /**
     * Generates an XML document in one go.
     *
     * The $rootElement must be specified in clark notation.
     * The value must be a string, an array or an object implementing
     * XmlSerializable. Basically, anything that's supported by the Writer
     * object.
     *
     * $contextUri can be used to specify a sort of 'root' of the PHP application,
     * in case the xml document is used as a http response.
     *
     * This allows an implementor to easily create URI's relative to the root
     * of the domain.
     *
     * @param string|array|object|XmlSerializable $value
     *
     * @return string
     */
    public function write(string $rootElementName, $value, string $contextUri = null)
    {
        $w = $this->getWriter();
        $w->openMemory();
        $w->contextUri = $contextUri;
        $w->setIndent(true);
        $w->startDocument();
        $w->writeElement($rootElementName, $value);

        return $w->outputMemory();
    }

    /**
     * Map an xml element to a PHP class.
     *
     * Calling this function will automatically setup the Reader and Writer
     * classes to turn a specific XML element to a PHP class.
     *
     * For example, given a class such as :
     *
     * class Author {
     *   public $firstName;
     *   public $lastName;
     * }
     *
     * and an XML element such as:
     *
     * <author xmlns="http://example.org/ns">
     *   <firstName>...</firstName>
     *   <lastName>...</lastName>
     * </author>
     *
     * These can easily be mapped by calling:
     *
     * $service->mapValueObject('{http://example.org}author', 'Author');
     */
    public function mapValueObject(string $elementName, string $className)
    {
        list($namespace) = self::parseClarkNotation($elementName);

        $this->elementMap[$elementName] = function (Reader $reader) use ($className, $namespace) {
            return \Sabre\Xml\Deserializer\valueObject($reader, $className, $namespace);
        };
        $this->classMap[$className] = function (Writer $writer, $valueObject) use ($namespace) {
            return \Sabre\Xml\Serializer\valueObject($writer, $valueObject, $namespace);
        };
        $this->valueObjectMap[$className] = $elementName;
    }

    /**
     * Writes a value object.
     *
     * This function largely behaves similar to write(), except that it's
     * intended specifically to serialize a Value Object into an XML document.
     *
     * The ValueObject must have been previously registered using
     * mapValueObject().
     *
     * @param object $object
     *
     * @throws \InvalidArgumentException
     */
    public function writeValueObject($object, string $contextUri = null)
    {
        if (!isset($this->valueObjectMap[get_class($object)])) {
            throw new \InvalidArgumentException('"'.get_class($object).'" is not a registered value object class. Register your class with mapValueObject.');
        }

        return $this->write(
            $this->valueObjectMap[get_class($object)],
            $object,
            $contextUri
        );
    }

    /**
     * Parses a clark-notation string, and returns the namespace and element
     * name components.
     *
     * If the string was invalid, it will throw an InvalidArgumentException.
     *
     * @throws \InvalidArgumentException
     */
    public static function parseClarkNotation(string $str): array
    {
        static $cache = [];

        if (!isset($cache[$str])) {
            if (!preg_match('/^{([^}]*)}(.*)$/', $str, $matches)) {
                throw new \InvalidArgumentException('\''.$str.'\' is not a valid clark-notation formatted string');
            }

            $cache[$str] = [
                $matches[1],
                $matches[2],
            ];
        }

        return $cache[$str];
    }

    /**
     * A list of classes and which XML elements they map to.
     */
    protected $valueObjectMap = [];
}