199 lines
5.4 KiB
Markdown
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!
|