1 | <?php |
||||
2 | |||||
3 | namespace DMT\Soap\Serializer; |
||||
4 | |||||
5 | use JMS\Serializer\EventDispatcher\EventSubscriberInterface; |
||||
6 | use JMS\Serializer\EventDispatcher\PreDeserializeEvent; |
||||
7 | use JMS\Serializer\EventDispatcher\PreSerializeEvent; |
||||
8 | use JMS\Serializer\Exception\InvalidArgumentException; |
||||
9 | use JMS\Serializer\Exception\RuntimeException; |
||||
10 | use JMS\Serializer\SerializationContext; |
||||
11 | use JMS\Serializer\XmlSerializationVisitor; |
||||
12 | use Metadata\ClassMetadata; |
||||
13 | use SimpleXMLElement; |
||||
14 | |||||
15 | /** |
||||
16 | * Class SoapMessageEventSubscriber |
||||
17 | * |
||||
18 | * @package DMT\Soap |
||||
19 | */ |
||||
20 | class SoapMessageEventSubscriber implements EventSubscriberInterface, SoapNamespaceInterface |
||||
21 | { |
||||
22 | /** |
||||
23 | * {@inheritdoc} |
||||
24 | */ |
||||
25 | 19 | public static function getSubscribedEvents(): array |
|||
26 | { |
||||
27 | return [ |
||||
28 | [ |
||||
29 | 19 | 'event' => 'serializer.pre_serialize', |
|||
30 | 'method' => 'addMessage', |
||||
31 | 'format' => 'soap', |
||||
32 | ], |
||||
33 | [ |
||||
34 | 'event' => 'serializer.pre_deserialize', |
||||
35 | 'method' => 'getMessage', |
||||
36 | 'format' => 'soap', |
||||
37 | ], |
||||
38 | ]; |
||||
39 | } |
||||
40 | |||||
41 | /** |
||||
42 | * @param PreSerializeEvent $event |
||||
43 | */ |
||||
44 | 11 | public function addMessage(PreSerializeEvent $event) |
|||
45 | { |
||||
46 | /** @var SerializationContext $context */ |
||||
47 | 11 | $context = $event->getContext(); |
|||
48 | /** @var XmlSerializationVisitor $visitor */ |
||||
49 | 11 | $visitor = $context->getVisitor(); |
|||
50 | |||||
51 | 11 | if ($context->getDepth() === 1 && $visitor->getCurrentNode()->nodeName === 'soap:Body') { |
|||
52 | /** @var ClassMetadata $metadata */ |
||||
53 | 11 | $metadata = $context->getMetadataFactory()->getMetadataForClass($event->getType()['name']); |
|||
54 | |||||
55 | 11 | if (!isset($metadata->xmlRootName, $metadata->xmlRootNamespace)) { |
|||
56 | 3 | throw new RuntimeException('Missing XmlRootName or XmlRootNamespace for ' . $event->getType()['name']); |
|||
57 | } |
||||
58 | |||||
59 | 8 | $tagName = $metadata->xmlRootName; |
|||
60 | 8 | if ($metadata->xmlRootPrefix !== null) { |
|||
61 | 3 | $tagName = $metadata->xmlRootPrefix . ':' . $metadata->xmlRootName; |
|||
62 | } |
||||
63 | |||||
64 | 8 | $document = $visitor->getDocument(); |
|||
65 | 8 | $message = $document->createElementNS($metadata->xmlRootNamespace, $tagName); |
|||
66 | |||||
67 | 8 | $visitor->getCurrentNode()->appendChild($message); |
|||
68 | 8 | $visitor->setCurrentNode($message); |
|||
69 | } |
||||
70 | 8 | } |
|||
71 | |||||
72 | /** |
||||
73 | * @param PreDeserializeEvent $event |
||||
74 | */ |
||||
75 | 11 | public function getMessage(PreDeserializeEvent $event) |
|||
76 | { |
||||
77 | /** @var SerializationContext $context */ |
||||
78 | 11 | $context = $event->getContext(); |
|||
79 | |||||
80 | 11 | if ($context->getDepth() === 1) { |
|||
81 | 11 | $element = $this->moveChildNamespacesToEnvelope($event->getData()); |
|||
82 | |||||
83 | 11 | $version = array_search(current($element->getNamespaces()), static::SOAP_NAMESPACES); |
|||
84 | 11 | if (!$version) { |
|||
85 | 1 | throw new InvalidArgumentException('Unsupported SOAP version'); |
|||
86 | } |
||||
87 | |||||
88 | 10 | $messages = $element->xpath('*[local-name()="Body"]/*'); |
|||
89 | 10 | if (count($messages) === 1) { |
|||
90 | 10 | $element = $messages[0]; |
|||
91 | } |
||||
92 | |||||
93 | 10 | if ($element->getName() === 'Fault') { |
|||
94 | 6 | if ($version == static::SOAP_1_1) { |
|||
95 | 3 | $this->throwSoap11Fault($element); |
|||
96 | } else { |
||||
97 | 3 | $this->throwSoap12Fault($element); |
|||
98 | } |
||||
99 | } |
||||
100 | |||||
101 | 4 | $event->setData($element); |
|||
102 | } |
||||
103 | 4 | } |
|||
104 | |||||
105 | /** |
||||
106 | * Move all underlying namespaces to root element. |
||||
107 | * |
||||
108 | * @param SimpleXMLElement $element |
||||
109 | * |
||||
110 | * @return SimpleXMLElement |
||||
111 | */ |
||||
112 | 11 | protected function moveChildNamespacesToEnvelope(SimpleXMLElement $element): SimpleXMLElement |
|||
113 | { |
||||
114 | 11 | $dom = dom_import_simplexml($element); |
|||
115 | |||||
116 | 11 | foreach ($element->getNamespaces(true) as $prefix => $namespace) { |
|||
117 | 11 | if (!in_array($namespace, $element->getDocNamespaces())) { |
|||
118 | 7 | $dom->setAttributeNS( |
|||
119 | 7 | 'http://www.w3.org/2000/xmlns/', |
|||
120 | 7 | $prefix ? 'xmlns:' . $prefix : 'xmlns', |
|||
121 | 11 | $namespace |
|||
122 | ); |
||||
123 | } |
||||
124 | } |
||||
125 | |||||
126 | 11 | return simplexml_import_dom($dom->ownerDocument); |
|||
0 ignored issues
–
show
Bug
Best Practice
introduced
by
![]() It seems like
$dom->ownerDocument can also be of type null ; however, parameter $node of simplexml_import_dom() does only seem to accept DOMNode , maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
127 | } |
||||
128 | |||||
129 | /** |
||||
130 | * @param SimpleXMLElement $fault |
||||
131 | * |
||||
132 | * @throws SoapFaultException |
||||
133 | */ |
||||
134 | 3 | protected function throwSoap11Fault(SimpleXMLElement $fault) |
|||
135 | { |
||||
136 | 3 | $faultcode = $faultstring = ''; |
|||
137 | 3 | $faultactor = $detail = null; |
|||
138 | |||||
139 | 3 | $elements = array_filter($this->elementToArray($fault)); |
|||
140 | 3 | extract($elements, EXTR_IF_EXISTS); |
|||
141 | |||||
142 | 3 | throw new SoapFaultException($this->removePrefix($faultcode), $faultstring, $faultactor, $detail); |
|||
143 | } |
||||
144 | |||||
145 | /** |
||||
146 | * @param SimpleXMLElement $fault |
||||
147 | * |
||||
148 | * @throws SoapFaultException |
||||
149 | */ |
||||
150 | 3 | protected function throwSoap12Fault(SimpleXMLElement $fault) |
|||
151 | { |
||||
152 | 3 | $lang = substr(locale_get_default(), 0, 2); |
|||
153 | 3 | $path = '*[local-name()="Reason"]/*[local-name()="Text" and @xml:lang="' . $lang . '"]'; |
|||
154 | |||||
155 | 3 | $messages = $fault->xpath($path); |
|||
156 | 3 | if (count($messages) === 0) { |
|||
157 | 1 | $messages = $fault->xpath('*[local-name()="Reason"]/*[local-name()="Text"]'); |
|||
158 | } |
||||
159 | |||||
160 | 3 | $code = array_map( |
|||
161 | 3 | [$this, 'removePrefix'], |
|||
162 | 3 | $fault->xpath('//*[local-name()="Code" or local-name()="Subcode"]/*[local-name()="Value"]') |
|||
163 | ); |
||||
164 | |||||
165 | 3 | $node = $fault->xpath('*[local-name()="Node"]')[0] ?? null; |
|||
166 | 3 | $detail = $fault->xpath('*[local-name()="Detail"]')[0] ?? null; |
|||
167 | |||||
168 | 3 | if ($detail !== null) { |
|||
169 | 3 | $detail = $this->elementToArray($detail); |
|||
170 | } |
||||
171 | |||||
172 | 3 | throw new SoapFaultException(implode('.', $code), $messages[0], $node, $detail); |
|||
173 | } |
||||
174 | |||||
175 | /** |
||||
176 | * @param SimpleXMLElement $element |
||||
177 | * |
||||
178 | * @return array |
||||
179 | */ |
||||
180 | 6 | protected function elementToArray(SimpleXMLElement $element): array |
|||
181 | { |
||||
182 | 6 | $result = []; |
|||
183 | 6 | foreach ($element->xpath('*') as $node) { |
|||
184 | 6 | if (count($node->xpath('*')) > 0) { |
|||
185 | 2 | $value = $this->elementToArray($node); |
|||
186 | } else { |
||||
187 | 6 | $value = strval($node); |
|||
188 | } |
||||
189 | 6 | $result = array_merge_recursive($result, [$node->getName() => $value]); |
|||
190 | } |
||||
191 | |||||
192 | 6 | return $result; |
|||
193 | } |
||||
194 | |||||
195 | /** |
||||
196 | * Remove a prefix from text node. |
||||
197 | * |
||||
198 | * @param string $value A node value containing a namespace prefix, eg SOAP-ENV:Client. |
||||
199 | * |
||||
200 | * @return string |
||||
201 | */ |
||||
202 | 6 | protected function removePrefix(string $value): string |
|||
203 | { |
||||
204 | 6 | return preg_replace('~^(.+:)?(.*)$~', "$2", $value); |
|||
205 | } |
||||
206 | } |
||||
207 |