I Got Tired of Wrestling with XML in PHP, So I Built a Package
Every developer has that one thing that makes them visibly annoyed. For some it's CSS. For others it's JavaScript's type coercion. For me, it's XML.
Not XML as a concept — I get it, it has its place, it's been around forever, and a lot of enterprise systems still speak it fluently. My problem is with what happens when you actually have to work with it in PHP. You load up DOMDocument, you start navigating nodes, you check for null everywhere, you forget whether something is an attribute or a child element, and before you know it you've written fifty lines of code just to extract a single value buried three levels deep in a SOAP envelope.
I got tired of it. So I built dmb/xml-converter .
The Problem with XML in PHP
The standard tooling in PHP for XML — DOMDocument, SimpleXML — is perfectly functional. But it forces you to think in XML's terms rather than your own. You're navigating a tree of nodes, checking types, handling namespaced tags, and the moment you want to do anything collection-like (filter a bunch of sibling elements, pluck a specific attribute from each) you're basically writing it yourself.
What I wanted was something that let me parse XML into a predictable PHP array structure, navigate that structure with a clean API, and go the other way too — build XML from an array when I need to send it somewhere.
Three things. That's it.
Installing the Package
composer require dmb/xml-converter
Requires PHP 8.2 or higher. It also auto-registers with Laravel if you're using it there — no need to touch config/app.php.
The Array Structure
Before diving into the classes, it helps to understand the normalized array format the package uses. Everything is consistent:
- _attributes holds XML attributes
- _value holds text content
- _children holds ordered/repeated child nodes
So this XML:
<ParentTag Version="1.0"> <Success Version="1.4"><Foo/></Success> </ParentTag>
Becomes this array:
[
'ParentTag' => [
'_attributes' => [
'Version' => '1.0',
],
'_children' => [
[
'Success' => [
'_attributes' => ['Version' => '1.4'],
'_children' => [
['Foo' => []],
],
],
],
],
],
]
It's explicit, predictable, and easy to reason about. Once you know the shape, you always know where to look.
1) XML to Array
The FromXml class handles parsing. Pass it an XML string, get back the normalized array.
use Dmb\XmlConverter\FromXml; use Dmb\XmlConverter\XmlParsingException; $xml = <<<'XML' <SOAP-ENV:Envelope Version="1.0"> <SOAP-ENV:Body> <ParentTag Version="1.0" Target="Test"> <Success Version="1.4"><Foo/></Success> </ParentTag> </SOAP-ENV:Body> </SOAP-ENV:Envelope> XML; try { $array = FromXml::make()->convertToArray($xml); } catch (XmlParsingException $e) { // invalid XML — handle gracefully $error = $e->getMessage(); }
If the XML is invalid, you get a XmlParsingException instead of a cryptic PHP warning. Small thing, but it matters when you're integrating with third-party APIs that occasionally send garbage.
2) Array to XML
FromArray goes the other direction. You give it the normalized array structure, it gives you XML back. This is useful when you're building payloads to send to a SOAP service or any other XML-based API.
use Dmb\XmlConverter\FromArray; $payload = [ 'header' => [ 'version' => [ '_attributes' => ['port' => '0000', 'host' => 'host'], '_value' => '1.0.0', ], 'timestamp' => '20230116170354', ], 'response' => [ '_attributes' => ['type' => 'type', 'product' => 'item'], '_children' => [ ['search' => ['_attributes' => ['number' => '123', 'time' => '0.00']]], ['nights' => ['_attributes' => ['number' => '11']]], ], ], ]; // Default root element $xml = FromArray::make()->convertToXml($payload); // Custom root element $xml = FromArray::make()->convertToXml($payload, 'envelope'); // Custom root with attributes $xml = FromArray::make()->convertToXml( $payload, [ 'rootElementName' => 'envelope', '_attributes' => [ 'xmlns' => 'https://github.com/davidemariabusi/xml-converter', ], ] );
Under the hood it uses spatie/array-to-xml to do the heavy lifting, with a transformation layer that maps the _children structure into what Spatie expects. Credit where it's due.
3) The Fluent API — The Part I'm Most Proud Of
This is the piece I really wanted to get right. Once you've parsed an XML response into an array, navigating it can still be tedious. The Fluent class wraps the array and gives you a chainable, safe API for getting to where you need to go.
use Dmb\XmlConverter\Fluent; use Dmb\XmlConverter\FluentException; $data = [/* your parsed XML array */]; try { $parentTag = Fluent::make($data) ->getRoot('SOAP-ENV:Envelope') ->getChild('SOAP-ENV:Body') ->getChild('ParentTag'); $target = $parentTag->getAttribute('Target'); // "Test" } catch (FluentException $e) { $error = $e->getMessage(); }
Every step in the chain is safe. If a node doesn't exist, you get a FluentException with a readable path telling you exactly where navigation failed — something like Root element "SOAP-ENV:Envelope" not found [path: $]. No more wondering which level blew up.
Collection-style helpers
The part that really removes the tedium: when you call getChildren(), you get back a Fluent instance wrapping the children list, and you can run collection-style operations on it.
$versions = $parentTag ->getChildren() ->filter(fn (Fluent $item, int|string $key, ?string $tag): bool => $tag === 'Success') ->pluck('Version'); // ['1.4', '1.5']
The callbacks in filter, map, each, and first receive three things: the Fluent item itself, its key, and its tag name. The tag name is particularly useful when you have a _children array containing elements of different types and you want to process only certain ones.
Positional access
When you know what position you want, there are named methods for that:
$children->first(); $children->second(); $children->last(); $children->at(0); // zero-based $children->nth(3); // one-based
Named ordinals go up to tenth(). Past that, nth() takes over. It's a small quality-of-life thing, but when you're reading code six months later, ->second() is a lot clearer than ->at(1).
Full method list
Navigation: getRoot(), getChild(), getChildren(), getAttributes()
Value/metadata: getValue(), toString(), hasChildren(), hasAttributes(), hasValue()
Collections: each(), map(), filter(), first(), last(), at(), nth(), second() ... tenth()
Query helpers: pluck(), contains(), count(), toArray(), isEmpty(), isNotEmpty()
The Path Tracking
One detail I want to call out because it quietly saves a lot of debugging time: every Fluent instance tracks its path through the data structure. When something goes wrong, the exception message tells you exactly where:
Child element "Success" not found [path: $ > SOAP-ENV:Envelope > SOAP-ENV:Body > ParentTag]
That [path: ...] part isn't just cosmetic. When you're working with deeply nested SOAP responses and something is missing or misnamed, knowing exactly where navigation failed is the difference between a two-minute fix and a twenty-minute debugging session.
When Should You Use This?
If you're hitting an XML-based API — SOAP services, legacy enterprise integrations, payment gateways that haven't updated their interface since 2008 — this package takes a lot of the grunt work away.
It's not going to replace a full SOAP client if you need WSDL support or proper envelope building from scratch. But if you're working with raw XML strings and you just want to parse them, navigate them, and potentially rebuild them, it's a clean solution with no magic hiding anywhere.
The array structure is explicit enough that you could bypass Fluent entirely and work with the raw array if you prefer. But why would you want to?
Source
The package is on Packagist as dmb/xml-converter and the source is on GitHub . Issues and PRs welcome.