php-soap/xml
php-soap/xml is a lightweight XML library for PHP focused on SOAP. It offers namespace-aware parsing and generation so you can build, inspect, and transform SOAP envelopes and XML payloads through a small, predictable API. Designed for dependable XML handling in PHP applications.
Install with Composer — composer require php-soap/xml. It has no hard dependency on a SOAP engine, so it can be pulled into any project that touches SOAP payloads, even ones that talk to remote endpoints via PSR-18/Guzzle rather than ext-soap.
First place to look is the XmlDocument class. That is the entry point: it extends DOMDocument, so everything you already know about DOM is available, and the package layers a small set of ergonomic helpers on top (xpath(), element creation, namespace-aware queries, string serialization). From there, Element, Node, and QName are the value objects you will pass around.
A realistic first use case: parsing a SOAP response you already have as a string.
use PhpSoap\Xml\XmlDocument;
$doc = XmlDocument::fromString($responseBody);
$body = $doc->xpath('//soap:Body/*')->first();
if ($body instanceof \PhpSoap\Xml\Element) {
$orderId = $body->xpath('./ns:OrderId/text()')->first()?->value();
}
If you are already inside the php-soap ecosystem (engine, wsdl, client-common), you rarely construct documents yourself — you receive an XmlDocument from the transport layer and read from it. Start by tracing where the response object is handed to your middleware/handler and confirm it is an XmlDocument, not a raw string.
Treat XmlDocument as an enhanced DOMDocument, not a replacement. The idiomatic pattern is to use the package helpers for construction and querying, but drop down to native DOM methods (importNode, appendChild, nodeValue, attributes) whenever the wrapper does not expose a shortcut. Mixing the two is expected and supported.
Namespace handling is the core value proposition. SOAP envelopes are namespace soup, and prefix-based XPath is fragile. Register your known namespaces once on the document and query with prefixes you control:
$doc->registerXPathNamespace('soap', 'http://schemas.xmlsoap.org/soap/envelope/');
$doc->registerXPathNamespace('ns', 'urn:example:orders');
This is the pattern you will repeat in nearly every handler. Because SOAP servers are free to choose arbitrary prefixes (or none), always match on the namespace URI, never on the literal prefix in the wire format.
Building requests: compose, don't concatenate. Use createElement/Element::create plus appendChild for the envelope, headers, and body. This gives you automatic escaping of text nodes and correct namespace declarations, which is the main correctness win over string templates:
$doc = XmlDocument::create();
$envelope = $doc->createElement('soap:Envelope', null, 'http://schemas.xmlsoap.org/soap/envelope/');
$body = $doc->createElement('soap:Body', null, 'http://schemas.xmlsoap.org/soap/envelope/');
$envelope->appendChild($body);
// ... build the operation payload under $body
$payload = $doc->toString();
Integration with the rest of php-soap. php-soap/xml is a foundation package: php-soap/engine (ext-soap and ext-soap-replacement drivers), php-soap/psr18-transport, and php-soap/wsdl all speak this type. If you are writing custom middleware — logging, signing, header injection, MTOM — operate on the XmlDocument before it is serialized to the transport. A common pattern is a middleware that clones the envelope, injects a WS-Security header element, and hands it back.
Testing pattern. Build a fixture XmlDocument from a real captured response and assert against XPath expressions in your tests. This decouples your tests from the live endpoint and from prefix naming. Keep a fixture per SOAP operation.
Extraction helper. Write a small private method per service — e.g. orderFromResponse(XmlDocument $doc): Order — that performs all XPath queries in one place. This keeps fragile XML paths centralized and makes schema changes a single-file diff.
DOMDocument whitespace matters for text nodes. Pretty-printed SOAP responses put newlines inside elements. Use trim() on extracted text or query with normalize-space(), or you will ship whitespace-padded values.xpath() returns a list, not a node. ->first() is your friend; guard against null. Do not assume exactly one match — SOAP arrays are a frequent source of duplicates (same element repeated vs. wrapper element with item children — both conventions exist and you must handle whichever your service uses).xmlns:* placements may differ from what you expect. Compare via XPath, not string equality.DOMDocument::$formatOutput changes output. It is off by default; enable it for logs/debugging but disable it (or be aware of it) if you are hashing or signing payloads, since whitespace alters signatures.http://schemas.xmlsoap.org/soap/envelope/ vs http://www.w3.org/2003/05/soap-envelope). The package does not pick for you; pick based on the WSDL soap:binding transport URI.loadXML emits warnings on malformed input. Wrap parsing in a try/catch and use libxml_use_internal_errors(true) so a bad gateway HTML error page does not spill warnings into your logs; convert to a domain exception instead.XmlDocument (for a service-specific document with typed accessors) or writing middleware in the php-soap engine. There is no plugin/event system.composer.json and read the changelog before upgrading. Because it extends DOMDocument, a fallback to plain DOM is always one delete away, which lowers the risk considerably — the API surface you actually depend on is usually a handful of methods.$doc->saveXML() and the registered namespaces together. Nine times out of ten the namespace URI in the document differs from the one you registered (trailing slash, urn: prefix, version suffix).How can I help you explore Laravel packages today?