If you’ve been cornered into creating API endpoints consuming a XML request body, you would see Laravel doesn’t have framework support. There is a $request->json()
method for JSON content but you must write app-level code to make validation work on inbound XML. There are StackOverflow posts floating around that suggest simplexml_load_string()
through json_encode()
-> json_decode()
will form a PHP associative array. However this solution incorrectly handles CDATA
nodes.
So I’ve created a HasXmlContentType
trait that can be placed in any FormRequest
class. This makes $request->input($key)
, $request->all()
, etc. a PHP array payload so Laravel’s request validation “just works”.
This requires adding an external Composer package to parse XML strings.
$ composer require midnite81/xml2array
Allow FormRequest
classes to use XML content
Define reusable PHP trait HasXmlContentType
.
app/Http/Requests/Concerns/HasXmlContentType.php
namespace App\Http\Requests\Concerns;
use Illuminate\Support\Arr;
use Illuminate\Http\Response;
use Midnite81\Xml2Array\Exceptions\IncorrectFormatException;
use Midnite81\Xml2Array\Xml2Array;
use Symfony\Component\HttpFoundation\ParameterBag;
trait HasXmlContentType
{
protected $xmlSource;
protected function getInputSource()
{
if ($this->xmlSource === null) {
$this->xmlSource = new ParameterBag(
$this->nullEmptyArrays($this->xmlParameters())
);
}
return $this->xmlSource;
}
protected function xmlParameters()
{
try {
return Xml2Array::create($this->getContent())->toArray();
} catch (IncorrectFormatException $e) {
abort(Response::HTTP_UNSUPPORTED_MEDIA_TYPE, 'Expected Content-Type: application/xml.');
} catch (\ErrorException $e) {
return [];
}
}
protected function nullEmptyArrays(array $arr)
{
foreach ($arr as $key => $value) {
if (is_array($value)) {
if (empty($value)) {
$arr[$key] = null;
} else {
$arr[$key] = $this->nullEmptyArrays($value);
}
}
}
return $arr;
}
abstract public function getContent($asResource = false);
}
Import this trait into any FormRequest
child class that always accepts XML content in the request body. Define validation rules like it is a regular HTTP GET query or POST body.
app/Http/Requests/StoreAlbumRequest.php
namespace App\Http\Requests;
class StoreAlbumRequest extends FormRequest
{
use Concerns\HasXmlContentType;
public function authorize()
{
return true;
}
public function rules()
{
return [
'artist' => 'required|string|max:255',
'title' => 'required|string|max:255',
'catalog' => 'required|string|max:32',
'description' => 'required|string|max:255',
'artwork' => 'nullable|array',
'artwork.image' => 'nullable|array|max:10',
'artwork.image.*.description' => 'nullable|string|max:255',
'artwork.image.*.url' => 'required|string|max:255',
];
}
}
Enable XML content on all FormRequest
classes
If your requesting user knows how to send HTTP header Content-Type 'application/xml'
, you wouldn’t need to explicitly tell each FormRequest
child class to use the custom trait. Instead conditionally enable XML parsing in the parent FormRequest
.
app/Http/Requests/FormRequest.php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest as BaseFormRequest;
use Illuminate\Support\Str;
abstract class FormRequest extends BaseFormRequest
{
protected $xmlSource;
protected function getInputSource()
{
if ($this->xmlSource === null && $this->isXml()) {
$this->xmlSource = new ParameterBag(
$this->nullEmptyArrays($this->xmlParameters())
);
}
return $this->xmlSource ?? parent::getInputSource();
}
protected function isXml()
{
return Str::contains($this->header('CONTENT_TYPE'), ['/xml', '+xml']);
}
// ...
}
Now any endpoint dependency injecting a FormRequest
can support XML, JSON, form data, etc.
Handling XML content in the Laravel controller
Sending this XML request to an endpoint using the above StoreAlbumRequest
class:
<?xml version="1.0" encoding="UTF-8"?>
<album>
<artist>Gridlock</artist>
<title>Formless</title>
<catalog>¥736</catalog>
<description><![CDATA[ A <b>bold</b> statement. ]]></description>
<artwork>
<image>
<description>US Cover Art</description>
<url>https://somedropbox.de/hrec/formless-us.jpg</url>
</image>
<image>
<description>German Cover Art</description>
<url>https://somedropbox.de/hrec/formless-de.jpg</url>
</image>
<image>
<description></description>
<url>https://somedropbox.de/hrec/formless-back.jpg</url>
</image>
</artwork>
</album>
Your $request
will have validated data as an associative array to be passed into Eloquent models.
namespace App\Http\Controllers;
use App\Http\Requests\StoreAlbumRequest;
class AlbumsController extends Controller
{
public function store(StoreAlbumRequest $request)
{
$request->all();
// [
// 'artist' => 'Gridlock',
// 'title' => 'Formless',
// 'catalog' => '¥736',
// 'description' => 'A <b>bold</b> statement.',
// 'artwork' => [
// 'image' => [
// [
// 'description' => 'US Cover Art',
// 'url' => 'https://somedropbox.de/hrec/formless-us.jpg',
// ],
// [
// 'description' => 'German Cover Art',
// 'url' => 'https://somedropbox.de/hrec/formless-de.jpg',
// ],
// [
// 'description' => null,
// 'url' => 'https://somedropbox.de/hrec/formless-back.jpg',
// ]
// ]
// ]
// '@root' => 'album',
// ]
}
}
You can see empty XML node <description></description>
was transformed into a PHP null
value. External package class Xml2Array
parses that XML inner content into a blank PHP array, so HasXmlContentType@nullEmptyArrays()
makes it null
for the database. Laravel’s web middleware TrimStrings
and ConvertEmptyStringsToNull
will be run on XML content as well.
Handling one-or-many child nodes
There is a quirk for nodes with one-or-many many children. If the above XML contained only one <image>
, PHP $request->input('artwork.image')
wouldn’t return an array of items but rather a single image item as an associative array.
[
'artist' => 'Gridlock',
'title' => 'Formless',
'catalog' => '¥736',
'description' => 'A <b>bold</b> statement.',
'artwork' => [
'image' => [
'description' => 'US Cover Art',
'url' => 'https://somedropbox.de/hrec/formless-us.jpg',
],
]
'@root' => 'album',
]
This will be problematic when trying to save many album images in your database. Add Laravel macro Illuminate\Support\Arr::wrapNumeric()
that guarantees the array can be iterated upon for one-or-many Eloquent models.
Arr::macro('wrapNumeric', static function ($value) {
$value = static::wrap($value);
if (static::isAssoc($value)) {
return [$value];
}
return $value;
});
$images = Arr::wrapNumeric($request->input('artwork.image'));
foreach ($images as $imageAttributes) {
// create one Eloquent model
}
In this case, you many have to manipulate the StoreAlbumRequest
class to make validation rules 'artwork.image.*.description'
and 'artwork.image.*.url'
correctly apply when only one <image>
is given.