Laravel's Responsable interface

Published in Code on Oct 9, 2020

Not many people know about the Responsable interface that ships with Laravel. I was introduced to it in this tweet by Adam Wathan.

As I was doing some cleanup on an app recently, I noticed there were multiple controllers that returned the same view. This view required some props that always needed to be passed in and a few optional props that each controller passed into the view.

Imagine something like the following:

class TransactionController
{
	public function create()
    {
     	return view('transaction.create', [
          	'data'                  => auth()->user()->detailsOnFile,
            'hasConvenienceFees'    => auth()->user()->hasConvenienceFees(),
          	'allowedPaymentMethods' => auth()->user()->allowedPaymentMethods,
          	'savedPaymentMethods' => SavedPaymentMethod::forUser(auth()->user()),
        ]);
    }
}

Imagine you have two additional controllers that also return this view, but they each have some extra props to pass in.

class PaymentLinkController
{
	public function create()
    {
     	return view('transaction.create', [
        	'data'                  => auth()->user()->detailsOnFile,
            'hasConvenienceFees'    => auth()->user()->hasConvenienceFees(),
          	'allowedPaymentMethods' => auth()->user()->allowedPaymentMethods,
          	'savedPaymentMethods' => SavedPaymentMethod::forUser(auth()->user()),
          	'paymentAmount' => $paymentAmount,
          	'isReadOnly' => true,
        ]);
    }
}
class SettlementPaymentController
{
	public function create()
    {
     	return view('transaction.create', [
        	'data'                  => auth()->user()->detailsOnFile,
            'hasConvenienceFees'    => auth()->user()->hasConvenienceFees(),
          	'allowedPaymentMethods' => auth()->user()->allowedPaymentMethods,
          	'savedPaymentMethods' => SavedPaymentMethod::forUser(auth()->user()),
          	'paymentAmount' => $paymentAmount,
          	'isReadOnly' => true,
          	'isSettlementOffer' => true,
        ]);
    }
}

Notice how we're forced to pass in all the required props and the props explicitly needed in each controller.

There are multiple ways to clean this up. The first solution for many might be to use View Composers, a feature built-in to Laravel to pass in data to a view each time it's rendered. I am not a fan of View Composers as they tend to get lost in the codebase. You can read more about view composers here.

Responsable saves the day

Using the Illuminate\Contracts\Support\Responsable interface we could create classes that many people call dedicated view objects that encapsulate all the logic necessary for a specific response; in this case, our transaction.create view.

Here's our new TransactionCreateView class that implements the Responsable interface. To adhere to this interface we only need to define a toResponse method. This method should return a normal Laravel response.

<?php

namespace App\Http\Responses;

use App\Services\Payment\SavedPaymentMethod;
use Illuminate\Contracts\Support\Responsable;

class TransactionCreateView implements Responsable
{
    public array $props;

    public function __construct(array $data = [])
    {
        $this->props = array_merge($this->getDefaultProps(), $data);
    }

    public function toResponse($request)
    {
        return view('transaction.create', $this->props);
    }

    private function getDefaultProps()
    {
        return [
            'data'                  => auth()->user()->detailsOnFile,
            'hasConvenienceFees'    => auth()->user()->hasConvenienceFees(),
            'allowedPaymentMethods' => SavedPaymentMethod::forUser(auth()->user()),
            'savedPaymentMethod'    => SavedPaymentMethod::forUser(auth()->user()),
        ];
    }
}

The TransactionCreateView class encapsulates all the props needed for the view but still allows additional props to be passed into the view when needed. So now, we could refactor our controllers to return this class and only pass in the specific data required in each scenario.

class TransactionController
{
	public function create()
    {
-     	return view('transaction.create', [
-          	'data'                  => auth()->user()->detailsOnFile,
-           'hasConvenienceFees'    => auth()->user()->hasConvenienceFees(),
-          	'allowedPaymentMethods' => auth()->user()->allowedPaymentMethods,
-          	'savedPaymentMethods' => SavedPaymentMethod::forUser(auth()->user()),
-        ]);
+		return new App\Http\Responses\TransactionCreateView();
    }
}

In our other two controllers, we now only need to pass in the additional required props since our Responsable class takes care of the default data.

class PaymentLinkController
{
	public function create()
    {
-     	return view('transaction.create', [
-        	'data'                  => auth()->user()->detailsOnFile,
-           'hasConvenienceFees'    => auth()->user()->hasConvenienceFees(),
-          	'allowedPaymentMethods' => auth()->user()->allowedPaymentMethods,
-          	'savedPaymentMethods' => SavedPaymentMethod::forUser(auth()->user()),
-          	'paymentAmount' => $paymentAmount,
-          	'isReadOnly' => true,
-        ]);
+		return new App\Http\Responses\TransactionCreateView([
+			'paymentAmount' => $paymentAmount,
+          	'isReadOnly' => true,
+		]);
    }
}
class SettlementPaymentController
{
	public function create()
    {
-     	return view('transaction.create', [
-        	'data'                  => auth()->user()->detailsOnFile,
-           'hasConvenienceFees'    => auth()->user()->hasConvenienceFees(),
-          	'allowedPaymentMethods' => auth()->user()->allowedPaymentMethods,
-          	'savedPaymentMethods' => SavedPaymentMethod::forUser(auth()->user()),
-          	'paymentAmount' => $paymentAmount,
-          	'isReadOnly' => true,
-          	'isSettlementOffer' => true,
-        ]);
+		return new App\Http\Responses\TransactionCreateView([
+			'paymentAmount' => $paymentAmount,
+          	'isReadOnly' => true,
+			'isSettlementOffer' => true,
+		]);
    }
}

There you go. Hopefully, this example gave you a better idea of the use cases for the Responsable interface in Laravel.