moveChildNamespacesToEnvelope()   A
last analyzed

Complexity

Conditions 4
Paths 3

Size

Total Lines 15
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 4

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 4
eloc 8
c 2
b 0
f 0
nc 3
nop 1
dl 0
loc 15
ccs 9
cts 9
cp 1
crap 4
rs 10
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
The expression return simplexml_import_dom($dom->ownerDocument) could return the type null which is incompatible with the type-hinted return SimpleXMLElement. Consider adding an additional type-check to rule them out.
Loading history...
Bug 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 ignore-type  annotation

126
        return simplexml_import_dom(/** @scrutinizer ignore-type */ $dom->ownerDocument);
Loading history...
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