Skip to content

Commit e4cc660

Browse files
authored
Merge pull request #86 from veewee/perf-plain-xmlwriter-per-element
Write each element with a plain XMLWriter
2 parents c366aa4 + e4c9599 commit e4cc660

10 files changed

Lines changed: 455 additions & 55 deletions

‎src/Fault/Encoder/Soap11FaultEncoder.php‎

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,17 @@
55

66
use Soap\Encoding\Fault\Soap11Fault;
77
use Soap\Encoding\Restriction\WhitespaceRestriction;
8+
use Soap\Encoding\Xml\Writer\InMemoryXmlWriter;
89
use Soap\Xml\Xmlns;
910
use VeeWee\Reflecta\Iso\Iso;
1011
use VeeWee\Xml\Dom\Document;
11-
use VeeWee\Xml\Writer\Writer;
1212
use function Psl\invariant;
1313
use function VeeWee\Xml\Dom\Xpath\Configurator\namespaces;
1414
use function VeeWee\Xml\Writer\Builder\children;
1515
use function VeeWee\Xml\Writer\Builder\element;
1616
use function VeeWee\Xml\Writer\Builder\namespaced_element;
1717
use function VeeWee\Xml\Writer\Builder\raw;
1818
use function VeeWee\Xml\Writer\Builder\value;
19-
use function VeeWee\Xml\Writer\Mapper\memory_output;
2019

2120
/**
2221
* @implements SoapFaultEncoder<Soap11Fault>
@@ -41,9 +40,8 @@ private function to(Soap11Fault $fault): string
4140
{
4241
$envNamespace = Xmlns::soap11Envelope()->value();
4342

44-
/** @var non-empty-string */
45-
return Writer::inMemory()
46-
->write(children([
43+
return (new InMemoryXmlWriter())(
44+
children([
4745
namespaced_element(
4846
$envNamespace,
4947
'env',
@@ -70,8 +68,8 @@ private function to(Soap11Fault $fault): string
7068
...($fault->detail !== null ? [raw($fault->detail)] : []),
7169
])
7270
)
73-
]))
74-
->map(memory_output());
71+
])
72+
);
7573
}
7674

7775
/**

‎src/Fault/Encoder/Soap12FaultEncoder.php‎

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,16 @@
55

66
use Soap\Encoding\Fault\Soap12Fault;
77
use Soap\Encoding\Restriction\WhitespaceRestriction;
8+
use Soap\Encoding\Xml\Writer\InMemoryXmlWriter;
89
use VeeWee\Reflecta\Iso\Iso;
910
use VeeWee\Xml\Dom\Document;
10-
use VeeWee\Xml\Writer\Writer;
1111
use function Psl\invariant;
1212
use function VeeWee\Xml\Dom\Xpath\Configurator\namespaces;
1313
use function VeeWee\Xml\Writer\Builder\children;
1414
use function VeeWee\Xml\Writer\Builder\namespaced_element;
1515
use function VeeWee\Xml\Writer\Builder\prefixed_element;
1616
use function VeeWee\Xml\Writer\Builder\raw;
1717
use function VeeWee\Xml\Writer\Builder\value;
18-
use function VeeWee\Xml\Writer\Mapper\memory_output;
1918

2019
/**
2120
* @implements SoapFaultEncoder<Soap12Fault>
@@ -40,9 +39,8 @@ public function iso(): Iso
4039
*/
4140
private function to(Soap12Fault $fault): string
4241
{
43-
/** @var non-empty-string */
44-
return Writer::inMemory()
45-
->write(children([
42+
return (new InMemoryXmlWriter())(
43+
children([
4644
namespaced_element(
4745
self::ENV_NAMESPACE,
4846
'env',
@@ -113,8 +111,8 @@ private function to(Soap12Fault $fault): string
113111
...($fault->detail !== null ? [raw($fault->detail)] : []),
114112
])
115113
)
116-
]))
117-
->map(memory_output());
114+
])
115+
);
118116
}
119117

