Шаблон Presenter в Laravel

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


image


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


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


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


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


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


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


<?php

namespace App\Http\Controllers;

use App\Http\Controllers\Controller;

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

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


Я люблю тупые аналогии и это одна из них: декоратор это что-то вроде костюма Бэтмена на Брюсе Уейне. И у Бэтмена есть куча разных костюмов для разных ситуаций. Как и костюмы Бэтмена, мы можем использовать различные декораторы для разных ситуаций, где нам нужна модель User. Давайте переименуем наш декоратор во что-то более подходящее, например, ApiPresenter, а затем поместим его в папку Presenters. Так же мы выделим код, который можно переиспользовать, в отдельный класс 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};
    }
}

Давайте добавим новый метод к 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');
    }
}

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


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


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


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

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


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


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

Это замечательно! Теперь 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 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);
    }
}

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


<?php

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

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


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

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


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


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


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


Оригинал: http://davidhemphill.com/blog/2016/09/06/presenters-in-laravel

Поделиться публикацией

Комментарии 30

    0

    Похоже, вы не пометили пост как перевод и не указали источник. А за идеи, обозначенные в статье, благодарю)

      0
      Надеюсь что автор просто забыл пометить пост как перевод) На всякий случай я напомню, что оригинал тут http://davidhemphill.com/blog/2016/09/06/presenters-in-laravel/
        0
        Тут дело в том, что это публикация из Песочницы, у нас на данный момент нельзя писать в Песочницу «Переводы», только стандартные типы публикаций. Обычно в таких случаях указывают ссылку на оригинал хотя бы обычным текстом в конце публикации.
      +2
      Мне всегда казалось, что подобную проблему решает ViewModel и DTO, но не Presenter
        +1
        Очень странная реализация подхода с кучей кода в контроллере.
        Давайте теперь добавим отчество, вы будете каждый контроллер править добавляя 'middle_name' => $user->middle_name?
          0
          Функционал скорее напоминает Symfony Serializer.
            0
            Использовали fractal для этих целей
              0
              строго говоря, то что Вы сделали, это не паттерн Декоратор, а паттерн Заместитель (Proxy)
              ну а в целом да, спасибо, думаю многим будет полезно узнать, что можно изменять/расширять поведение и так
                0
                строго говоря, Вы не правы и презентер в статье это именно Декоратор, а не Прокси.
                  0
                  пересмотрел, согласен, всё-таки декоратор, простите мою невнимательность
                    +1
                    Нет, вы были скорее правы.
                    Декоратор предполагает обёртывание объекта с сохранением интерфейса и предпологает обёртывание существующих методов, здесь интерфейса вообще нет, плюс добавляются новые методы. А здесь действительно прокси, так как проктирует обращение на модель, тем более магический __call выполняет функцию паблика морозова, нарушая инкапсуляцию, что недопустимо даже для презентера.
                      0
                      тут на самом деле смесь того и того, с уклоном больше в Декоратор
                        0
                        Вы тоже всё напутали. Именно Proxy дает доступ к внутреннему объекту с сохранением интерфейса но с например выполнением внутри дополнительной логики. А Decorator дает дополнительный функционал, т.е. имеет свой интерфейс.
                        т. е.
                        предполагает обёртывание объекта с сохранением интерфейса и предпологает обёртывание существующих методов

                        Вот это как раз Прокси
                          +1
                          Они оба предполагают соблюдение интерфейса, но цель разная. Прокси контролирует объект, а Декоратор предоставляет возможность _динамического_ изменения поведения.
                            0
                            Соблюдение — да. Но как вы сами и сказали Декоратор дает дополнительное поведение.

                            Если кратко то вот:
                            Adapter предоставляет к своему объекту другой интерфейс.
                            Proxy предоставляет тот же интерфейс, управляя обращениями к вложенному объекту.
                            Decorator предоставляет расширенный интерфейс.
                              +1
                              Вот, _расширять_. Потому что наличие интерфейса согласуется с принципом лискоу. При этом слово расширять — лучше понимать как наследовать несколько интерфейсов, соблюдая принцип разделения интерфейсов.
                              Я сам об это споткнулся пару лет назад, пришлось ставить костыли из-за наличия __call и обратной совместимости вместо нормально интерфейса.
                                –2
                                Ну вот так в Ларе это работает. Отойдите вы от понятий php и от конкретной реализации. Для модельки ее возвращаемые поля можно вполне считать интерфейсом. Презентер же берет и добавляет возможность взять еще какие то данные, сохраняя доступ к старым. И возвращаясь к теме ветки обсуждения это как раз Декоратор.
                                  +1
                                  Нет, вам следует убрать магический __call и тогда это будет презентер или viewmodel, а так это паблик морозов, который позволяет делать $object->delete() в слое представления.
                                    –1
                                    А типо, я до этого так сделать не мог? Уж извините это дает мне AR.
                                    Вообще Вы что то мешаете всё в кучу. Начинался диалог с опровержения определения структурного паттерна. Давайте по пунктам.
                                    1. То что в статье (какое бы оно ни было) это Декоратор. Он именно сохранил всё что умел предыдущий объект. И добавил то что тот не умел.
                                    2. Нет это никакой не «паблик морозов» (хотя я вообще не уверен можно ли дискутировать про шуточный антипаттерн). Он не позволит обратиться к закрытым методам внутреннего объекта. Он им был бы если бы враппер (использовано в общем смысле) был бы наследником или того же класса что и внутренний объект.
                                    3. В статье «презентор» не имеет отношения к MVP. Так же как и ваша попытка притянуть сюда архитектурный паттерн MVVM а точнее его часть ViewModel.
                                    4. Стоит ли убрать магический call из конкретно таких презентеров? Да возможно стоит.
                                      +1
                                      Это вы смешали.
                                      1. Нет, это декоратор, он сломал принцип лискоу, так как не сломал интерфейсы и не пройдёт тайпхинтинг.
                                      2. Это паблик морозов, т.к. у вас model — протектид, а __call предоставляет к нему доступ как к публичному.
                                      3. В статье недоразумение, а не паттерн, он нарушает сразу несколько солид принципов, о чём и был смысл данного обсуждения.
                                      4. Обязательно уберите и код из контроллера тоже.
                                        –2
                                        Я мог бы еще с вами спорить (даже писать начал) но мне лень. Я считаю что Вы не правы почти по всем пунктам.
                            0
                            AmdY в общем-то у декоратора и декорируемого один и тот же интерфейс должен быть, но расширять они его могут без проблем
                            image
                              0
                              Да, декораторы могут наследовать несколько интерфейсов, но не плодить свои. Это всё же динамический паттерн.
                    0
                    Не понимаю зачем добавлять лишний слой абстракции для Laravel когда он сам изначально реализует данный шаблон.
                    Eloquent: Accessors & Mutators

                    А если нужно приводить что-то к JSON или массиву — этот механизм тоже имеется у Eloquent.
                    Eloquent: Serialization

                      0
                      Потому что Так как Модели в Ларе это AR то они и так часто перегружены логикой. И порой хочется чуток их разгрузить. И тут кто во что горазд) Сериализация всё же мне нравится больше через уже упомянутый fractal. А вот презенторы приятно использовать как часть view layer.
                        –1
                        Модели в Ларе это AR то они и так часто перегружены логикой

                        Потому что у фреймворков дебильное понимание MVC. :)
                        Но мне говорят постоянно, что я работал только с Yii, хотя это касается всех (мейнстримовых) :)
                          –1
                          С кем то другим я бы подискутировал на тему AR и хорошо это или плохо, но только не с вами.
                      –1
                      Выше уже написал для чего «нравятся» презенторы. И вот решил проиллюстрировать: Gist
                        –1
                        public function __call($method, $args)
                        {
                        return call_user_func_array([$this->model, $method], $args);
                        }


                        Мне тут ребята-фреймворщики рассказывали, что такое делать в самописи нельзя, типа не используем интерфейсы и все такое:
                        https://habrahabr.ru/company/mailru/blog/308788/

                        Что ж Вы такое советуете? :)
                          0
                          Я презентеры делаю, но не так. Ваш пример я назову anemic presenter :) по сути набор хелпер-функций.

                          У меня обычно получается в контроллере что-то вида

                          return ['fooBar' => $fooBarPresenter->render($foo, $bar)]; // ну или $view...->with(...);

                          А вся логика представления для данного блока находится в FooBarPresenter.

                          Презентеры могут внутри создавать другие презентеры — как для комбинаций, так и для коллекций.

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

                          Самое читаемое