Versatile Response Objects in Laravel

I've found that introducing dedicated response objects that can return different response formats is a really nice pattern to cleanup my controllers

Published:
Reading time: 8m you'll never. get. back.

I’ve been tinkering with a new way of returning various response formats by introducing dedicated response objects to my Laravel web applications. This has been heavily inspired (read: copy & paste) by DHH and Adam Wathan’s chats on the Full Stack Radio Podcast and I thought I’d share my journey through, and ideas on, it with you.

CRUD controller

Within my application I generally, if not always, approach my controllers from a CRUD only perspective. A BeanController would provide the standard CRUD controller methods: index, create, store, show, edit, update, and destroy. Each of these methods would return a response suitable for my web interface, i.e., a view or a redirect.

Here is a bare bones example of my general structure and approach:

class BeanController
{
    public function index()
    {
        return view('beans.index', ['beans' => Bean::paginate()]);
    }

    public function create()
    {
        return view('beans.create', ['bean' => new Bean]);
    }

    public function store(BeanRequest $request)
    {
        $bean = Bean::create($request->validated());

        return redirect()->route('beans.show', $bean)->with(['status' => 'Bean created successfully']);
    }

    public function show(Bean $bean)
    {
        return view('beans.show', ['bean' => $bean]);
    }

    public function edit(Bean $bean)
    {
        return view('beans.edit', ['bean' => $bean]);
    }

    public function update(BeanRequest $request, Bean $bean)
    {
        $bean->update($request->validated());

        return redirect()->route('beans.show', $bean)->with(['status' => 'Bean updated successfully']);
    }

    public function destroy(Bean $bean)
    {
        $bean->delete();

        return redirect()->route('beans.index')->with(['status' => 'Bean deleted successfully']);
    }
}

Nothing out of the ordinary there.

Single action controller

Then along comes a request to download all Bean’s as a CSV file. Well that isn’t a big deal - what I’ll do is create a dedicated “single action” controller BeanCSVExportController to handle it for me.

I felt it was a good idea to have another controller handle the CSV export directly, as I didn’t like depending on / injecting a CsvWriter in my main [email protected] method when 99% of requests are for a web interface response. It didn’t feel right.

So I’d end up with a second controller to handle this scenario which would receive the CsvWriter as a method dependency:

<?php

class BeanCSVExportController
{
    public function __invoke(CsvWriter $csvWriter)
    {
        $csvWriter->insertOne($attributes = ['id', 'brand', 'strength']);

        Bean::each(function ($bean) use ($csvWriter, $attributes) {
            $csvWriter->insertOne($bean->only($attributes));
        });

        return response($csvWriter->getContent(), 200, [
            'Content-Encoding' => 'none',
            'Content-Description' => 'File Transfer',
            'Content-Type' => 'text/csv; charset=UTF-8',
            'Content-Disposition' => 'attachment; filename="beans-export.csv"',
        ]);
      }
}

And for a time - things were good.

But I started to think it was a bit strange using the __invoke() method. When I took a step back it looked to me like an index() call. After all I am showing a list of users…just in a different format…right?!? 🤔

So I started moving these over to use index() instead of __invoke(). But still - if it is single action - why would you specify what the action is?

I’m also going to be adding another controller for each new format: XML, JSON, etc.

Something just was not right either way I looked at it.

Seeing the light

Then it hit me; that I was really doing the same thing with my [email protected] and [email protected]. Pulling the Beans out of the database and pushing them to a response. I knew I had to find a simple solution to combine these controllers but still offer the different response formats.

This all became crystal clear when I started adding filters into the mix. When both my index() methods were adding query scopes based on HTTP query parameters - I instantly saw the duplicate code and knew there had to be a better way as they both would always start exactly the same.

public function index(Request $request)
{
    $beans = Bean::when($request->distributor, function ($query, $value) {
        $query->whereDistributor($value);
    })-> // format specific stuff would follow...
}

I could have refactored the query building out to a Repository - but I already felt the controllers needed to be merged - so that solution didn’t make sense to me.

Ah-hah! Response objects

I listened to DHH and Adam talking about dealing with different response formats by looking at the Accepts header. I saw Adam tweet that he had put together a macro to help with this kinda thing. In addition to this, I also saw the Responsable interface Laravel provided.

It all looked pretty neat and the cogs started turning, but I was busy on other projects and didn’t have time to play and work out a better solution.

