Files
olla/content/posts/2017-07-26-improvements-of-the-new-Responsable-interface-in-Laravel.md
2026-02-18 17:26:07 +01:00

199 lines
5.4 KiB
Markdown

---
title: "Improvements of the new Responsable interface in Laravel"
image: /images/responsable.png
thumbnail: /images/thumbnail-responsable.png
description: "Laravel 5.5 will ship with a new feature: the Responsable interface. Why not add some sugar to it?"
lang: en
website: dev
---
The new `Responsable` interface in Laravel is really awesome, it allows us to simplify our controllers with custom responses objects.
But one of the complexity with responses is the content negotiation and [the new feature](https://github.com/laravel/framework/commit/c0c89fd73cebf9ed56e6c5e69ad35106df03d9db) doesn't help us on this. How could we solve this problem?
<!--more-->
---
### The new feature
First, let's look at how Laravel uses the `Responsable` interface in the Router.
```php
if ($response instanceof Responsable) {
$response = $response->toResponse();
}
if ($response instanceof PsrResponseInterface) {
$response = (new HttpFoundationFactory)->createResponse($response);
} elseif (! $response instanceof SymfonyResponse &&
($response instanceof Arrayable ||
$response instanceof Jsonable ||
$response instanceof ArrayObject ||
$response instanceof JsonSerializable ||
is_array($response))) {
$response = new JsonResponse($response);
} elseif (! $response instanceof SymfonyResponse) {
$response = new Response($response);
}
```
The only method on the `Respondable` interface is the `toResponse` method. Then, if the response is `Jsonable` or something similar, Laravel will use the `JsonResponse` which `json_encode` all the data.
### Adding Content Negotiation
There is no sign of Content-Negotiation in this method. So if I need to build a response in JSON or in HTML depending of the type the request, I will have some conditions in my controller:
```php
public function index()
{
$users = User::all();
if (request()->wantsJson()) {
return new UsersJsonResponse($users);
}
return new UsersHtmlResponse($users);
}
```
Not too bad, thanks to the awesome `wantsJson()` method, but it could be better (especially if my controller is not a simple database query):
```php
public function index()
{
$users = User::all();
return UsersResponse($users);
}
```
Better! Now let's dive in the `UsersResponse`:
```php
class UsersResponse implements Responsable
{
public $users;
public function __construct($users)
{
$this->users = $users;
}
public function toResponse()
{
if (request()->wantsJson()) {
return $this->users;
}
return view('users.index', ['users' => $this->users]);
}
}
```
### Avoiding conditionals with polymorphism
It's often a good thing to replace conditionals by polymorphism. I would rather prefer this response with a new `NegociateContent` trait:
```php
class UsersResponse implements Responsable
{
use NegociateContent;
public $users;
public function __construct($users)
{
$this->users = $users;
}
public function toJsonResponse()
{
return $this->users;
}
public function toHtmlResponse()
{
return view('users.index', ['users' => $this->users]);
}
}
```
Or this one which extends a new `BaseResponse`:
```php
class UsersResponse extends BaseResponse
{
public $users;
public function __construct($users)
{
$this->users = $users;
}
public function toJsonResponse()
{
return $this->users;
}
public function toHtmlResponse()
{
return view('users.index', ['users' => $this->users]);
}
}
```
The `NegociateContent` or the `BaseResponse` implementations are really straightforward. We can add more checks and content type if needed of course.
```php
class BaseResponse implements Respondable
{
public function toResponse()
{
if (request()->wantsJson()) {
return $this->toJsonResponse();
}
return $this->toHtmlResponse();
}
}
```
### Let's try to build the same `view` helper as in mailables
We can even simplify the `toHtmlResponse` method with the same concept as in `Mailable`. As you may know, in mailables, all public properties are directly accessible in the mail view.
```php
public $users;
public function toHtmlResponse()
{
return $this->view('users.index');
}
```
I stole this code from the [`Illuminate\Mail\Mailable`](https://github.com/laravel/framework/blob/master/src/Illuminate/Mail/Mailable.php#L223) class in the framework (I just refactored it to collections).
```php
public function view($view, $overrides = [])
{
return collect((new ReflectionClass($this))->getProperties(ReflectionProperty::IS_PUBLIC))
->reject(function($property) {
return $property->getDeclaringClass()->getName() == self::class;
})
->mapWithKeys(function($property) {
return [$property->getName(), $property->getValue($this)];
})
->merge($overrides)
->pipe(function($data) use($view) {
return view($view, $data);
});
}
```
### Conclusion
I think these few lines of code really improve the `Responsable` interface. I really like the `Mailable` reflection as it feels really natural to me.
I may try to pull request the framework to add these features if people like them so don't hesitate to send me a message [on Twitter \@ThibaudDauce](https://twitter.com/ThibaudDauce) or [by mail (thibaud@dauce.fr)](mailto:thibaud@dauce.fr) to tell me what you think!