If you use Laravel in your project for a long time, your models are likely to become quite large. Over time, they become harder to maintain, because They are overgrown with new features. When you write code for each case where your models are used, it is tempting to “fatten” our models until they get fat.
In such situations, we can use the Decorator pattern, which will allow us to separate the code specific to each case into a separate class. For example, we can use decorators to separate presentation views for a PDF document, CSV, or API response.
A decorator is an object that wraps another object in order to extend its functionality. It also passes the method calls to the object that was wrapped if they are not in the decorator. Decorators can be useful when you need to change the behavior of a class without resorting to inheritance. You can use them to add additional functionality to your objects, such as logging, access control, and the like.
A presenter is a type of Decorator used to bring an object to the desired form (for example, for a Blade-template or an API response).
Suppose we have a collection of users, which we must return in the API response. In Laravel, we can easily implement this by simply returning the collection itself, which will then be converted to JSON. Let's get the models of our users in the controller:
<?php namespace App\Http\Controllers; use App\Http\Controllers\Controller; class UsersController extends Controller { public function index() { return User::all(); } }
The all
method returns all users from the database. The User model contains all table fields. Passwords and other important information are also there. In addition, Laravel automatically converts the result of the all
method to JSON when output.
However, this is not the best solution to the problem. For example, you do not need to send user password hashes in response.
Also, we may not like what the dates created_at
and updated_at
look like. Or if our is_active
field is of type tinyint
, we may want to convert it to a string or to a boolean value.
Note: Yes, yes, I know that Eloquent allows you to hide the model's fields when converting it to JSON using the model's $ hidden property. Just play along to me.
Now that we have a collection of User models, we need to figure out how to pass them on to the presentation by wrapping them in a decorator. We need a class that will serve as the Presenter. Our class UserPresenter in this case will look like this:
<?php namespace App\Users; class UserPresenter { protected $model; public function __construct(Model $model) { $this->model = $model; } public function __call($method, $args) { return call_user_func_array([$this->model, $method], $args); } public function __get($name) { return $this->model->{$name}; } public function fullName() { return trim($this->model->first_name . ' ' . $this->model->last_name); } }
Note that our presenter gets the properties first_name
, last_name
, and created_at
the model, because the presenter does not have these properties.
I love stupid analogies and this is one of them: the decorator is something like a Batman costume on Bruce Wayne. And Batman has a bunch of different costumes for different situations. Like Batman's costumes, we can use different decorators for different situations where we need a User model. Let's rename our decorator to something more suitable, for example, ApiPresenter, and then place it in the Presenters folder. We will also separate the code that can be reused into a separate class Presenter:
<?php namespace App\Presenter; abstract class Presenter { protected $model; public function __construct(Model $model) { $this->model = $model; } public function __call($method, $args) { return call_user_func_array([$this->model, $method], $args); } public function __get($name) { return $this->model->{$name}; } }
Let's add a new method to the ApiPresenter
:
<?php namespace App\Users\Presenters; use App\Presenter\Presenter; class ApiPresenter extends Presenter { public function fullName() { return trim($this->model->first_name . ' ' . $this->model->last_name); } public function createdAt() { return $this->model->created_at->format('n/j/Y'); } }
You might think that you can use Laravel mutators to convert dates to the format we need and avoid all this messing with presenters. This is possible if we only need one mapping option.
You can also say: "I could leave the created_at
field as it is and use several mutators for different situations. For example, friendlyCreatedAt()
, pdfCreatedAt()
and createdAtAsYear()
". The main argument against this approach is that your model will gradually become huge and will bring us a lot of anxiety. We can shift this responsibility to a separate class, which will bring our model to the desired form.
Let's add some more methods to our presenter:
<?php namespace App\Users\Presenters; class ApiPresenter { public function fullName() { return trim($this->model->full_name . ' ' . $this->model->last_name); } public function createdAt() { return $this->model->created_at->format('n/j/Y'); } public function isActive() { return (bool) $this->model->is_active; } public function role() { if ($this->model->is_admin) { return 'Admin'; } return 'User'; } }
Here we is_active
field of our model to a logical type instead of tinyint
. We also provide a string representation of the user role API.
Let's return to our controller. Now we can use the presenter to build the answer:
<?php namespace App\Http\Controllers; use App\Users\Presenters\ApiPresenter; use App\Http\Controllers\Controller; class UsersController extends Controller { public function show($id) { $user = new ApiPresenter(User::findOrFail($id)); return response()->json([ 'name' => $user->fullName(), 'role' => $user->role(), 'created_at' => $user->createdAt(), 'is_active' => $user->isActive(), ]); } }
It is wonderful! Now the API returns only the necessary information and the code began to look cleaner. But even better, if we want to use a value that is not in ApiPresenter
, but in the User model, we can simply return it dynamically from the model, as we are used to:
<?php return response()->json([ 'first_name' => $user->first_name, 'last_name' => $user->last_name, 'name' => $user->fullName(), 'role' => $user->role(), 'created_at' => $user->createdAt(), 'is_active' => $user->isActive(), ]);
Decorator is a pretty powerful pattern that allows you to keep your code clean and tidy. But what about the first situation when we had a collection of models? We can loop through it and create a new array:
<?php namespace App\Http\Controllers; use App\Http\Controllers\Controller; use App\Users\Presenters\ApiPresenter; class UsersController extends Controller { public function index() { $users = User::all(); $apiUsers = []; foreach ($users as $user) { $apiUser = new ApiPresenter($user); $apiUsers[] = [ 'first_name' => $apiUser->model->first_name, 'last_name' => $apiUser->model->last_name, 'name' => $apiUser->fullName(), 'role' => $apiUser->role(), 'created_at' => $apiUser->createdAt(), 'is_active' => $apiUser->isActive(), ]; } return response()->json($apiUsers); } }
Everything is beautiful, but it doesn't look very beautiful. Instead, I want to use macros that the Collection
class allows to create:
<?php Collection::macro('present', function ($class) { return $this->map(function ($model) use ($class) { return new $class($model); }); });
This code can be placed in the service provider of your application. Now we call our macro by specifying the desired presenter:
<?php namespace App\Http\Controllers; use App\Http\Controllers\Controller; use App\Users\Presenters\ApiPresenter; class UsersController extends Controller { public function index() { $users = User::all() ->present(ApiPresenter::class) ->map(function ($user) { return [ 'first_name' => $user->first_name, 'last_name' => $user->last_name, 'name' => $user->fullName(), 'role' => $user->role(), 'created_at' => $user->createdAt(), 'is_active' => $user->isActive(), ]; }); return response()->json($users); } }
Each model turns into a presenter. If you want, you can transfer the collection to another decorator to combine several objects into a single JSON.
Thus, decorators and presenters are powerful tools that we have. They are easy to write and easy to test. Use them when it makes sense. They can help you with refactoring.
But that is not all. It would be cool if you could call the present method for a particular model. And if we had a helper that would allow us to wrap the model in a presenter.
Well, let me introduce you to the Hemp / Presenter package. He does all the things we talked about, plus everything he realizes the wishes I was talking about. And all this is tested. Try and tell me what you think of him. Enjoy!
Original: http://davidhemphill.com/blog/2016/09/06/presenters-in-laravel
Source: https://habr.com/ru/post/309942/
All Articles