120118
/**
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Soap\Encoding\Xml\Writer;
5+
6+
use Generator;
7+
use Throwable;
8+
use VeeWee\Xml\Exception\RuntimeException;
9+
use XMLWriter;
10+
use function count;
11+
use function VeeWee\Xml\ErrorHandling\issue_collection_from_xml_errors;
12+
13+
/**
14+
* Runs once per encoded element, so it drives XMLWriter directly: cheaper than VeeWee\Xml\Writer\Writer,
15+
* while throwing the same exceptions as Writer::write() with disallow_issues().
16+
*/
17+
final class InMemoryXmlWriter
18+
{
19+
/**
20+
* @param callable(XMLWriter): Generator<bool> $writer
21+
*
22+
* @throws RuntimeException
23+
*
24+
* @return non-empty-string
25+
*/
26+
public function __invoke(callable $writer): string
27+
{
28+
$xmlWriter = XMLWriter::toMemory();
29+
30+
$previousErrorReporting = libxml_use_internal_errors(true);
31+
libxml_clear_errors();
32+
33+
$failure = null;
34+
try {
35+
foreach ($writer($xmlWriter) as $written) {
36+
if ($written === false) {
37+
throw RuntimeException::withMessage('Could not write the provided XML to the stream.');
38+
}
39+
}
40+
} catch (Throwable $exception) {
41+
$failure = $exception;
42+
}
43+
44+
$errors = libxml_get_errors();
45+
libxml_clear_errors();
46+
libxml_use_internal_errors($previousErrorReporting);
47+
48+
if ($failure !== null) {
49+
throw RuntimeException::combineExceptionWithIssues($failure, issue_collection_from_xml_errors($errors));
50+
}
51+
52+
if ($errors) {
53+
$issues = issue_collection_from_xml_errors($errors);
54+
if (count($issues)) {
55+
throw RuntimeException::fromIssues('XML issues detected: ', $issues);
56+
}
57+
}
58+
59+
/** @psalm-var non-empty-string */
60+
return $xmlWriter->outputMemory();
61+
}
62+
}

‎src/Xml/Writer/SoapEnvelopeWriter.php‎

Lines changed: 31 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,12 @@
1212
use Soap\WsdlReader\Model\Definitions\SoapVersion;
1313
use Soap\Xml\Xmlns;
1414
use VeeWee\Xml\Exception\RuntimeException as XmlRuntimeException;
15-
use VeeWee\Xml\Writer\Writer;
1615
use XMLWriter;
1716
use function Psl\Vec\filter_nulls;
1817
use function VeeWee\Xml\Writer\Builder\children;
1918
use function VeeWee\Xml\Writer\Builder\namespace_attribute;
2019
use function VeeWee\Xml\Writer\Builder\namespaced_attribute;
2120
use function VeeWee\Xml\Writer\Builder\namespaced_element;
22-
use function VeeWee\Xml\Writer\Mapper\memory_output;
2321

2422
final class SoapEnvelopeWriter
2523
{
@@ -45,45 +43,43 @@ public function __invoke(): string
4543
};
4644

