Mapping requests to DTOs inside Laravel's form requests

March 02, 20203 min read

Laravel’s form requests are custom request classes that contain validation logic. To use them, you just have to type-hint the form request on your controller method and the incoming request will be automatically validated.

Form request example:

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

final class StoreBlogPost extends FormRequest
{
    public function rules()
    {
        return [
            'title' => [
                'required',
            ],
            'body' => [
                'required',
            ],
        ];
    }
}

Form request usage inside controller methods:

<?php

declare(strict_types=1);

namespace App\Http\Controllers;

use App\Http\Requests\StoreBlogPost;

final class BlogPostsController extends Controller
{
    public function store(StoreBlogPost $request)
    {
        // The incoming request is valid...
    }
}

The problem is that in our controller method, the IDE can’t tell us what data is inside the $request variable.

To solve this problem, we can use DTOs. Since native DTOs aren’t supported in PHP, we can use data-transfer-object by Spatie (I’d recommend taking a look at it first, before reading this post), which can be installed by running:

composer require spatie/data-transfer-object

Our first request DTO

Here’s how the DTO for our StoreBlogPost request would look:

<?php

declare(strict_types=1);

namespace App\DataTransferObjects;

use Illuminate\Http\Request;
use Spatie\DataTransferObject\DataTransferObject;

final class BlogPostRequestData extends DataTransferObject
{
    public string $title;

    public string $body;

    public static function fromRequest(Request $request): BlogPostRequestData
    {
        return new static([
            'title' => $request->input('title'),
            'body' => $request->input('body'),
        ]);
    }
}

Notice that we’ve declared a static constructor fromRequest, where we’ve defined how the request maps to the DTO.

Now inside our controller method, we can instantiate the DTO from the incoming request:

<?php

declare(strict_types=1);

namespace App\Http\Controllers;

use App\Http\Requests\StoreBlogPost;
use App\DataTransferObjects\BlogPostRequestData;

final class BlogPostsController extends Controller
{
    public function store(StoreBlogPost $request)
    {
        $requestData = BlogPostRequestData::fromRequest($request);
    }
}

Our IDE now knows that $requestData contains title and body.

Instantiating the DTO inside form requests

Since form requests are simple classes, we can define helper methods right inside them, for further usage inside our controller methods.

The static constructor fromRequest from BlogPostRequestData, can be moved to the form request, so we won’t have to import and instantiate DTOs in our controllers.

The formRequest method can be removed from our DTO:

<?php

declare(strict_types=1);

namespace App\DataTransferObjects;

use Spatie\DataTransferObject\DataTransferObject;

final class BlogPostRequestData extends DataTransferObject
{
    public string $title;

    public string $body;
}

And here is how our form request will look now:

<?php

namespace App\Http\Requests;

use App\DataTransferObjects\BlogPostRequestData;
use Illuminate\Foundation\Http\FormRequest;

final class StoreBlogPost extends FormRequest
{
    public function rules()
    {
        return [
            'title' => [
                'required',
            ],
            'body' => [
                'required',
            ],
        ];
    }

    public function data()
    {
        return new BlogPostRequestData([
            'title' => $this->input('title'),
            'body' => $this->input('body'),
        ]);
    }
}

This way, we can keep our controllers clean and retrieve the mapped request using the data method, defined in our form request:

public function store(StoreBlogPost $request)
{
    $request->data()->title;
}

Thank you very much for reading this post! Subscribe below and get notified when new posts will be released or follow me on Twitter (@sandulat).


Join the Newsletter

Subscribe to get my latest posts by email.

I respect your privacy. Unsubscribe at any time.