1<?php
2
3declare(strict_types=1);
4
5namespace Sabre\Xml\Element;
6
7use Sabre\Xml\Element;
8use Sabre\Xml\Reader;
9use Sabre\Xml\Writer;
10
11/**
12 * The XmlFragment element allows you to extract a portion of your xml tree,
13 * and get a well-formed xml string.
14 *
15 * This goes a bit beyond `innerXml` and friends, as we'll also match all the
16 * correct namespaces.
17 *
18 * Please note that the XML fragment:
19 *
20 * 1. Will not have an <?xml declaration.
21 * 2. Or a DTD
22 * 3. It will have all the relevant xmlns attributes.
23 * 4. It may not have a root element.
24 */
25class XmlFragment implements Element
26{
27    /**
28     * The inner XML value.
29     *
30     * @var string
31     */
32    protected $xml;
33
34    /**
35     * Constructor.
36     */
37    public function __construct(string $xml)
38    {
39        $this->xml = $xml;
40    }
41
42    /**
43     * Returns the inner XML document.
44     */
45    public function getXml(): string
46    {
47        return $this->xml;
48    }
49
50    /**
51     * The xmlSerialize metod is called during xml writing.
52     *
53     * Use the $writer argument to write its own xml serialization.
54     *
55     * An important note: do _not_ create a parent element. Any element
56     * implementing XmlSerializble should only ever write what's considered
57     * its 'inner xml'.
58     *
59     * The parent of the current element is responsible for writing a
60     * containing element.
61     *
62     * This allows serializers to be re-used for different element names.
63     *
64     * If you are opening new elements, you must also close them again.
65     */
66    public function xmlSerialize(Writer $writer)
67    {
68        $reader = new Reader();
69
70        // Wrapping the xml in a container, so root-less values can still be
71        // parsed.
72        $xml = <<<XML
73<?xml version="1.0"?>
74<xml-fragment xmlns="http://sabre.io/ns">{$this->getXml()}</xml-fragment>
75XML;
76
77        $reader->xml($xml);
78
79        while ($reader->read()) {
80            if ($reader->depth < 1) {
81                // Skipping the root node.
82                continue;
83            }
84
85            switch ($reader->nodeType) {
86                case Reader::ELEMENT:
87                    $writer->startElement(
88                        (string) $reader->getClark()
89                    );
90                    $empty = $reader->isEmptyElement;
91                    while ($reader->moveToNextAttribute()) {
92                        switch ($reader->namespaceURI) {
93                            case '':
94                                $writer->writeAttribute($reader->localName, $reader->value);
95                                break;
96                            case 'http://www.w3.org/2000/xmlns/':
97                                // Skip namespace declarations
98                                break;
99                            default:
100                                $writer->writeAttribute((string) $reader->getClark(), $reader->value);
101                                break;
102                        }
103                    }
104                    if ($empty) {
105                        $writer->endElement();
106                    }
107                    break;
108                case Reader::CDATA:
109                case Reader::TEXT:
110                    $writer->text(
111                        $reader->value
112                    );
113                    break;
114                case Reader::END_ELEMENT:
115                    $writer->endElement();
116                    break;
117            }
118        }
119    }
120
121    /**
122     * The deserialize method is called during xml parsing.
123     *
124     * This method is called statictly, this is because in theory this method
125     * may be used as a type of constructor, or factory method.
126     *
127     * Often you want to return an instance of the current class, but you are
128     * free to return other data as well.
129     *
130     * You are responsible for advancing the reader to the next element. Not
131     * doing anything will result in a never-ending loop.
132     *
133     * If you just want to skip parsing for this element altogether, you can
134     * just call $reader->next();
135     *
136     * $reader->parseInnerTree() will parse the entire sub-tree, and advance to
137     * the next element.
138     *
139     * @return mixed
140     */
141    public static function xmlDeserialize(Reader $reader)
142    {
143        $result = new self($reader->readInnerXml());
144        $reader->next();
145
146        return $result;
147    }
148}
149