From b915152d29a0ff7c75eca62526a867e5c0410b47 Mon Sep 17 00:00:00 2001 From: Toon Verwerft Date: Thu, 15 Oct 2026 20:40:24 +0200 Subject: [PATCH 1/4] Pin the exceptions an element write surfaces Every element goes through Writer::write, which wraps whatever its generator throws in a veewee RuntimeException and turns libxml issues into one. Callers see that wrapping, nested once per element and unwrapped by the envelope only when an EncodingException sits right under it. These tests fix that behaviour before the writer changes. --- .../Xml/Writer/SoapEnvelopeWriterTest.php | 48 ++++ .../Writer/XsdTypeXmlElementWriterTest.php | 213 ++++++++++++++++++ 2 files changed, 261 insertions(+) create mode 100644 tests/Unit/Xml/Writer/XsdTypeXmlElementWriterTest.php diff --git a/tests/Unit/Xml/Writer/SoapEnvelopeWriterTest.php b/tests/Unit/Xml/Writer/SoapEnvelopeWriterTest.php index ec334a3..4774a25 100644 --- a/tests/Unit/Xml/Writer/SoapEnvelopeWriterTest.php +++ b/tests/Unit/Xml/Writer/SoapEnvelopeWriterTest.php @@ -9,13 +9,25 @@ use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Psl\Option\Option; +use Soap\Encoding\Encoder\Context; +use Soap\Encoding\EncoderRegistry; use Soap\Encoding\Exception\EncodingException; use Soap\Encoding\Exception\ExceptionInterface; +use Soap\Encoding\Xml\Writer\ParameterBuilder; use Soap\Encoding\Xml\Writer\SoapEnvelopeWriter; +use Soap\Engine\Metadata\Collection\MethodCollection; +use Soap\Engine\Metadata\Collection\TypeCollection; +use Soap\Engine\Metadata\InMemoryMetadata; +use Soap\Engine\Metadata\Model\MethodMeta; +use Soap\Engine\Metadata\Model\TypeMeta; use Soap\Engine\Metadata\Model\XsdType; +use Soap\WsdlReader\Model\Definitions\BindingStyle; use Soap\WsdlReader\Model\Definitions\BindingUse; use Soap\WsdlReader\Model\Definitions\EncodingStyle; +use Soap\WsdlReader\Model\Definitions\Namespaces; use Soap\WsdlReader\Model\Definitions\SoapVersion; +use ValueError; +use VeeWee\Xml\Exception\RuntimeException as XmlRuntimeException; use function Psl\Option\none; use function Psl\Option\some; use function VeeWee\Xml\Writer\Builder\raw; @@ -53,6 +65,42 @@ static function () { $writer(); } + public function test_it_surfaces_an_element_write_failure_as_the_encoding_exception(): void + { + $context = new Context( + XsdType::guess('string') + ->withXmlTargetNodeName('1st') + ->withMeta(static fn (TypeMeta $meta) => $meta->withIsSimple(true)->withIsElement(true)), + new InMemoryMetadata(new TypeCollection(), new MethodCollection()), + EncoderRegistry::default(), + new Namespaces([], []) + ); + $parameter = new ParameterBuilder( + (new MethodMeta())->withBindingStyle(BindingStyle::RPC->value), + $context, + 'hello' + ); + $writer = new SoapEnvelopeWriter(SoapVersion::SOAP_11, BindingUse::LITERAL, none(), $parameter(...)); + + try { + $writer(); + } catch (EncodingException $exception) { + $message = 'XMLWriter::startElement(): Argument #2 must be a valid element name, "1st" given'; + + $previous = $exception->getPrevious(); + static::assertInstanceOf(XmlRuntimeException::class, $previous); + static::assertSame($message . PHP_EOL, $previous->getMessage()); + + $previous = $previous->getPrevious(); + static::assertInstanceOf(ValueError::class, $previous); + static::assertSame($message, $previous->getMessage()); + + return; + } + + static::fail('Expected an EncodingException.'); + } + public static function provideEnvelopeCases() { yield 'soap-1.1-literal' => [ diff --git a/tests/Unit/Xml/Writer/XsdTypeXmlElementWriterTest.php b/tests/Unit/Xml/Writer/XsdTypeXmlElementWriterTest.php new file mode 100644 index 0000000..fa6e89a --- /dev/null +++ b/tests/Unit/Xml/Writer/XsdTypeXmlElementWriterTest.php @@ -0,0 +1,213 @@ +hello', $actual); + } + + public function test_it_wraps_an_exception_thrown_inside_the_children(): void + { + $original = new LogicException('boom', 5); + + try { + (new XsdTypeXmlElementWriter())( + self::contextFor('a'), + static function () use ($original) { + throw $original; + yield true; + } + ); + } catch (Throwable $exception) { + static::assertInstanceOf(RuntimeException::class, $exception); + static::assertSame('boom' . PHP_EOL, $exception->getMessage()); + static::assertSame(5, $exception->getCode()); + static::assertSame($original, $exception->getPrevious()); + + return; + } + + static::fail('Expected the write to fail.'); + } + + public function test_it_fails_when_the_writer_returns_false(): void + { + try { + (new XsdTypeXmlElementWriter())( + self::contextFor('a'), + static function () { + yield false; + } + ); + } catch (Throwable $exception) { + $message = 'Could not write the provided XML to the stream.'; + static::assertInstanceOf(RuntimeException::class, $exception); + static::assertSame($message . PHP_EOL, $exception->getMessage()); + + $previous = $exception->getPrevious(); + static::assertInstanceOf(RuntimeException::class, $previous); + static::assertSame($message, $previous->getMessage()); + static::assertNull($previous->getPrevious()); + + return; + } + + static::fail('Expected the write to fail.'); + } + + public function test_it_wraps_an_invalid_element_name(): void + { + try { + (new XsdTypeXmlElementWriter())(self::contextFor('1st'), value('hello')); + } catch (Throwable $exception) { + $message = 'XMLWriter::startElement(): Argument #2 must be a valid element name, "1st" given'; + static::assertInstanceOf(RuntimeException::class, $exception); + static::assertSame($message . PHP_EOL, $exception->getMessage()); + + $previous = $exception->getPrevious(); + static::assertInstanceOf(ValueError::class, $previous); + static::assertSame($message, $previous->getMessage()); + + return; + } + + static::fail('Expected the write to fail.'); + } + + public function test_it_fails_on_libxml_issues_raised_while_writing(): void + { + try { + (new XsdTypeXmlElementWriter())( + self::contextFor('a'), + static function () { + (new DOMDocument())->loadXML('getMessage()); + static::assertNull($exception->getPrevious()); + + return; + } + + static::fail('Expected the write to fail.'); + } + + public function test_it_adds_libxml_issues_to_an_exception_thrown_inside_the_children(): void + { + $original = new LogicException('boom'); + + try { + (new XsdTypeXmlElementWriter())( + self::contextFor('a'), + static function () use ($original) { + (new DOMDocument())->loadXML('getMessage()); + static::assertSame($original, $exception->getPrevious()); + + return; + } + + static::fail('Expected the write to fail.'); + } + + public function test_it_wraps_once_per_nested_element_write(): void + { + $original = new LogicException('inner'); + $writer = new XsdTypeXmlElementWriter(); + + try { + $writer( + self::contextFor('a'), + static function (XMLWriter $xmlWriter) use ($writer, $original) { + yield $xmlWriter->writeRaw($writer( + self::contextFor('b'), + static function () use ($original) { + throw $original; + yield true; + } + )); + } + ); + } catch (Throwable $exception) { + static::assertInstanceOf(RuntimeException::class, $exception); + static::assertSame('inner' . PHP_EOL . PHP_EOL, $exception->getMessage()); + + $previous = $exception->getPrevious(); + static::assertInstanceOf(RuntimeException::class, $previous); + static::assertSame('inner' . PHP_EOL, $previous->getMessage()); + static::assertSame($original, $previous->getPrevious()); + + return; + } + + static::fail('Expected the write to fail.'); + } + + public function test_it_clears_libxml_errors_and_restores_the_setting(): void + { + $previousSetting = libxml_use_internal_errors(true); + try { + (new DOMDocument())->loadXML('withXmlTargetNodeName($nodeName)); + } +} From 2791c3334d3e9f92c6e759e0b824237533cff74f Mon Sep 17 00:00:00 2001 From: Toon Verwerft Date: Thu, 15 Oct 2026 20:43:25 +0200 Subject: [PATCH 2/4] Write each element with a plain XMLWriter XsdTypeXmlElementWriter runs once per encoded element and opened a veewee/xml Writer every time. That costs a configure() pipeline, a Psl Result with closures and an IssueCollection from disallow_issues, and a guard call per yielded value, even when nothing goes wrong. It now opens XMLWriter::toMemory() itself and checks for false inline. It keeps the same libxml error handling as disallow_issues and builds the IssueCollection only when there is a failure or a libxml error, so callers get the same exceptions as before. --- src/Xml/Writer/XsdTypeXmlElementWriter.php | 48 +++++++++++++++++++--- 1 file changed, 43 insertions(+), 5 deletions(-) diff --git a/src/Xml/Writer/XsdTypeXmlElementWriter.php b/src/Xml/Writer/XsdTypeXmlElementWriter.php index f2558c5..09fb0a0 100644 --- a/src/Xml/Writer/XsdTypeXmlElementWriter.php +++ b/src/Xml/Writer/XsdTypeXmlElementWriter.php @@ -5,22 +5,60 @@ use Generator; use Soap\Encoding\Encoder\Context; -use VeeWee\Xml\Writer\Writer; +use Throwable; +use VeeWee\Xml\Exception\RuntimeException; use XMLWriter; -use function VeeWee\Xml\Writer\Mapper\memory_output; +use function count; +use function VeeWee\Xml\ErrorHandling\issue_collection_from_xml_errors; final class XsdTypeXmlElementWriter { /** + * This runs once per encoded element, so it drives XMLWriter itself instead of going through VeeWee\Xml\Writer\Writer. + * The error handling matches Writer::write() and disallow_issues(): a throw or a false write is wrapped + * in a RuntimeException together with the libxml issues, and libxml issues on their own fail the write as well. + * * @param callable(XMLWriter): Generator $children * + * @throws RuntimeException + * * @return non-empty-string */ public function __invoke(Context $context, callable $children): string { + $writer = XMLWriter::toMemory(); + $cursor = (new ElementBuilder($context, $children(...)))($writer); + + $previousErrorReporting = libxml_use_internal_errors(true); + libxml_clear_errors(); + + $failure = null; + try { + foreach ($cursor as $written) { + if ($written === false) { + throw RuntimeException::withMessage('Could not write the provided XML to the stream.'); + } + } + } catch (Throwable $exception) { + $failure = $exception; + } + + $errors = libxml_get_errors(); + libxml_clear_errors(); + libxml_use_internal_errors($previousErrorReporting); + + if ($failure !== null) { + throw RuntimeException::combineExceptionWithIssues($failure, issue_collection_from_xml_errors($errors)); + } + + if ($errors) { + $issues = issue_collection_from_xml_errors($errors); + if (count($issues)) { + throw RuntimeException::fromIssues('XML issues detected: ', $issues); + } + } + /** @psalm-var non-empty-string */ - return Writer::inMemory() - ->write(new ElementBuilder($context, $children(...))) - ->map(memory_output()); + return $writer->outputMemory(); } } From 8b424da7243c270e90be85679c5268b0f213082a Mon Sep 17 00:00:00 2001 From: Toon Verwerft Date: Fri, 16 Oct 2026 07:54:19 +0200 Subject: [PATCH 3/4] Add InMemoryXmlWriter for in-memory XML fragments The plain XMLWriter loop with the libxml error handling lived inside XsdTypeXmlElementWriter. It moves into its own writer so the envelope and the fault encoders can use it too, with the same exceptions as veewee/xml Writer::write and disallow_issues. The exception and libxml tests now pin that behaviour on InMemoryXmlWriter, and XsdTypeXmlElementWriter keeps a thin test. --- src/Xml/Writer/InMemoryXmlWriter.php | 62 ++++++ src/Xml/Writer/XsdTypeXmlElementWriter.php | 42 +--- .../Unit/Xml/Writer/InMemoryXmlWriterTest.php | 198 ++++++++++++++++++ .../Writer/XsdTypeXmlElementWriterTest.php | 161 -------------- 4 files changed, 261 insertions(+), 202 deletions(-) create mode 100644 src/Xml/Writer/InMemoryXmlWriter.php create mode 100644 tests/Unit/Xml/Writer/InMemoryXmlWriterTest.php diff --git a/src/Xml/Writer/InMemoryXmlWriter.php b/src/Xml/Writer/InMemoryXmlWriter.php new file mode 100644 index 0000000..8a4f739 --- /dev/null +++ b/src/Xml/Writer/InMemoryXmlWriter.php @@ -0,0 +1,62 @@ + $writer + * + * @throws RuntimeException + * + * @return non-empty-string + */ + public function __invoke(callable $writer): string + { + $xmlWriter = XMLWriter::toMemory(); + + $previousErrorReporting = libxml_use_internal_errors(true); + libxml_clear_errors(); + + $failure = null; + try { + foreach ($writer($xmlWriter) as $written) { + if ($written === false) { + throw RuntimeException::withMessage('Could not write the provided XML to the stream.'); + } + } + } catch (Throwable $exception) { + $failure = $exception; + } + + $errors = libxml_get_errors(); + libxml_clear_errors(); + libxml_use_internal_errors($previousErrorReporting); + + if ($failure !== null) { + throw RuntimeException::combineExceptionWithIssues($failure, issue_collection_from_xml_errors($errors)); + } + + if ($errors) { + $issues = issue_collection_from_xml_errors($errors); + if (count($issues)) { + throw RuntimeException::fromIssues('XML issues detected: ', $issues); + } + } + + /** @psalm-var non-empty-string */ + return $xmlWriter->outputMemory(); + } +} diff --git a/src/Xml/Writer/XsdTypeXmlElementWriter.php b/src/Xml/Writer/XsdTypeXmlElementWriter.php index 09fb0a0..2d23ec6 100644 --- a/src/Xml/Writer/XsdTypeXmlElementWriter.php +++ b/src/Xml/Writer/XsdTypeXmlElementWriter.php @@ -5,19 +5,12 @@ use Generator; use Soap\Encoding\Encoder\Context; -use Throwable; use VeeWee\Xml\Exception\RuntimeException; use XMLWriter; -use function count; -use function VeeWee\Xml\ErrorHandling\issue_collection_from_xml_errors; final class XsdTypeXmlElementWriter { /** - * This runs once per encoded element, so it drives XMLWriter itself instead of going through VeeWee\Xml\Writer\Writer. - * The error handling matches Writer::write() and disallow_issues(): a throw or a false write is wrapped - * in a RuntimeException together with the libxml issues, and libxml issues on their own fail the write as well. - * * @param callable(XMLWriter): Generator $children * * @throws RuntimeException @@ -26,39 +19,6 @@ final class XsdTypeXmlElementWriter */ public function __invoke(Context $context, callable $children): string { - $writer = XMLWriter::toMemory(); - $cursor = (new ElementBuilder($context, $children(...)))($writer); - - $previousErrorReporting = libxml_use_internal_errors(true); - libxml_clear_errors(); - - $failure = null; - try { - foreach ($cursor as $written) { - if ($written === false) { - throw RuntimeException::withMessage('Could not write the provided XML to the stream.'); - } - } - } catch (Throwable $exception) { - $failure = $exception; - } - - $errors = libxml_get_errors(); - libxml_clear_errors(); - libxml_use_internal_errors($previousErrorReporting); - - if ($failure !== null) { - throw RuntimeException::combineExceptionWithIssues($failure, issue_collection_from_xml_errors($errors)); - } - - if ($errors) { - $issues = issue_collection_from_xml_errors($errors); - if (count($issues)) { - throw RuntimeException::fromIssues('XML issues detected: ', $issues); - } - } - - /** @psalm-var non-empty-string */ - return $writer->outputMemory(); + return (new InMemoryXmlWriter())(new ElementBuilder($context, $children(...))); } } diff --git a/tests/Unit/Xml/Writer/InMemoryXmlWriterTest.php b/tests/Unit/Xml/Writer/InMemoryXmlWriterTest.php new file mode 100644 index 0000000..0410bce --- /dev/null +++ b/tests/Unit/Xml/Writer/InMemoryXmlWriterTest.php @@ -0,0 +1,198 @@ +hello', $actual); + } + + public function test_it_wraps_an_exception_thrown_by_the_writer(): void + { + $original = new LogicException('boom', 5); + + try { + (new InMemoryXmlWriter())( + static function () use ($original) { + throw $original; + yield true; + } + ); + } catch (Throwable $exception) { + static::assertInstanceOf(RuntimeException::class, $exception); + static::assertSame('boom' . PHP_EOL, $exception->getMessage()); + static::assertSame(5, $exception->getCode()); + static::assertSame($original, $exception->getPrevious()); + + return; + } + + static::fail('Expected the write to fail.'); + } + + public function test_it_fails_when_the_writer_returns_false(): void + { + try { + (new InMemoryXmlWriter())( + static function () { + yield false; + } + ); + } catch (Throwable $exception) { + $message = 'Could not write the provided XML to the stream.'; + static::assertInstanceOf(RuntimeException::class, $exception); + static::assertSame($message . PHP_EOL, $exception->getMessage()); + + $previous = $exception->getPrevious(); + static::assertInstanceOf(RuntimeException::class, $previous); + static::assertSame($message, $previous->getMessage()); + static::assertNull($previous->getPrevious()); + + return; + } + + static::fail('Expected the write to fail.'); + } + + public function test_it_wraps_an_invalid_element_name(): void + { + try { + (new InMemoryXmlWriter())(element('1st', value('hello'))); + } catch (Throwable $exception) { + $message = 'XMLWriter::startElement(): Argument #2 must be a valid element name, "1st" given'; + static::assertInstanceOf(RuntimeException::class, $exception); + static::assertSame($message . PHP_EOL, $exception->getMessage()); + + $previous = $exception->getPrevious(); + static::assertInstanceOf(ValueError::class, $previous); + static::assertSame($message, $previous->getMessage()); + + return; + } + + static::fail('Expected the write to fail.'); + } + + public function test_it_fails_on_libxml_issues_raised_while_writing(): void + { + try { + (new InMemoryXmlWriter())( + static function () { + (new DOMDocument())->loadXML('getMessage()); + static::assertNull($exception->getPrevious()); + + return; + } + + static::fail('Expected the write to fail.'); + } + + public function test_it_adds_libxml_issues_to_an_exception_thrown_by_the_writer(): void + { + $original = new LogicException('boom'); + + try { + (new InMemoryXmlWriter())( + static function () use ($original) { + (new DOMDocument())->loadXML('getMessage()); + static::assertSame($original, $exception->getPrevious()); + + return; + } + + static::fail('Expected the write to fail.'); + } + + public function test_it_wraps_once_per_nested_write(): void + { + $original = new LogicException('inner'); + $writer = new InMemoryXmlWriter(); + + try { + $writer( + static function (XMLWriter $xmlWriter) use ($writer, $original) { + yield $xmlWriter->writeRaw($writer( + static function () use ($original) { + throw $original; + yield true; + } + )); + } + ); + } catch (Throwable $exception) { + static::assertInstanceOf(RuntimeException::class, $exception); + static::assertSame('inner' . PHP_EOL . PHP_EOL, $exception->getMessage()); + + $previous = $exception->getPrevious(); + static::assertInstanceOf(RuntimeException::class, $previous); + static::assertSame('inner' . PHP_EOL, $previous->getMessage()); + static::assertSame($original, $previous->getPrevious()); + + return; + } + + static::fail('Expected the write to fail.'); + } + + public function test_it_clears_libxml_errors_and_restores_the_setting(): void + { + $previousSetting = libxml_use_internal_errors(true); + try { + (new DOMDocument())->loadXML('hello', $actual); } - public function test_it_wraps_an_exception_thrown_inside_the_children(): void - { - $original = new LogicException('boom', 5); - - try { - (new XsdTypeXmlElementWriter())( - self::contextFor('a'), - static function () use ($original) { - throw $original; - yield true; - } - ); - } catch (Throwable $exception) { - static::assertInstanceOf(RuntimeException::class, $exception); - static::assertSame('boom' . PHP_EOL, $exception->getMessage()); - static::assertSame(5, $exception->getCode()); - static::assertSame($original, $exception->getPrevious()); - - return; - } - - static::fail('Expected the write to fail.'); - } - - public function test_it_fails_when_the_writer_returns_false(): void - { - try { - (new XsdTypeXmlElementWriter())( - self::contextFor('a'), - static function () { - yield false; - } - ); - } catch (Throwable $exception) { - $message = 'Could not write the provided XML to the stream.'; - static::assertInstanceOf(RuntimeException::class, $exception); - static::assertSame($message . PHP_EOL, $exception->getMessage()); - - $previous = $exception->getPrevious(); - static::assertInstanceOf(RuntimeException::class, $previous); - static::assertSame($message, $previous->getMessage()); - static::assertNull($previous->getPrevious()); - - return; - } - - static::fail('Expected the write to fail.'); - } - public function test_it_wraps_an_invalid_element_name(): void { try { @@ -99,113 +45,6 @@ public function test_it_wraps_an_invalid_element_name(): void static::fail('Expected the write to fail.'); } - public function test_it_fails_on_libxml_issues_raised_while_writing(): void - { - try { - (new XsdTypeXmlElementWriter())( - self::contextFor('a'), - static function () { - (new DOMDocument())->loadXML('getMessage()); - static::assertNull($exception->getPrevious()); - - return; - } - - static::fail('Expected the write to fail.'); - } - - public function test_it_adds_libxml_issues_to_an_exception_thrown_inside_the_children(): void - { - $original = new LogicException('boom'); - - try { - (new XsdTypeXmlElementWriter())( - self::contextFor('a'), - static function () use ($original) { - (new DOMDocument())->loadXML('getMessage()); - static::assertSame($original, $exception->getPrevious()); - - return; - } - - static::fail('Expected the write to fail.'); - } - - public function test_it_wraps_once_per_nested_element_write(): void - { - $original = new LogicException('inner'); - $writer = new XsdTypeXmlElementWriter(); - - try { - $writer( - self::contextFor('a'), - static function (XMLWriter $xmlWriter) use ($writer, $original) { - yield $xmlWriter->writeRaw($writer( - self::contextFor('b'), - static function () use ($original) { - throw $original; - yield true; - } - )); - } - ); - } catch (Throwable $exception) { - static::assertInstanceOf(RuntimeException::class, $exception); - static::assertSame('inner' . PHP_EOL . PHP_EOL, $exception->getMessage()); - - $previous = $exception->getPrevious(); - static::assertInstanceOf(RuntimeException::class, $previous); - static::assertSame('inner' . PHP_EOL, $previous->getMessage()); - static::assertSame($original, $previous->getPrevious()); - - return; - } - - static::fail('Expected the write to fail.'); - } - - public function test_it_clears_libxml_errors_and_restores_the_setting(): void - { - $previousSetting = libxml_use_internal_errors(true); - try { - (new DOMDocument())->loadXML('withXmlTargetNodeName($nodeName)); From e4c959916406aab2abea6e773617b14ae87f86df Mon Sep 17 00:00:00 2001 From: Toon Verwerft Date: Fri, 16 Oct 2026 07:56:13 +0200 Subject: [PATCH 4/4] Write the envelope and faults through InMemoryXmlWriter The envelope and both fault encoders built their XML through veewee/xml Writer::inMemory with the same error contract InMemoryXmlWriter keeps, so they now share it with the element writer. New tests pin the exact bytes of an envelope and of both faults; they passed against the previous writer as well. The envelope still unwraps an EncodingException from the RuntimeException exactly as before. --- src/Fault/Encoder/Soap11FaultEncoder.php | 12 ++-- src/Fault/Encoder/Soap12FaultEncoder.php | 12 ++-- src/Xml/Writer/SoapEnvelopeWriter.php | 66 +++++++++---------- .../Fault/Encoder/Soap11FaultEncoderTest.php | 15 +++++ .../Fault/Encoder/Soap12FaultEncoderTest.php | 17 +++++ .../Xml/Writer/SoapEnvelopeWriterTest.php | 18 +++++ 6 files changed, 91 insertions(+), 49 deletions(-) diff --git a/src/Fault/Encoder/Soap11FaultEncoder.php b/src/Fault/Encoder/Soap11FaultEncoder.php index a857155..c7f3689 100644 --- a/src/Fault/Encoder/Soap11FaultEncoder.php +++ b/src/Fault/Encoder/Soap11FaultEncoder.php @@ -5,10 +5,10 @@ use Soap\Encoding\Fault\Soap11Fault; use Soap\Encoding\Restriction\WhitespaceRestriction; +use Soap\Encoding\Xml\Writer\InMemoryXmlWriter; use Soap\Xml\Xmlns; use VeeWee\Reflecta\Iso\Iso; use VeeWee\Xml\Dom\Document; -use VeeWee\Xml\Writer\Writer; use function Psl\invariant; use function VeeWee\Xml\Dom\Xpath\Configurator\namespaces; use function VeeWee\Xml\Writer\Builder\children; @@ -16,7 +16,6 @@ use function VeeWee\Xml\Writer\Builder\namespaced_element; use function VeeWee\Xml\Writer\Builder\raw; use function VeeWee\Xml\Writer\Builder\value; -use function VeeWee\Xml\Writer\Mapper\memory_output; /** * @implements SoapFaultEncoder @@ -41,9 +40,8 @@ private function to(Soap11Fault $fault): string { $envNamespace = Xmlns::soap11Envelope()->value(); - /** @var non-empty-string */ - return Writer::inMemory() - ->write(children([ + return (new InMemoryXmlWriter())( + children([ namespaced_element( $envNamespace, 'env', @@ -70,8 +68,8 @@ private function to(Soap11Fault $fault): string ...($fault->detail !== null ? [raw($fault->detail)] : []), ]) ) - ])) - ->map(memory_output()); + ]) + ); } /** diff --git a/src/Fault/Encoder/Soap12FaultEncoder.php b/src/Fault/Encoder/Soap12FaultEncoder.php index 36b02af..30d5a24 100644 --- a/src/Fault/Encoder/Soap12FaultEncoder.php +++ b/src/Fault/Encoder/Soap12FaultEncoder.php @@ -5,9 +5,9 @@ use Soap\Encoding\Fault\Soap12Fault; use Soap\Encoding\Restriction\WhitespaceRestriction; +use Soap\Encoding\Xml\Writer\InMemoryXmlWriter; use VeeWee\Reflecta\Iso\Iso; use VeeWee\Xml\Dom\Document; -use VeeWee\Xml\Writer\Writer; use function Psl\invariant; use function VeeWee\Xml\Dom\Xpath\Configurator\namespaces; use function VeeWee\Xml\Writer\Builder\children; @@ -15,7 +15,6 @@ use function VeeWee\Xml\Writer\Builder\prefixed_element; use function VeeWee\Xml\Writer\Builder\raw; use function VeeWee\Xml\Writer\Builder\value; -use function VeeWee\Xml\Writer\Mapper\memory_output; /** * @implements SoapFaultEncoder @@ -40,9 +39,8 @@ public function iso(): Iso */ private function to(Soap12Fault $fault): string { - /** @var non-empty-string */ - return Writer::inMemory() - ->write(children([ + return (new InMemoryXmlWriter())( + children([ namespaced_element( self::ENV_NAMESPACE, 'env', @@ -113,8 +111,8 @@ private function to(Soap12Fault $fault): string ...($fault->detail !== null ? [raw($fault->detail)] : []), ]) ) - ])) - ->map(memory_output()); + ]) + ); } /** diff --git a/src/Xml/Writer/SoapEnvelopeWriter.php b/src/Xml/Writer/SoapEnvelopeWriter.php index 1622199..8d6b4e9 100644 --- a/src/Xml/Writer/SoapEnvelopeWriter.php +++ b/src/Xml/Writer/SoapEnvelopeWriter.php @@ -12,14 +12,12 @@ use Soap\WsdlReader\Model\Definitions\SoapVersion; use Soap\Xml\Xmlns; use VeeWee\Xml\Exception\RuntimeException as XmlRuntimeException; -use VeeWee\Xml\Writer\Writer; use XMLWriter; use function Psl\Vec\filter_nulls; use function VeeWee\Xml\Writer\Builder\children; use function VeeWee\Xml\Writer\Builder\namespace_attribute; use function VeeWee\Xml\Writer\Builder\namespaced_attribute; use function VeeWee\Xml\Writer\Builder\namespaced_element; -use function VeeWee\Xml\Writer\Mapper\memory_output; final class SoapEnvelopeWriter { @@ -45,45 +43,43 @@ public function __invoke(): string }; try { - return Writer::inMemory() - ->write( + return (new InMemoryXmlWriter())( + namespaced_element( + $envelopeNamespace, + 'SOAP-ENV', + 'Envelope', namespaced_element( $envelopeNamespace, 'SOAP-ENV', - 'Envelope', - namespaced_element( - $envelopeNamespace, - 'SOAP-ENV', - 'Body', - children( - filter_nulls([ - // In SOAP 1.2 the position of the encoding attributes is limited: - // See: https://www.w3.org/TR/soap12-part1/#soapencattr - // For SOAP 1.1 it can be everywhere: - // See: https://www.w3.org/TR/2000/NOTE-SOAP-20000508/#_Toc478383495 - $this->encodingStyle - ->filter(fn (): bool => $this->bindingUse === BindingUse::ENCODED) - ->map( - static fn (EncodingStyle $encodingStyle) => children([ - namespace_attribute( - $encodingStyle->value, - 'SOAP-ENC' - ), - namespaced_attribute( - $envelopeNamespace, - 'SOAP-ENV', - 'encodingStyle', - $encodingStyle->value - ) - ]) - )->unwrapOr(null), - $this->children - ]) - ) + 'Body', + children( + filter_nulls([ + // In SOAP 1.2 the position of the encoding attributes is limited: + // See: https://www.w3.org/TR/soap12-part1/#soapencattr + // For SOAP 1.1 it can be everywhere: + // See: https://www.w3.org/TR/2000/NOTE-SOAP-20000508/#_Toc478383495 + $this->encodingStyle + ->filter(fn (): bool => $this->bindingUse === BindingUse::ENCODED) + ->map( + static fn (EncodingStyle $encodingStyle) => children([ + namespace_attribute( + $encodingStyle->value, + 'SOAP-ENC' + ), + namespaced_attribute( + $envelopeNamespace, + 'SOAP-ENV', + 'encodingStyle', + $encodingStyle->value + ) + ]) + )->unwrapOr(null), + $this->children + ]) ) ) ) - ->map(memory_output()); + ); } catch (XmlRuntimeException $e) { throw ($e->getPrevious() instanceof EncodingExceptionInterface) ? $e->getPrevious() : $e; } diff --git a/tests/Unit/Fault/Encoder/Soap11FaultEncoderTest.php b/tests/Unit/Fault/Encoder/Soap11FaultEncoderTest.php index 74d5a5a..d817029 100644 --- a/tests/Unit/Fault/Encoder/Soap11FaultEncoderTest.php +++ b/tests/Unit/Fault/Encoder/Soap11FaultEncoderTest.php @@ -13,6 +13,21 @@ #[CoversClass(Soap11Fault::class)] final class Soap11FaultEncoderTest extends AbstractFaultEncoderTests { + public function test_it_writes_the_exact_fault_bytes(): void + { + $actual = (new Soap11FaultEncoder())->iso()->to(new Soap11Fault( + faultCode: 'env:Server', + faultString: 'a < b', + faultActor: 'uri:actor', + detail: 'value', + )); + + static::assertSame( + 'env:Servera < buri:actorvalue', + $actual + ); + } + public static function provideIsomorphicCases(): iterable { $baseConfig = [ diff --git a/tests/Unit/Fault/Encoder/Soap12FaultEncoderTest.php b/tests/Unit/Fault/Encoder/Soap12FaultEncoderTest.php index 5265134..72a4ea4 100644 --- a/tests/Unit/Fault/Encoder/Soap12FaultEncoderTest.php +++ b/tests/Unit/Fault/Encoder/Soap12FaultEncoderTest.php @@ -13,6 +13,23 @@ #[CoversClass(Soap12Fault::class)] final class Soap12FaultEncoderTest extends AbstractFaultEncoderTests { + public function test_it_writes_the_exact_fault_bytes(): void + { + $actual = (new Soap12FaultEncoder())->iso()->to(new Soap12Fault( + code: 'env:Sender', + reason: 'a < b', + subCode: 'm:Invalid', + node: 'uri:node', + role: 'uri:role', + detail: 'value', + )); + + static::assertSame( + 'env:Senderm:Invalida < buri:nodeuri:rolevalue', + $actual + ); + } + public static function provideIsomorphicCases(): iterable { $baseConfig = [ diff --git a/tests/Unit/Xml/Writer/SoapEnvelopeWriterTest.php b/tests/Unit/Xml/Writer/SoapEnvelopeWriterTest.php index 4774a25..1cfe8eb 100644 --- a/tests/Unit/Xml/Writer/SoapEnvelopeWriterTest.php +++ b/tests/Unit/Xml/Writer/SoapEnvelopeWriterTest.php @@ -49,6 +49,24 @@ public function test_it_can_write_a_soap_envelope( static::assertXmlStringEqualsXmlString($expected, $actual); } + public function test_it_writes_the_exact_envelope_bytes(): void + { + $writer = new SoapEnvelopeWriter( + SoapVersion::SOAP_11, + BindingUse::ENCODED, + some(EncodingStyle::SOAP_11), + raw('content') + ); + + static::assertSame( + '' + . '' + . 'content' + . '', + $writer() + ); + } + public function test_it_can_fail_writing_with_encoding_exception(): void { $this->expectException(ExceptionInterface::class);