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>&#165;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.