bmatovu/laravel-xml
A Laravel package for working with XML. It converts XML requests and responses into arrays, adds XML response helpers and middleware, and lets your APIs accept and return XML alongside JSON, with configurable parsing and validation support.
Install
composer require bmatovu/laravel-xml
Before you run that, check the support matrix — as of v4.2.0 the package requires PHP 8.2+ and Laravel 12/13. Laravel 10/11 and PHP 8.1 were dropped in that release, so:
composer require bmatovu/laravel-xml:^4.2 # PHP 8.2+, Laravel 12/13
# apps still on Laravel 10/11 or PHP 8.1 are pinned to ^4.1 and no longer receive fixes
If composer require resolves to 4.1.x on your machine, that is the signal your PHP/Laravel combination is below the new floor — confirm with composer why-not bmatovu/laravel-xml 4.2.0 before assuming the package is at fault.
Laravel auto-discovers Bmatovu\LaravelXml\XmlServiceProvider. On older Laravel versions, add the provider to config/app.php. If the package ships a config file, publish it before you need it:
php artisan vendor:publish --provider="Bmatovu\LaravelXml\XmlServiceProvider"
Where to look first
vendor/bmatovu/laravel-xml/README.md — the API surface is small enough to read in five minutes.response()->xml(), $request->xml(), etc.) and middleware actually exist in your pinned version.ParseXml) — this is where incoming XML is turned into request input, and where most surprises live.First use case: XML out
Add a response to an existing controller without touching your models:
Route::get('/users', fn () => response()->xml(User::all()->toArray()));
First use case: XML in
Register the parse middleware for the route group that speaks XML, then consume the request like any other:
Route::middleware(\Bmatovu\LaravelXml\Middleware\ParseXml::class)->group(function () {
Route::post('/users', [UserController::class, 'store']);
});
If $request->all() is populated (not just the raw body from getContent()), the middleware ran and did its job.
Response macro from controllers and resources. The lowest-friction pattern is wrapping your existing array output: response()->xml(['users' => $users]). Keep the array shaping in a Resource or a toXml()/toArray() method so JSON and XML endpoints share one data source. Repeated elements need an explicit wrapper key (['user' => $users]) — an array of scalars does not automatically become repeated <user> nodes in most array-to-XML implementations.
Content negotiation. Don't duplicate routes. Inspect $request->header('Accept') (or a ?format=xml query param) and branch:
return $request->wantsXml()
? response()->xml($payload)
: response()->json($payload);
This keeps one controller and one test suite.
Reuse validation untouched. The main benefit of the parse middleware is that incoming XML becomes normal request input, so FormRequest, $request->validate(), and $request->only() all work unchanged. That means your day-to-day workflow is: enable middleware on the XML route group → write validation exactly as you would for JSON → forget XML exists.
Testing. Assert the header, then parse the body rather than string-matching:
$response->assertHeader('Content-Type', 'application/xml');
$xml = simplexml_load_string($response->getContent());
$this->assertSame('Jane', (string) $xml->user->name);
For inbound tests, use $this->call('POST', $uri, [], [], [], ['CONTENT_TYPE' => 'application/xml'], $xmlString); $this->post() with an array will be encoded as form data, not XML.
Integration tip. Put the parse middleware on a dedicated route group (or a named xml middleware group) rather than globally. Global registration forces every JSON and file-upload request through an XML sniff, and the ordering relative to TrimStrings / ConvertEmptyStringsToNull becomes something you have to reason about.
Upgrading to 4.2.0. The 4.1 → 4.2 diff is version floors and CI plumbing, not behaviour — no API removal, no macro or middleware rename. In practice the upgrade is: bump PHP to 8.2 if you haven't, make sure your Laravel constraint allows 12/13, composer update bmatovu/laravel-xml, then re-run your XML output-shape tests. If your app is on Laravel 10/11, plan the framework upgrade first; there is no XML work involved.
^4.1 deliberately and write the removal ticket the same day."1"/"true" where the contract says integer/boolean without documenting it.'@attributes' => [...]; if the package doesn't, you'll need to fall back to raw SimpleXMLElement for anything with attributes, namespaces, or CDATA. Don't promise a schema you can't produce.simplexml_load_string without LIBXML_NONET and with entity loading enabled, a malicious body is a real risk. Check the middleware source once and harden or fork if necessary — this is the single most important thing to verify in a low-adoption package. Note that a CI/Dependabot-green release like 4.2.0 says nothing about parser hardening; diff the middleware yourself.text/xml, none at all, or application/xml; charset=utf-8. If the middleware only triggers on an exact match, requests silently bypass parsing and $request->all() comes back empty. Log or dd($request->getContent(), $request->all()) when debugging.[] may serialize to <items/> or be omitted entirely depending on the underlying builder; decide and test it, because it's the kind of thing that breaks a consumer's parser in production.TransformsRequest sits in the global stack). Later registration means other middleware sees an unparsed body.response()->xml() is a macro registered at boot. It's fine under Octane, but if you define your own xml macro for another purpose you'll get a confusing overwrite. Grep before adding.composer.json, add a test that asserts your XML output shape, and accept that the escape hatch is ~30 lines: spatie/array-to-xml for output plus a small request macro for input. Keeping your XML shaping in one Resource method makes that swap cheap.mtvbrianking/laravel-xml, not the bmatovu namespace you installed. That's the same code lineage, but it means release notes aren't where you'd instinctively look — subscribe to the upstream repo's releases or you'll miss the next floor bump the way you may have missed this one.SimpleXMLElement yourself and return it with an explicit Content-Type header. Don't contort the array convention.How can I help you explore Laravel packages today?