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     * @return void
142     */
143    function expect($rootElementName, $input, $contextUri = null) {
144
145        if (is_resource($input)) {
146            // Unfortunately the XMLReader doesn't support streams. When it
147            // does, we can optimize this.
148            $input = stream_get_contents($input);
149        }
150        $r = $this->getReader();
151        $r->contextUri = $contextUri;
152        $r->xml($input);
153
154        $rootElementName = (array)$rootElementName;
155
156        foreach ($rootElementName as &$rEl) {
157            if ($rEl[0] !== '{') $rEl = '{}' . $rEl;
158        }
159
160        $result = $r->parse();
161        if (!in_array($result['name'], $rootElementName, true)) {
162            throw new ParseException('Expected ' . implode(' or ', (array)$rootElementName) . ' but received ' . $result['name'] . ' as the root element');
163        }
164        return $result['value'];
165
166    }
167
168    /**
169     * Generates an XML document in one go.
170     *
171     * The $rootElement must be specified in clark notation.
172     * The value must be a string, an array or an object implementing
173     * XmlSerializable. Basically, anything that's supported by the Writer
174     * object.
175     *
176     * $contextUri can be used to specify a sort of 'root' of the PHP application,
177     * in case the xml document is used as a http response.
178     *
179     * This allows an implementor to easily create URI's relative to the root
180     * of the domain.
181     *
182     * @param string $rootElementName
183     * @param string|array|XmlSerializable $value
184     * @param string|null $contextUri
185     */
186    function write($rootElementName, $value, $contextUri = null) {
187
188        $w = $this->getWriter();
189        $w->openMemory();
190        $w->contextUri = $contextUri;
191        $w->setIndent(true);
192        $w->startDocument();
193        $w->writeElement($rootElementName, $value);
194        return $w->outputMemory();
195
196    }
197
198    /**
199     * Map an xml element to a PHP class.
200     *
201     * Calling this function will automatically setup the Reader and Writer
202     * classes to turn a specific XML element to a PHP class.
203     *
204     * For example, given a class such as :
205     *
206     * class Author {
207     *   public $firstName;
208     *   public $lastName;
209     * }
210     *
211     * and an XML element such as:
212     *
213     * <author xmlns="http://example.org/ns">
214     *   <firstName>...</firstName>
215     *   <lastName>...</lastName>
216     * </author>
217     *
218     * These can easily be mapped by calling:
219     *
220     * $service->mapValueObject('{http://example.org}author', 'Author');
221     *
222     * @param string $elementName
223     * @param object $className
224     * @return void
225     */
226    function mapValueObject($elementName, $className) {
227        list($namespace) = self::parseClarkNotation($elementName);
228
229        $this->elementMap[$elementName] = function(Reader $reader) use ($className, $namespace) {
230            return \Sabre\Xml\Deserializer\valueObject($reader, $className, $namespace);
231        };
232        $this->classMap[$className] = function(Writer $writer, $valueObject) use ($namespace) {
233            return \Sabre\Xml\Serializer\valueObject($writer, $valueObject, $namespace);
234        };
235        $this->valueObjectMap[$className] = $elementName;
236    }
237
238    /**
239     * Writes a value object.
240     *
241     * This function largely behaves similar to write(), except that it's
242     * intended specifically to serialize a Value Object into an XML document.
243     *
244     * The ValueObject must have been previously registered using
245     * mapValueObject().
246     *
247     * @param object $object
248     * @param string $contextUri
249     * @return void
250     */
251    function writeValueObject($object, $contextUri = null) {
252
253        if (!isset($this->valueObjectMap[get_class($object)])) {
254            throw new \InvalidArgumentException('"' . get_class($object) . '" is not a registered value object class. Register your class with mapValueObject.');
255        }
256        return $this->write(
257            $this->valueObjectMap[get_class($object)],
258            $object,
259            $contextUri
260        );
261
262    }
263
264    /**
265     * Parses a clark-notation string, and returns the namespace and element
266     * name components.
267     *
268     * If the string was invalid, it will throw an InvalidArgumentException.
269     *
270     * @param string $str
271     * @throws InvalidArgumentException
272     * @return array
273     */
274    static function parseClarkNotation($str) {
275        static $cache = [];
276
277        if (!isset($cache[$str])) {
278
279            if (!preg_match('/^{([^}]*)}(.*)$/', $str, $matches)) {
280                throw new \InvalidArgumentException('\'' . $str . '\' is not a valid clark-notation formatted string');
281            }
282
283            $cache[$str] = [
284                $matches[1],
285                $matches[2]
286            ];
287        }
288
289        return $cache[$str];
290    }
291
292    /**
293     * A list of classes and which XML elements they map to.
294     */
295    protected $valueObjectMap = [];
296
297}
298