Types are awesome, DTOs rule

Recently I was working on an Inertia/Laravel app, and needed to share some data with a Vue component without passing the data through the standard Inertia::render() system. I realized that this is something that a newer member on my team had never done before, so we paired up on it.

We just needed to check some quick data so we were using axios to POST it to some Route. Nothing special there, something like this:

JS Code:

const checkData = () => {
    const result = axios.post(route('data-check'), {
        data: someRef.value
    }).then((res) => {
        // deal with the result
    }).catch((err) => {
        // catch an error
    })
}

Routes file:

Route::post('/check-data', [Controller::class, 'dataCheck'])->name('data-check');

Controller file:

<?php

namespace App\Http\Controllers;

class Controller
{
    public function dataCheck(Request $request) : array
    {
        // check the request data
        $result = Thing::checkData($request->get("data"));

        // now, what does $result look like? if it doesn't match what we want in
        // JS, we can provide the desired shape in a vanilla PHP array
        return [
            "isValid" => $result->dataIsGood,
            "userID" => $result->userID,
            "lastUpdated" => $result?->lastUpdated ?? "2006-01-02 15:04:05",
        ];
    }
}

Really nothing special going on at all. However, we got to talking, and realized that while returning an array from Laravel works great, it doesn't make anything clearer for the front end. Laravel will turn that array into JSON, and we're happy with how fast and easy this all is. But it can get confusing. It'd be nice if we had a standard object that we returned and we can see exactly what the response type looks like.

Enter in the DTO (Data Transfer Object). There's a ton of packages out there to wire up some of this stuff for you, but modern PHP has everything we need. Also, the Gopher in me hates the idea of one more dependancy just to send a DTO out the door.

OK, so what does this look like?

First, let's see the goal that we are aiming for. I want to clean up that controller, so that instead of returning some random array (who's shape can change at any time, with no warning) let's return some DTO:

<?php

namespace App\Http\Controllers;

class Controller
{
    public function dataCheck(Request $request) : DataCheckDTO
    {
        // do something with the request data
        $result = Thing::checkData($request->get("data"));

        // return the JSON that the javascript is expecting
        return new DataCheckDto($result);
    }
}

or even better, just inline the call check (I don't always do this, it just depends on how long the line gets). Now our controller is about as stupid simple as it can be:

<?php

namespace App\Http\Controllers;

class Controller
{
    public function dataCheck(Request $request) : DataCheckDTO
    {
        return new DataCheckDto(Thing::checkData($request->get("data")));
    }
}

Time to make this class. I stick all my DTOs into a new folder inside of app. Something like this:

<?php

namespace App\DTO;

class DataCheckDTO
{
    public function __construct($result)
    {
        // work your magic here?
    }
}

OK, so now we are returning some class from a Laravel controller. What does Laravel do with this class? Right now, it'll thrown an error. A pretty cryptic one too:

Symfony\Component\HttpFoundation\Response::setContent(): Argument #1 ($content) must be of type ?string, App\DTO\DataCheckDTO given, called in ...

Basically, Laravel is trying to autmoatically convert your controller's return into something that makes sense. By default, Laravel can reutrn models, strings, ints, and a whole bunch of other things and just kinda read your mind as to what you're trying to do. When you return an array, Laravel will even just magically turn that into JSON for you. It's lovely.

But, now that we are returning some random class, Laravel has no idea what to do with it. Fear not! It's super easy to tell it what to do! Check it out:

<?php

namespace App\DTO;

use JsonSerializable;

class DataCheckDTO implements JsonSerializable
{
    public function __construct($result)
    {
        // work your magic here?
    }

    // add this method
    public function jsonSerialize(): mixed
    {
        return [
            //
        ];
    }
}

PHP comes with an internal interface named JsonSerializable which is exactly what Laravel is looking for. Once we make our DTO classes compatible with this interface, Laravel picks it up and uses that method. Now you're in complete control over how the JSON representation of your data will be!

You still have to keep this in sync with your JS manually, and if you're using TypeScript then you'll hwant to update your types over there. We typically don't, we'll just eyeball it to make the Inertia page component props or axios/fetch data handlers match these return arrays. It's so easy to spot check them, so we don't really mind the lack of a compile step that you'd get from TS. We'll also write an Integration test to verify our output shape so that we have both test coverage and a fallback to make sure our JSON schema isn't changing unless we want it to. It's also way easier to check the return line of a very small class than it is to find the one random array shape in a controller file, which is likely handling multiple route methods.

Happy coding!