Шаблон Presenter в Laravel

в 21:03, , рубрики: laravel, php, шаблоны проектирования

Если вы используете Laravel в своем проекте достаточно долго, скорее всего ваши Eloquent-модели стали довольно большими. Со временем их становится все труднее поддерживать, по мере того, как они обрастают функционалом. Когда вы пишете код для каждого случая, где вы используете ваши модели, возникает соблазн "откормить" наши модели до тех пор, пока они не разжиреют.

Шаблон Presenter в Laravel - 1

В таких ситуациях мы можем воспользоваться паттерном Декоратор, который позволит нам выделить код, специфичный для каждого случая в отдельный класс. Например, мы можем использовать декораторы для того, чтобы разделить формирование представления для PDF-документа, CSV или ответа API.

Что такое Декоратор и что такое Презентер?

Декоратор — это объект, который оборачивает другой объект для того, чтобы расширить его функционал. Он так же передает вызовы методов объекту, который был обернут, если их не нашлось в декораторе. Декораторы могут быть полезны, когда вам нужно изменить поведение класса не прибегая к наследованию. Вы можете использовать их для того, чтобы добавить дополнительный функционал вашим объектам, как например логирование, контроль доступа и тому подобное.

Презентер это разновидность Декоратора, используемая для приведения объекта к нужному виду (например для Blade-шаблона или ответа API).

Приведение коллекции пользователей к ответу API

Скажем, у нас есть коллекция пользователей, которую должен вернуть наш API. В Laravel мы можем легко реализовать это, просто вернув саму коллекцию, которая затем будет преобразована в JSON. Давайте получим модели наших пользователей в контроллере:

<?php

namespace AppHttpControllers;

use AppHttpControllersController;

class UsersController extends Controller
{
    public function index()
    {
        return User::all();
    }
}

Метод all возвращает всех пользователей из базы данных. Модель User содержит все поля таблицы. Пароли и другая важная информация так же находится там. При выводе Laravel автоматически преобразует результат метода all в JSON.

Однако это не самый лучший вариант решения задачи. К примеру, мы возможно не должны отправлять хеши паролей пользователей в ответе.

Также нам может не понравится, как выглядят даты created_at и updated_at. Еще, если наше поле is_active имеет тип tinyint, мы можем захотеть перевести ее в строку, либо в логическое значение.

Примечание: да-да, я знаю, что Eloquent позволяет скрыть поля модели при ее преобразовании в JSON, используя свойство $hidden у модели. Просто подыграйте мне.

Воспользуемся паттерном Presenter.

Сейчас, когда у нас есть коллекция моделей User, нам нужно понять, как передать их представлению, обернув их в декоратор. Нам понадобится класс, который будет выполнять роль Презентера. Наш класс UserPresenter в таком случае будет выглядеть следующим образом:

<?php

namespace AppUsers;

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);
    }
}

Заметьте, что наш презентер получает свойства first_name, last_name, и created_at у модели, потому что этих свойств нет у презентера.

Я люблю тупые аналогии и это одна из них: декоратор это что-то вроде костюма Бэтмена на Брюсе Уейне. И у Бэтмена есть куча разных костюмов для разных ситуаций. Как и костюмы Бэтмена мы можем использовать различные декораторы для разных сценариев, где нам нужна модель User. Давайте воспользуемся нашими знаниями и переименуем наш декотратор во что-то более подходящее, например, ApiPresenter, а затем поместим его в папку Presenters. И раз мы решили заняться этим, давайте выделим код, который можно переиспользовать в отдельный класс Presenter:

<?php

namespace AppPresenter;

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};
    }
}

Давайте добавим новый метод к ApiPresenter:

<?php

namespace AppUsersPresenters;

use AppPresenterPresenter;

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');
    }
}

Вы могли бы подумать, что можно использовать мутаторы Laravel для преобразования дат в нужный нам формат и избежать всей это возни с презентерами. Это возможно, если нам нужен только один вариант отображения.

Вы также можете сказать: "Я мог бы оставить поле created_at как есть и использовать несколько мутаторов для разных ситуаций. Например, friendlyCreatedAt(), pdfCreatedAt() и createdAtAsYear()". Главным аргументом против такого подхода является то, что ваша модель постепенно станет огромной и будет приносить нам много беспокойства. Мы можем переложить эту ответственность на отдельный класс, который будет приводить нашу модель к нужному виду.

Давайте добавим еще несколько методов нашему презентеру:

<?php

namespace AppUsersPresenters;

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';
    }
}

Здесь мы приводим поле is_active нашей модели к логическому типу вместо tinyint. Также мы предоставляем API строковое представление роли пользователя.

Вернемся к нашему контроллеру. Теперь мы можем использовать презентер для построения ответа:

<?php

namespace AppHttpControllers;

use AppUsersPresentersApiPresenter;
use AppHttpControllersController;

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(),
        ]);
    }
}

Это замечательно! Теперь API возвращает только нужную информацию и код стал выглядеть чище. Но еще лучше то, что если мы захотим использовать значение, которого нет в ApiPresenter, но есть в модели User мы можем просто вернуть его динамически из модели, как мы привыкли:

<?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(),
]);

Декорирование коллекции пользователей

Декоратор это довольно мощный паттерн, который позволяет сохранять в вашем коде чистоту и порядок. Но как насчет первой ситуации, когда у нас была коллекция моделей? Мы можем пройтись по ней циклом и создать новый массив:

<?php

namespace AppHttpControllers;

use AppHttpControllersController;
use AppUsersPresentersApiPresenter;

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);
    }
}

Все прекрасно, но выглядит это не очень красиво. Вместо этого я хочу воспользоваться макросами, которые позволяет создавать класс Collection:

<?php

Collection::macro('present', function ($class) {
    return $this->map(function ($model) use ($class) {
        return new $class($model);
    });
});

Этот код можно поместить в сервис-провайдер вашего приложения. Теперь мы вызвать наш макрос, указав нужный презентер:

<?php

namespace AppHttpControllers;

use AppHttpControllersController;
use AppUsersPresentersApiPresenter;

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);
    }
}

Каждая модель оборачивается в презентер. Если хотите, вы можете передать коллекцию другому декоратору, чтобы объединить несколько объектов в единый JSON.

Таким образом декораторы и презентеры являются мощным инструментом, которым мы располагаем. Их легко писать и они легко тестируются. Используйте их, когда это имеет смысл. Они могут помочь вам при рефакторинге.

Но это еще не все. Было бы круто, если бы могли вызвать метод present для отдельной модели. И если бы у нас был хелпер, который позволил бы нам обернуть модель в презентер.

Что же, позвольте мне представить вам пакет Hemp/Presenter. Он делает все то, о чем мы говорили, плюс ко всему он реализует те пожелания, о которых я говорил. И все это протестировано. Попробуйте и расскажите мне, что вы думаете о нем. Наслаждайтесь!

Автор: muhammad_97

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js