4745
try {
48-
return Writer::inMemory()
49-
->write(
46+
return (new InMemoryXmlWriter())(
47+
namespaced_element(
48+
$envelopeNamespace,
49+
'SOAP-ENV',
50+
'Envelope',
5051
namespaced_element(
5152
$envelopeNamespace,
5253
'SOAP-ENV',
53-
'Envelope',
54-
namespaced_element(
55-
$envelopeNamespace,
56-
'SOAP-ENV',
57-
'Body',
58-
children(
59-
filter_nulls([
60-
// In SOAP 1.2 the position of the encoding attributes is limited:
61-
// See: https://www.w3.org/TR/soap12-part1/#soapencattr
62-
// For SOAP 1.1 it can be everywhere:
63-
// See: https://www.w3.org/TR/2000/NOTE-SOAP-20000508/#_Toc478383495
64-
$this->encodingStyle
65-
->filter(fn (): bool => $this->bindingUse === BindingUse::ENCODED)
66-
->map(
67-
static fn (EncodingStyle $encodingStyle) => children([
68-
namespace_attribute(
69-
$encodingStyle->value,
70-
'SOAP-ENC'
71-
),
72-
namespaced_attribute(
73-
$envelopeNamespace,
74-
'SOAP-ENV',
75-
'encodingStyle',
76-
$encodingStyle->value
77-
)
78-
])
79-
)->unwrapOr(null),
80-
$this->children
81-
])
82-
)
54+
'Body',
55+
children(
56+
filter_nulls([
57+
// In SOAP 1.2 the position of the encoding attributes is limited:
58+
// See: https://www.w3.org/TR/soap12-part1/#soapencattr
59+
// For SOAP 1.1 it can be everywhere:
60+
// See: https://www.w3.org/TR/2000/NOTE-SOAP-20000508/#_Toc478383495
61+
$this->encodingStyle
62+
->filter(fn (): bool => $this->bindingUse === BindingUse::ENCODED)
63+
->map(
64+
static fn (EncodingStyle $encodingStyle) => children([
65+
namespace_attribute(
66+
$encodingStyle->value,
67+
'SOAP-ENC'
68+
),
69+
namespaced_attribute(
70+
$envelopeNamespace,
71+
'SOAP-ENV',
72+
'encodingStyle',
73+
$encodingStyle->value
74+
)
75+
])
76+
)->unwrapOr(null),
77+
$this->children
78+
])
8379
)
8480
)
8581
)
86-
->map(memory_output());
82+
);
8783
} catch (XmlRuntimeException $e) {
8884
throw ($e->getPrevious() instanceof EncodingExceptionInterface) ? $e->getPrevious() : $e;
8985
}

‎src/Xml/Writer/XsdTypeXmlElementWriter.php‎

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,20 @@
55

66
use Generator;
77
use Soap\Encoding\Encoder\Context;
8-
use VeeWee\Xml\Writer\Writer;
8+
use VeeWee\Xml\Exception\RuntimeException;
99
use XMLWriter;
10-
use function VeeWee\Xml\Writer\Mapper\memory_output;
1110

1211
final class XsdTypeXmlElementWriter
1312
{
1413
/**
1514
* @param callable(XMLWriter): Generator<bool> $children
1615
*
16+
* @throws RuntimeException
17+
*
1718
* @return non-empty-string
1819
*/
1920
public function __invoke(Context $context, callable $children): string
2021
{
21-
/** @psalm-var non-empty-string */
22-
return Writer::inMemory()
23-
->write(new ElementBuilder($context, $children(...)))
24-
->map(memory_output());
22+
return (new InMemoryXmlWriter())(new ElementBuilder($context, $children(...)));
2523
}
2624
}

‎tests/Unit/Fault/Encoder/Soap11FaultEncoderTest.php‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,21 @@
1313
#[CoversClass(Soap11Fault::class)]
1414
final class Soap11FaultEncoderTest extends AbstractFaultEncoderTests
1515
{
16+
public function test_it_writes_the_exact_fault_bytes(): void
17+
{
18+
$actual = (new Soap11FaultEncoder())->iso()->to(new Soap11Fault(
19+
faultCode: 'env:Server',
20+
faultString: 'a < b',
21+
faultActor: 'uri:actor',
22+
detail: '<detail><element>value</element></detail>',
23+
));
24+
25+
static::assertSame(
26+
'<env:Fault xmlns:env="http://schemas.xmlsoap.org/soap/envelope/"><faultcode>env:Server</faultcode><faultstring>a &lt; b</faultstring><faultactor>uri:actor</faultactor><detail><element>value</element></detail></env:Fault>',
27+
$actual
28+
);
29+
}
30+
1631
public static function provideIsomorphicCases(): iterable
1732
{
1833
$baseConfig = [

‎tests/Unit/Fault/Encoder/Soap12FaultEncoderTest.php‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,23 @@
1313
#[CoversClass(Soap12Fault::class)]
1414
final class Soap12FaultEncoderTest extends AbstractFaultEncoderTests
1515
{
16+
public function test_it_writes_the_exact_fault_bytes(): void
17+
{
18+
$actual = (new Soap12FaultEncoder())->iso()->to(new Soap12Fault(
19+
code: 'env:Sender',
20+
reason: 'a < b',
21+
subCode: 'm:Invalid',
22+
node: 'uri:node',
23+
role: 'uri:role',
24+
detail: '<env:Detail><element>value</element></env:Detail>',
25+
));
26+
27+
static::assertSame(
28+
'<env:Fault xmlns:env="http://www.w3.org/2003/05/soap-envelope"><env:Code><env:Value>env:Sender</env:Value><env:Subcode><env:Value>m:Invalid</env:Value></env:Subcode></env:Code><env:Reason><env:Text>a &lt; b</env:Text></env:Reason><env:Node>uri:node</env:Node><env:Role>uri:role</env:Role><env:Detail><element>value</element></env:Detail></env:Fault>',
29+
$actual
30+
);
31+
}
32+
1633
public static function provideIsomorphicCases(): iterable
1734
{
1835
$baseConfig = [

0 commit comments

Comments
 (0)