I also found myself starting to build more API first approaches to web systems and the whole time I’m thinking: if I needed to introduce a web interface here, I’m again going to be duplicating these controllers: there. must. be. a. better. way!

I finally got some time to come back to the multi response format idea on a project and look at some ways to clean this all up using a new approach. However, since that Twitter post there was another (awesome) episode of the Full Stack Radio where Adam and DHH also discussed the idea of listening to file extensions and why that is valid - and I think that was the ah-hah moment for me.

This is where dedicated response objects rock!

Response objects that implement the Responsable interface can be returned from a controller and Laravel will call the toResponse($request) method on it. This allows you to move any complexity you might have creating a response out of the controller and into a dedicated object. They are really nice.

One controller to rule them all

I had a play with response objects and built a base Responsable class that would determine the response format (HTML, JSON, CSV, etc) based on either the url file extension or the Accepts header.

If a HTML response was expected, the toHtmlResponse() method would be called on the object, if JSON was the preferred response format, the toJsonResponse() method would be called, and so on. This allowed me to break up the logic required to create format specific responses into their own methods.

This was just the solution I was looking for to combine my controllers. Suddenly I’m cleaning things up and everything is starting to click. I can pipe all responses that need to list Bean’s through the [email protected] method. I can share the filtering across all response formats and defer the creation of the actual response to a dedicated object. Combining both the HTML and CSV controller resulted in a really streamlined controller:

class BeanController
{
    public function index()
    {
        $query = Bean::when($request->distributor, function ($query, $value) {
            $query->whereDistributor($value);
        });

        return new BeanIndexResponse($query);
    }
}

Within my response object I can now decide how I want things to happen for each response format and other formats can be added as needed. If we need to add a JSON API endpoint - that is easy: leave the controller as is and add a toJsonResponse() method to the response object. Then our JSON endpoint will inherit all the filtering abilities shared amongst the other response formats for free.

Here is a response object that extends my base Responsable class that will return our HTML, CSV, and JSON for our Beans:

class BeanIndexResponse extends Response
{
    protected $query;

    public function __construct($query)
    {
        $this->query = $query;
    }

    protected function toHtmlResponse()
    {
        return view('beans.index', ['beans' => $this->query->paginate()]);
    }

    // Dependencies are resolved from the container!
    protected function toCsvResponse(CsvWriter $csvWriter)
    {
        $csvWriter->insertOne($attributes = ['id', 'brand', 'strength']);

        $this->query->each(function ($bean) use ($csvWriter, $attributes) {
            $csvWriter->insertOne($bean->only($attributes));
        });

        return response($csvWriter->getContent(), 200, [
            'Content-Encoding' => 'none',
            'Content-Type' => 'text/csv; charset=UTF-8',
            'Content-Disposition' => 'attachment; filename="beans-export.csv"',
            'Content-Description' => 'File Transfer',
        ]);
    }

    // Returned values are recursively checked for `Responsable`
    // objects so you can even return other Response objects.
    protected function toJsonResponse()
    {
        return new ResourceCollection($this->query->paginate());
    }
}

With the correct routes setup, my [email protected] method can now respond with the correct format to these following urls. All query parameter filters would work accross them all!

// html view
GET: /beans

// csv export
GET: /beans.csv

// json api
GET: /beans.json

This pattern really has no use in a single response format situation - but once you need to provide multiple response formats - it works really well to clean up your controllers and move the response logic to its own home within your application.

I am really loving this new pattern and it has been the kind of thing, for me, where I want to go back and re-write everything right now instead of waiting until I touch the code again to update it.

After I tweeted about this Adam was great enough to reply with what he settled on for this kind of thing. I like the approach he has taken. It is simple and gets the job done. I would never have gotten to my own solution without seeing his approaches. I also saw that Miguel Piedrafita has packaged it up for others to use.

I personally like the dedicated class, as I can:

I also feel that it is more readable when dealing with multiple formats that each might do a bit of work - this is all totally subjective of course, and these things, I’m sure, could also be added to the macro.

Either way I think it is a great pattern. I am really looking forward to implementing it, while both cleaning up and combining some of my controllers! Hopefully some of this might be new to you as well and you can give it a whirl in your own applications. If you have any thoughts or your own implementations of this kind of thing I’d love to see it and learn more.

If you are interested in the base class I’ve been using I made a gist where you can check it out. If you have any suggestions on improvements I’d love to hear them.