Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Response Xml Laravel Package

mtownsend/response-xml

Return XML from your Laravel API as easily as JSON. This package adds a fluent xml() response helper that converts Eloquent models, collections, arrays, and paginators into clean, customizable XML, with support for custom root nodes, headers, and encoding — no manual string building required.

View on GitHub
Deep Wiki
Context7

Getting Started

The package is a thin macro layer over spatie/array-to-xml. Install it and you can return XML from any controller without touching Response internals:

composer require mtownsend/response-xml

On Laravel 5.5+ there is nothing to register — the service provider is auto-discovered and registers an xml macro on the response factory plus a global xml() helper. Only add Mtownsend\ResponseXml\ResponseXmlServiceProvider::class to config/app.php if you are on an older skeleton. If you want a global default root element, publish the package config (php artisan vendor:publish --provider="Mtownsend\ResponseXml\ResponseXmlServiceProvider") rather than passing $xmlRoot everywhere.

First working route:

Route::get('/users/{id}', function (int $id) {
    return response()->xml([
        'user' => [
            'id'   => $id,
            'name' => 'Jane Doe',
        ],
    ]);
});

curl -i will show Content-Type: text/xml and a <response> root wrapping your data. That is the whole mental model: you shape a PHP array, the package serializes it.

Where to look first:

  1. The response()->xml() signature in the README/service provider — it controls status, headers, root element, namespace, and libxml options.
  2. The spatie/array-to-xml docs. Every quirk about nested arrays, _attributes, _value, repeated elements, and key sanitization comes from that dependency, not this package.
  3. The package's own config if you publish it — the only knob is typically the default root name.

First real use case: legacy integrations, sitemaps, RSS/Atom feeds, and clients that send Accept: application/xml. Start by building one endpoint that mirrors an existing JSON endpoint, then put the array-building logic in a dedicated transformer so it stays testable.

Implementation Patterns

Controller response with full control. The macro signature is essentially:

response()->xml(
    $data,
    $status = 200,
    $headers = [],
    $xmlRoot = 'response',
    $domain = null,
    $options = null
);

Use it directly for one-off endpoints:

return response()->xml($payload, 201, ['Cache-Control' => 'no-store'], 'order');

Repeated elements via list arrays. Numeric keys mean "repeat this element". This is the pattern for collections:

return response()->xml([
    'users' => [
        ['id' => 1, 'name' => 'Jane'],
        ['id' => 2, 'name' => 'John'],
    ],
], 200, [], 'users');

Attributes and text content. Do not invent your own convention — use spatie's reserved keys:

return response()->xml([
    'user' => [
        '_attributes' => ['id' => 42, 'active' => 'true'],
        'name'        => 'Jane',
        'bio'         => [
            '_attributes' => ['lang' => 'en'],
            '_value'      => 'Hello & welcome',
        ],
    ],
]);

Sitemaps and namespaced roots. The $domain argument is the cleanest way to set a default xmlns:

return response()->xml(
    ['url' => $urls],           // each: ['loc' => ..., 'lastmod' => ...]
    200,
    [],
    'urlset',
    'http://www.sitemaps.org/schemas/sitemap/0.9'
);

Content negotiation instead of duplicate routes. Decide the format once and branch at the response boundary:

$accept = $request->header('Accept', '');
$data   = (new UserResource($user))->resolve();

return Str::contains($accept, ['xml', 'application/xml'])
    ? response()->xml(['user' => $data], 200, [], 'user')
    : response()->json($data);

Add format=xml as an explicit query fallback — browsers frequently send Accept: text/html and will never hit the XML branch otherwise.

Normalize before serializing. Eloquent models, Collections, and API Resources are not arrays in the way ArrayToXml expects. Always run ->toArray()/->resolve() and format Carbon dates and enums explicitly. Consider a small XmlPresenter or toXmlArray() method on your resources so JSON and XML share one domain representation.

Testing. Treat the response as a normal Illuminate\Http\Response:

$response = $this->get('/users/1');

$response->assertOk();
$response->assertHeader('Content-Type', 'text/xml; charset=UTF-8');
$response->assertSee('<name>Jane Doe</name>', false); // false = don't escape

$this->assertXmlStringEqualsXmlString(
    '<response><user><id>1</id></user></response>',
    $response->getContent()
);

Caching. XML is a string once built. For hot endpoints, cache the serialized body and return it with a manual content type rather than re-serializing per request:

$xml = Cache::remember("user:{$id}:xml", 300, fn () => response()->xml(...)->getContent());
return response($xml)->header('Content-Type', 'text/xml; charset=UTF-8');

Gotchas and Tips

  • Content-Type is text/xml, not application/xml. Many SOAP-ish clients and API gateways are picky. Override via the $headers argument rather than fighting the macro. Also, assert on the exact charset your installed version emits when writing tests.
  • assertSee() escapes by default. Passing true (the default) will make <name> assertions fail. Always pass false when asserting markup.
  • Top-level arrays should be associative. A numer-indexed top-level array serializes as repeated generic children under the root; there is no way to make the root itself an attribute-carrying element beyond $xmlRoot/$domain. If you need root attributes beyond the namespace, build the root manually with spatie directly or a DOMDocument.
  • Key sanitization bites. Spaces in keys are (by default in spatie) replaced with underscores; keys starting with digits, or containing odd characters, can produce invalid XML or silent renames. Prefer explicit string keys and keep element names simple.
  • Empty values are inconsistent. null, [], '', and false all produce different output across spatie versions. Test each shape you rely on; don't assume <foo/> versus omission.
  • Booleans and dates don't format themselves. true becomes 1 and false becomes an empty string in PHP casting; Carbon becomes Y-m-d H:i:s via __toString. Format to Y-m-d\TH:i:sP or true/false strings explicitly before handing the array over.
  • Don't pre-escape. The serializer escapes &, <, and > for you. Passing htmlspecialchars()-prepared values results in double-escaping (&amp;amp;). Pass raw values.
  • Special key names are reserved. _attributes, _value (and version-dependent _cdata) are interpreted, not emitted as child elements. Renaming a domain field to one of these without knowing this is a classic silent bug.
  • Debug with the raw body. When output looks wrong, run $response->getContent() through xmllint --format - or simplexml_load_string() in a test. A well-formedness check catches escaping and invalid-name issues faster than eyeballing.
  • $options is a libxml passthrough. Useful for things like LIBXML_NOBLANKS/LIBXML_NOCDATA, but behaviors differ between dependency versions. Pin spatie/array-to-xml in composer.json if your output shape matters.
  • Reserve this for simple XML. Sitemaps, feeds, and flat resource payloads are its sweet spot. For XSD-validated documents, SOAP envelopes, or heavy namespace prefix work, build with DOMDocument or a dedicated package — you'll spend less time fighting the array abstraction.
  • Beware the global xml() helper. If another package defines xml(), you get a fatal redeclaration. Check app()->bound()/helper registration order, or just stick to response()->xml().
  • Version compatibility is the maintenance risk. It depends on Laravel's response factory and spatie v3+. Before upgrading Laravel, run your XML assertions against the new skeleton; the macro is small enough that a fork is viable if upstream lags.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib