Skip to content

Write each element with a plain XMLWriter - #86

Merged
veewee merged 4 commits into
php-soap:mainfrom
veewee:perf-plain-xmlwriter-per-element
Oct 16, 2026
Merged

veewee merged 4 commits into
php-soap:mainfrom
veewee:perf-plain-xmlwriter-per-element

Conversation

@veewee

@veewee veewee commented Oct 15, 2026 •

Copy link
Copy Markdown
Member
Q A
Type improvement
BC Break no
Fixed issues

Summary

XsdTypeXmlElementWriter runs once for every encoded element and opened a new Writer::inMemory() each time. Most of that cost is in the veewee/xml wrapper rather than in XMLWriter. configure() folds an empty pipeline through Iter\reduce, disallow_issues builds a Psl Result, two closures and an IssueCollection even when nothing went wrong, and write() calls a guard function for every yielded value. The 3MB dense encode goes through this about 157k times per call.

This PR adds Soap\Encoding\Xml\Writer\InMemoryXmlWriter, an in-memory XML writer for this package. It takes a callable(XMLWriter): Generator<bool>, opens a fresh XMLWriter::toMemory() per call, runs the builder with an inline false check and returns outputMemory(). It holds no state between calls. XsdTypeXmlElementWriter now delegates to it, and so do SoapEnvelopeWriter, Soap11FaultEncoder and Soap12FaultEncoder, which were the other places building XML through Writer::inMemory(). The builders and the XmlEncoder contract stay as they are.

Same exceptions

Callers can see the old wrapping. Anything thrown inside a builder comes out as a veewee RuntimeException with the original as previous, wrapped once more for each element level it passes through. Libxml errors raised during the write fail it as well. SoapEnvelopeWriter only unwraps one level, so an encoder that relies on this would notice a change. InMemoryXmlWriter replicates Writer::write with disallow_issues without the overhead: the same libxml_use_internal_errors and libxml_clear_errors sequence, the same RuntimeException factories and messages, and an IssueCollection that is only built when there is a failure or a libxml error.

The first commit adds tests for this that pass on main before the writer changes. They now pin that behaviour on InMemoryXmlWriter directly: a throw inside the builder, a false write, an invalid element name, libxml issues with and without a throw, nested writes, and the libxml error state afterwards. XsdTypeXmlElementWriter keeps a thin test, SoapEnvelopeWriterTest still covers an element failure seen from the envelope, and the envelope and both fault encoders have a test that pins their exact output bytes, which passed against the old writer too. The encoded requests and decoded results of the 3MB workloads have the same sha1 before and after.

Speed

Median of 3 interleaved rounds on the same machine, with ext-soap as the noise reference.

workload main this PR
3MB encode sparse, stdClass 1.432 s 1.176 s (-17.9%)
3MB encode sparse, classmap 1.579 s 1.326 s (-16.1%)
3MB encode dense, stdClass 0.718 s 0.512 s (-28.6%)
3MB encode dense, classmap 0.726 s 0.539 s (-25.8%)
EncodeBench literal x500 32.30 ms 25.13 ms (-22.2%)
EncodeBench encoded x500 54.79 ms 45.08 ms (-17.7%)
EncodeSparseBench sparse x500 31.64 ms 23.33 ms (-26.3%)
ext-soap encode literal x500 3.73 ms 3.62 ms (-2.9%)

These numbers were measured before the writer moved into InMemoryXmlWriter. A 3MB encode round after the move is within noise of them.

Decode does not go through this writer and stays within 2%.

Summary by CodeRabbit

  • Bug Fixes
    • Improved error reporting when XML generation fails, including invalid XML and writer errors.
    • Preserved SOAP fault and envelope output while tightening checks for serialization issues.
  • Tests
    • Added coverage for exact SOAP XML output, escaping, and failure handling.

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.
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.
@coderabbitai

coderabbitai Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 0eabf7e6-5ef7-4ee1-bf12-faec0bc3af51
📥 Commits

Reviewing files that changed from the base of the PR and between 403a479 and e4c9599.

📒 Files selected for processing: 10
  • src/Fault/Encoder/Soap11FaultEncoder.php
  • src/Fault/Encoder/Soap12FaultEncoder.php
  • src/Xml/Writer/InMemoryXmlWriter.php
  • src/Xml/Writer/SoapEnvelopeWriter.php
  • src/Xml/Writer/XsdTypeXmlElementWriter.php
  • tests/Unit/Fault/Encoder/Soap11FaultEncoderTest.php
  • tests/Unit/Fault/Encoder/Soap12FaultEncoderTest.php
  • tests/Unit/Xml/Writer/InMemoryXmlWriterTest.php
  • tests/Unit/Xml/Writer/SoapEnvelopeWriterTest.php
  • tests/Unit/Xml/Writer/XsdTypeXmlElementWriterTest.php

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.

📝 Walkthrough

The change adds InMemoryXmlWriter to generate XML and report writer or libxml errors. SOAP fault encoders, the SOAP envelope writer, and the XSD type element writer now use it. Tests cover output bytes and error handling.

Changes: XML serialization

Layer / File(s) Summary
In-memory XML writing and error handling
src/Xml/Writer/InMemoryXmlWriter.php, tests/Unit/Xml/Writer/InMemoryXmlWriterTest.php
InMemoryXmlWriter returns generated XML and reports callback failures, false write results, and libxml issues. Tests cover exception chaining and restoration of libxml error settings.
Envelope and XSD writer integrations
src/Xml/Writer/SoapEnvelopeWriter.php, src/Xml/Writer/XsdTypeXmlElementWriter.php, tests/Unit/Xml/Writer/SoapEnvelopeWriterTest.php, tests/Unit/Xml/Writer/XsdTypeXmlElementWriterTest.php
Both writers use InMemoryXmlWriter. Tests check serialized output and invalid element-name exception chains.
SOAP fault encoder integrations
src/Fault/Encoder/Soap11FaultEncoder.php, src/Fault/Encoder/Soap12FaultEncoder.php, tests/Unit/Fault/Encoder/Soap11FaultEncoderTest.php, tests/Unit/Fault/Encoder/Soap12FaultEncoderTest.php
Both fault encoders use InMemoryXmlWriter. Tests assert exact serialized fault output.

Priority: ➖ Normal

Estimated code review effort: 3 (Moderate) | ~25 minutes

Change: Refactor

Merge Risk: ⚪ Minimal · up to e4c95

No actionable merge-blocking issue is established; the change is ready for normal checks.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 15.38% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 26 functions across 10 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title clearly describes the main change: replacing the previous writer setup with a plain XMLWriter-based implementation across XML elements, envelopes, and faults.
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
  • Fix all pre-merge checks with AI
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

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.
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.
@veewee
veewee merged commit e4cc660 into php-soap:main Oct 16, 2026
13 checks passed
@veewee
veewee deleted the perf-plain-xmlwriter-per-element branch October 16, 2026 08:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant