zendframework/zend-xml2json
Converts XML to JSON using PHP's DOM and JSON extensions, letting you transform XML documents or strings into JSON output ready for APIs, storage, or JavaScript consumption.
zendframework/zend-xml2json is a tiny adapter around simplexml_load_string() plus a JSON encoder. It exposes essentially one useful entry point:
use Zend\Xml2Json\Xml2Json;
$json = Xml2Json::fromXml($xmlString); // attributes preserved
$json = Xml2Json::fromXml($xmlString, true); // attributes ignored
Minimal setup:
composer require zendframework/zend-xml2json
Requirements: PHP with ext-simplexml (and ext-libxml) enabled. There is no service provider, no config file, and no facade — it is a stateless static helper, so it drops into a Laravel service or controller without wiring.
Where to look first:
src/Xml2Json.php for the public API and error behavior.laminas/laminas-xml2json (which is itself worth checking for current maintenance status before adopting).First realistic use case in a Laravel app: accepting a legacy XML payload (SOAP-ish partner feed, payment callback, shipping manifest) at an endpoint and returning JSON to a modern frontend or downstream API.
Route::post('/webhooks/legacy', function (Request $request) {
$json = Xml2Json::fromXml($request->getContent());
return response($json, 200, ['Content-Type' => 'application/json']);
});
Note the response is returned as raw JSON, not response()->json(json_decode(...)), to avoid decoding and re-encoding the payload.
Thin service wrapper. Hide the static call behind a small class so your app can swap the implementation later (important given the package is archived):
final class XmlToJsonConverter
{
public function convert(string $xml, bool $ignoreAttributes = false): array
{
return json_decode(
Xml2Json::fromXml($xml, $ignoreAttributes),
true,
512,
JSON_THROW_ON_ERROR
);
}
}
Normalize the output shape before it hits your domain. The raw structure uses @attributes and @text conventions and is unpleasant to consume. Add a recursive normalizer that flattens @attributes into prefixed keys and unwraps @text:
private function flatten(array $node): array
{
$out = [];
foreach ($node as $key => $value) {
if ($key === '@attributes' && is_array($value)) {
foreach ($value as $attr => $v) {
$out['attr_' . $attr] = $v;
}
} elseif ($key === '@text') {
$out['value'] = $value;
} elseif (is_array($value)) {
$out[$key] = $this->flatten($value);
} else {
$out[$key] = $value;
}
}
return $out;
}
Middleware for XML request bodies. Laravel does not parse XML into the request bag. Add a middleware that converts XML bodies once and merges the result, so controllers work with $request->input(...):
if (str_contains($request->header('Content-Type', ''), 'xml')) {
$converted = $converter->convert($request->getContent());
$request->merge($converted);
}
Only do this on routes that expect XML — mutating every request is a debugging trap.
Queue large documents. Conversion is in-memory and synchronous. For multi-megabyte feeds, accept the upload, store it on a disk, dispatch a job that reads the file and converts it, then persist the JSON to a json column or dispatch a further job. Never do this inline in a web request.
Testing. Keep XML fixtures under tests/Fixtures/*.xml and assert against decoded arrays, not raw JSON strings (key ordering and whitespace make string assertions brittle). For feature tests, send XML with the explicit call helper rather than postJson:
$this->call('POST', '/webhooks/legacy', [], [], [], [
'CONTENT_TYPE' => 'application/xml',
], $xml);
Integration tip for existing Laminas/ZF code. If you are pulling this in because a legacy service already uses it, isolate it in one adapter class. The class namespace changes (Zend\Xml2Json → Laminas\Xml2Json) and the rest of your Laravel app should not care.
composer.json constraints (PHP 5.6/7.x era) may block installation entirely. Prefer laminas/laminas-xml2json, or replace it with ~10 lines of simplexml_load_string() + recursive array conversion, which also lets you control the output shape.<item> becomes an object; two <item> elements become an array. Your consuming code must normalize to arrays before iterating, or you will get intermittent "trying to access array offset on string" errors in production.xmlns can appear as ordinary keys/attributes. Do not use this to round-trip XML.@attributes and @text are reserved keys. If your downstream flattening logic is naive, it will clash with real element names or produce inconsistent field names. Keep the normalization step centralized.RuntimeException (and libxml warnings) rather than a rich parse error. Wrap conversion in a try/catch, validate content length and content type first, and return a 422 for malformed payloads.libxml_set_external_entity_loader() / LIBXML_NONET so you control entity resolution.<foo/>, <foo></foo>, and <foo> </foo> may not produce the representation you expect. Write a fixture test for each form you actually receive before building logic on top.simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA) and inspect the tree. The bug is almost always in SimpleXML's representation, not in the JSON encoding step.How can I help you explore Laravel packages today?