Passed
Pull Request — master (#27)
by
unknown
02:19
created

SoapMessageEventSubscriber   A

Complexity

Total Complexity 24

Size/Duplication

Total Lines 186
Duplicated Lines 0 %

Test Coverage

Coverage 98.61%

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 72
c 3
b 0
f 0
dl 0
loc 186
ccs 71
cts 72
cp 0.9861
rs 10
wmc 24

8 Methods

Rating   Name   Duplication   Size   Complexity  
A getSubscribedEvents() 0 12 1
A throwSoap11Fault() 0 9 1
A moveChildNamespacesToEnvelope() 0 15 4
A removePrefix() 0 3 1
A getMessage() 0 27 6
A throwSoap12Fault() 0 23 3
A addMessage() 0 26 5
A elementToArray() 0 13 3
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 15
    public static function getSubscribedEvents(): array
26
    {
27
        return [
28
            [
29 15
                '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 7
    public function addMessage(PreSerializeEvent $event)
45
    {
46
        /** @var SerializationContext $context */
47 7
        $context = $event->getContext();
48
        /** @var XmlSerializationVisitor $visitor */
49 7
        $visitor = $context->getVisitor();
50
51 7
        if ($context->getDepth() === 1 && $visitor->getCurrentNode()->nodeName === 'soap:Body') {
52
            /** @var ClassMetadata $metadata */
53 7
            $metadata = $context->getMetadataFactory()->getMetadataForClass($event->getType()['name']);
54
55 7
            if (!isset($metadata->xmlRootName, $metadata->xmlRootNamespace)) {
56 3
                throw new RuntimeException('Missing XmlRootName or XmlRootNamespace for ' . $event->getType()['name']);
57
            }
58
59 4
            if(null !== $metadata->xmlRootPrefix){
60
                $tagName = $metadata->xmlRootPrefix . ':' . $metadata->xmlRootName;
61
            }else{
62 4
                $tagName = $metadata->xmlRootName;
63
            }
64
            
65 4
            $document = $visitor->getDocument();
66 4
            $message = $document->createElementNS($metadata->xmlRootNamespace, $tagName);
67
68 4
            $visitor->getCurrentNode()->appendChild($message);
69 4
            $visitor->setCurrentNode($message);
70
        }
71 4
    }
72
73
    /**
74
     * @param PreDeserializeEvent $event
75
     */
76 11
    public function getMessage(PreDeserializeEvent $event)
77
    {
78
        /** @var SerializationContext $context */
79 11
        $context = $event->getContext();
80
81 11
        if ($context->getDepth() === 1) {
82 11
            $element = $this->moveChildNamespacesToEnvelope($event->getData());
83
84 11
            $version = array_search(current($element->getNamespaces()), static::SOAP_NAMESPACES);
85 11
            if (!$version) {
86 1
                throw new InvalidArgumentException('Unsupported SOAP version');
87
            }
88
89 10
            $messages = $element->xpath('*[local-name()="Body"]/*');
90 10
            if (count($messages) === 1) {
91 10
                $element = $messages[0];
92
            }
93
94 10
            if ($element->getName() === 'Fault') {
95 6
                if ($version == static::SOAP_1_1) {
96 3
                    $this->throwSoap11Fault($element);
97
                } else {
98 3
                    $this->throwSoap12Fault($element);
99
                }
100
            }
101
102 4
            $event->setData($element);
103
        }
104 4
    }
105
106
    /**
107
     * Move all underlying namespaces to root element.
108
     *
109
     * @param SimpleXMLElement $element
110
     *
111
     * @return SimpleXMLElement
112
     */
113 11
    protected function moveChildNamespacesToEnvelope(SimpleXMLElement $element): SimpleXMLElement
114
    {
115 11
        $dom = dom_import_simplexml($element);
116
117 11
        foreach ($element->getNamespaces(true) as $prefix => $namespace) {
118 11
            if (!in_array($namespace, $element->getDocNamespaces())) {
119 7
                $dom->setAttributeNS(
120 7
                    'http://www.w3.org/2000/xmlns/',
121 7
                    $prefix ? 'xmlns:' . $prefix : 'xmlns',
122 11
                    $namespace
123
                );
124
            }
125
        }
126
127 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

127
        return simplexml_import_dom(/** @scrutinizer ignore-type */ $dom->ownerDocument);
Loading history...
128
    }
129
130
    /**
131
     * @param SimpleXMLElement $fault
132
     *
133
     * @throws SoapFaultException
134
     */
135 3
    protected function throwSoap11Fault(SimpleXMLElement $fault)
136
    {
137 3
        $faultcode = $faultstring = '';
138 3
        $faultactor = $detail = null;
139
140 3
        $elements = array_filter($this->elementToArray($fault));
141 3
        extract($elements, EXTR_IF_EXISTS);
142
143 3
        throw new SoapFaultException($this->removePrefix($faultcode), $faultstring, $faultactor, $detail);
144
    }
145
146
    /**
147
     * @param SimpleXMLElement $fault
148
     *
149
     * @throws SoapFaultException
150
     */
151 3
    protected function throwSoap12Fault(SimpleXMLElement $fault)
152
    {
153 3
        $lang = substr(locale_get_default(), 0, 2);
154 3
        $path = '*[local-name()="Reason"]/*[local-name()="Text" and @xml:lang="' . $lang . '"]';
155
156 3
        $messages = $fault->xpath($path);
157 3
        if (count($messages) === 0) {
158 1
            $messages = $fault->xpath('*[local-name()="Reason"]/*[local-name()="Text"]');
159
        }
160
161 3
        $code = array_map(
162 3
            [$this, 'removePrefix'],
163 3
            $fault->xpath('//*[local-name()="Code" or local-name()="Subcode"]/*[local-name()="Value"]')
164
        );
165
166 3
        $node = $fault->xpath('*[local-name()="Node"]')[0] ?? null;
167 3
        $detail = $fault->xpath('*[local-name()="Detail"]')[0] ?? null;
168
169 3
        if ($detail !== null) {
170 3
            $detail = $this->elementToArray($detail);
171
        }
172
173 3
        throw new SoapFaultException(implode('.', $code), $messages[0], $node, $detail);
174
    }
175
176
    /**
177
     * @param SimpleXMLElement $element
178
     *
179
     * @return array
180
     */
181 6
    protected function elementToArray(SimpleXMLElement $element): array
182
    {
183 6
        $result = [];
184 6
        foreach ($element->xpath('*') as $node) {
185 6
            if (count($node->xpath('*')) > 0) {
186 2
                $value = $this->elementToArray($node);
187
            } else {
188 6
                $value = strval($node);
189
            }
190 6
            $result = array_merge_recursive($result, [$node->getName() => $value]);
191
        }
192
193 6
        return $result;
194
    }
195
196
    /**
197
     * Remove a prefix from text node.
198
     *
199
     * @param string $value A node value containing a namespace prefix, eg SOAP-ENV:Client.
200
     *
201
     * @return string
202
     */
203 6
    protected function removePrefix(string $value): string
204
    {
205 6
        return preg_replace('~^(.+:)?(.*)$~', "$2", $value);
206
    }
207
}
208