1<?php
2
3namespace Sabre\Xml;
4
5/**
6 * XML parsing and writing service.
7 *
8 * You are encouraged to make a instance of this for your application and
9 * potentially extend it, as a central API point for dealing with xml and
10 * configuring the reader and writer.
11 *
12 * @copyright Copyright (C) 2009-2015 fruux GmbH (https://fruux.com/).
13 * @author Evert Pot (http://evertpot.com/)
14 * @license http://sabre.io/license/ Modified BSD License
15 */
16class Service {
17
18    /**
19     * This is the element map. It contains a list of XML elements (in clark
20     * notation) as keys and PHP class names as values.
21     *
22     * The PHP class names must implement Sabre\Xml\Element.
23     *
24     * Values may also be a callable. In that case the function will be called
25     * directly.
26     *
27     * @var array
28     */
29    public $elementMap = [];
30
31    /**
32     * This is a list of namespaces that you want to give default prefixes.
33     *
34     * You must make sure you create this entire list before starting to write.
35     * They should be registered on the root element.
36     *
37     * @var array
38     */
39    public $namespaceMap = [];
40
41    /**
42     * This is a list of custom serializers for specific classes.
43     *
44     * The writer may use this if you attempt to serialize an object with a
45     * class that does not implement XmlSerializable.
46     *
47     * Instead it will look at this classmap to see if there is a custom
48     * serializer here. This is useful if you don't want your value objects
49     * to be responsible for serializing themselves.
50     *
51     * The keys in this classmap need to be fully qualified PHP class names,
52     * the values must be callbacks. The callbacks take two arguments. The
53     * writer class, and the value that must be written.
54     *
55     * function (Writer $writer, object $value)
56     *
57     * @var array
58     */
59    public $classMap = [];
60
61    /**
62     * Returns a fresh XML Reader
63     *
64     * @return Reader
65     */
66    function getReader() {
67
68        $r = new Reader();
69        $r->elementMap = $this->elementMap;
70        return $r;
71
72    }
73
74    /**
75     * Returns a fresh xml writer
76     *
77     * @return Writer
78     */
79    function getWriter() {
80
81        $w = new Writer();
82        $w->namespaceMap = $this->namespaceMap;
83        $w->classMap = $this->classMap;
84        return $w;
85
86    }
87
88    /**
89     * Parses a document in full.
90     *
91     * Input may be specified as a string or readable stream resource.
92     * The returned value is the value of the root document.
93     *
94     * Specifying the $contextUri allows the parser to figure out what the URI
95     * of the document was. This allows relative URIs within the document to be
96     * expanded easily.
97     *
98     * The $rootElementName is specified by reference and will be populated
99     * with the root element name of the document.
100     *
101     * @param string|resource $input
102     * @param string|null $contextUri
103     * @param string|null $rootElementName
104     * @throws ParseException
105     * @return array|object|string
106     */
107    function parse($input, $contextUri = null, &$rootElementName = null) {
108
109        if (is_resource($input)) {
110            // Unfortunately the XMLReader doesn't support streams. When it
111            // does, we can optimize this.
112            $input = stream_get_contents($input);
113        }
114        $r = $this->getReader();
115        $r->contextUri = $contextUri;
116        $r->xml($input);
117
118        $result = $r->parse();
119        $rootElementName = $result['name'];
120        return $result['value'];
121
122    }
123
124    /**
125     * Parses a document in full, and specify what the expected root element
126     * name is.
127     *
128     * This function works similar to parse, but the difference is that the
129     * user can specify what the expected name of the root element should be,
130     * in clark notation.
131     *
132     * This is useful in cases where you expected a specific document to be
133     * passed, and reduces the amount of if statements.
134     *
135     * It's also possible to pass an array of expected rootElements if your
136     * code may expect more than one document type.
137     *
138     * @param string|string[] $rootElementName
139     * @param string|resource $input
140     * @param string|null $contextUri
141     * @throws ParseException
142     * @return array|object|string
143     */
144    function expect($rootElementName, $input, $contextUri = null) {
145
146        if (is_resource($input)) {
147            // Unfortunately the XMLReader doesn't support streams. When it
148            // does, we can optimize this.
149            $input = stream_get_contents($input);
150        }
151        $r = $this->getReader();
152        $r->contextUri = $contextUri;
153        $r->xml($input);
154
155        $rootElementName = (array)$rootElementName;
156
157        foreach ($rootElementName as &$rEl) {
158            if ($rEl[0] !== '{') $rEl = '{}' . $rEl;
159        }
160
161        $result = $r->parse();
162        if (!in_array($result['name'], $rootElementName, true)) {
163            throw new ParseException('Expected ' . implode(' or ', (array)$rootElementName) . ' but received ' . $result['name'] . ' as the root element');
164        }
165        return $result['value'];
166
167    }
168
169    /**
170     * Generates an XML document in one go.
171     *
172     * The $rootElement must be specified in clark notation.
173     * The value must be a string, an array or an object implementing
174     * XmlSerializable. Basically, anything that's supported by the Writer
175     * object.
176     *
177     * $contextUri can be used to specify a sort of 'root' of the PHP application,
178     * in case the xml document is used as a http response.
179     *
180     * This allows an implementor to easily create URI's relative to the root
181     * of the domain.
182     *
183     * @param string $rootElementName
184     * @param string|array|XmlSerializable $value
185     * @param string|null $contextUri
186     */
187    function write($rootElementName, $value, $contextUri = null) {
188
189        $w = $this->getWriter();
190        $w->openMemory();
191        $w->contextUri = $contextUri;
192        $w->setIndent(true);
193        $w->startDocument();
194        $w->writeElement($rootElementName, $value);
195        return $w->outputMemory();
196
197    }
198
199    /**
200     * Map an xml element to a PHP class.
201     *
202     * Calling this function will automatically setup the Reader and Writer
203     * classes to turn a specific XML element to a PHP class.
204     *
205     * For example, given a class such as :
206     *
207     * class Author {
208     *   public $firstName;
209     *   public $lastName;
210     * }
211     *
212     * and an XML element such as:
213     *
214     * <author xmlns="http://example.org/ns">
215     *   <firstName>...</firstName>
216     *   <lastName>...</lastName>
217     * </author>
218     *
219     * These can easily be mapped by calling:
220     *
221     * $service->mapValueObject('{http://example.org}author', 'Author');
222     *
223     * @param string $elementName
224     * @param object $className
225     * @return void
226     */
227    function mapValueObject($elementName, $className) {
228        list($namespace) = self::parseClarkNotation($elementName);
229
230        $this->elementMap[$elementName] = function(Reader $reader) use ($className, $namespace) {
231            return \Sabre\Xml\Deserializer\valueObject($reader, $className, $namespace);
232        };
233        $this->classMap[$className] = function(Writer $writer, $valueObject) use ($namespace) {
234            return \Sabre\Xml\Serializer\valueObject($writer, $valueObject, $namespace);
235        };
236        $this->valueObjectMap[$className] = $elementName;
237    }
238
239    /**
240     * Writes a value object.
241     *
242     * This function largely behaves similar to write(), except that it's
243     * intended specifically to serialize a Value Object into an XML document.
244     *
245     * The ValueObject must have been previously registered using
246     * mapValueObject().
247     *
248     * @param object $object
249     * @param string $contextUri
250     * @return void
251     */
252    function writeValueObject($object, $contextUri = null) {
253
254        if (!isset($this->valueObjectMap[get_class($object)])) {
255            throw new \InvalidArgumentException('"' . get_class($object) . '" is not a registered value object class. Register your class with mapValueObject.');
256        }
257        return $this->write(
258            $this->valueObjectMap[get_class($object)],
259            $object,
260            $contextUri
261        );
262
263    }
264
265    /**
266     * Parses a clark-notation string, and returns the namespace and element
267     * name components.
268     *
269     * If the string was invalid, it will throw an InvalidArgumentException.
270     *
271     * @param string $str
272     * @throws InvalidArgumentException
273     * @return array
274     */
275    static function parseClarkNotation($str) {
276        static $cache = [];
277
278        if (!isset($cache[$str])) {
279
280            if (!preg_match('/^{([^}]*)}(.*)$/', $str, $matches)) {
281                throw new \InvalidArgumentException('\'' . $str . '\' is not a valid clark-notation formatted string');
282            }
283
284            $cache[$str] = [
285                $matches[1],
286                $matches[2]
287            ];
288        }
289
290        return $cache[$str];
291    }
292
293    /**
294     * A list of classes and which XML elements they map to.
295     */
296    protected $valueObjectMap = [];
297
298}
299