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




В таких ситуациях мы можем воспользоваться паттерном Декоратор, который позволит нам выделить код, специфичный для каждого случая в отдельный класс. Например, мы можем использовать декораторы для того, чтобы разделить формирование представления для 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)


  1. franzose
    14.09.2016 07:00

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


    1. ellrion
      14.09.2016 11:47

      Надеюсь что автор просто забыл пометить пост как перевод) На всякий случай я напомню, что оригинал тут http://davidhemphill.com/blog/2016/09/06/presenters-in-laravel/


      1. Sterhel
        14.09.2016 11:49

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


  1. perfectdaemon
    14.09.2016 07:52
    +2

    Мне всегда казалось, что подобную проблему решает ViewModel и DTO, но не Presenter


  1. AmdY
    14.09.2016 10:17
    +1

    Очень странная реализация подхода с кучей кода в контроллере.
    Давайте теперь добавим отчество, вы будете каждый контроллер править добавляя 'middle_name' => $user->middle_name?


  1. rsvasilyev
    14.09.2016 10:33

    Функционал скорее напоминает Symfony Serializer.


  1. Fantyk
    14.09.2016 10:50

    Использовали fractal для этих целей


  1. andrewnester
    14.09.2016 11:14

    строго говоря, то что Вы сделали, это не паттерн Декоратор, а паттерн Заместитель (Proxy)
    ну а в целом да, спасибо, думаю многим будет полезно узнать, что можно изменять/расширять поведение и так


    1. ellrion
      14.09.2016 11:51

      строго говоря, Вы не правы и презентер в статье это именно Декоратор, а не Прокси.


      1. andrewnester
        14.09.2016 12:30

        пересмотрел, согласен, всё-таки декоратор, простите мою невнимательность


        1. AmdY
          14.09.2016 12:41
          +1

          Нет, вы были скорее правы.
          Декоратор предполагает обёртывание объекта с сохранением интерфейса и предпологает обёртывание существующих методов, здесь интерфейса вообще нет, плюс добавляются новые методы. А здесь действительно прокси, так как проктирует обращение на модель, тем более магический __call выполняет функцию паблика морозова, нарушая инкапсуляцию, что недопустимо даже для презентера.


          1. andrewnester
            14.09.2016 12:42

            тут на самом деле смесь того и того, с уклоном больше в Декоратор


          1. ellrion
            14.09.2016 12:48

            Вы тоже всё напутали. Именно Proxy дает доступ к внутреннему объекту с сохранением интерфейса но с например выполнением внутри дополнительной логики. А Decorator дает дополнительный функционал, т.е. имеет свой интерфейс.
            т. е.

            предполагает обёртывание объекта с сохранением интерфейса и предпологает обёртывание существующих методов

            Вот это как раз Прокси


            1. AmdY
              14.09.2016 13:03
              +1

              Они оба предполагают соблюдение интерфейса, но цель разная. Прокси контролирует объект, а Декоратор предоставляет возможность _динамического_ изменения поведения.


              1. ellrion
                14.09.2016 13:15

                Соблюдение — да. Но как вы сами и сказали Декоратор дает дополнительное поведение.

                Если кратко то вот:
                Adapter предоставляет к своему объекту другой интерфейс.
                Proxy предоставляет тот же интерфейс, управляя обращениями к вложенному объекту.
                Decorator предоставляет расширенный интерфейс.


                1. AmdY
                  14.09.2016 13:46
                  +1

                  Вот, _расширять_. Потому что наличие интерфейса согласуется с принципом лискоу. При этом слово расширять — лучше понимать как наследовать несколько интерфейсов, соблюдая принцип разделения интерфейсов.
                  Я сам об это споткнулся пару лет назад, пришлось ставить костыли из-за наличия __call и обратной совместимости вместо нормально интерфейса.


                  1. ellrion
                    14.09.2016 14:01
                    -2

                    Ну вот так в Ларе это работает. Отойдите вы от понятий php и от конкретной реализации. Для модельки ее возвращаемые поля можно вполне считать интерфейсом. Презентер же берет и добавляет возможность взять еще какие то данные, сохраняя доступ к старым. И возвращаясь к теме ветки обсуждения это как раз Декоратор.


                    1. AmdY
                      14.09.2016 14:10
                      +1

                      Нет, вам следует убрать магический __call и тогда это будет презентер или viewmodel, а так это паблик морозов, который позволяет делать $object->delete() в слое представления.


                      1. ellrion
                        14.09.2016 14:51
                        -1

                        А типо, я до этого так сделать не мог? Уж извините это дает мне AR.
                        Вообще Вы что то мешаете всё в кучу. Начинался диалог с опровержения определения структурного паттерна. Давайте по пунктам.
                        1. То что в статье (какое бы оно ни было) это Декоратор. Он именно сохранил всё что умел предыдущий объект. И добавил то что тот не умел.
                        2. Нет это никакой не «паблик морозов» (хотя я вообще не уверен можно ли дискутировать про шуточный антипаттерн). Он не позволит обратиться к закрытым методам внутреннего объекта. Он им был бы если бы враппер (использовано в общем смысле) был бы наследником или того же класса что и внутренний объект.
                        3. В статье «презентор» не имеет отношения к MVP. Так же как и ваша попытка притянуть сюда архитектурный паттерн MVVM а точнее его часть ViewModel.
                        4. Стоит ли убрать магический call из конкретно таких презентеров? Да возможно стоит.


                        1. AmdY
                          14.09.2016 16:26
                          +1

                          Это вы смешали.
                          1. Нет, это декоратор, он сломал принцип лискоу, так как не сломал интерфейсы и не пройдёт тайпхинтинг.
                          2. Это паблик морозов, т.к. у вас model — протектид, а __call предоставляет к нему доступ как к публичному.
                          3. В статье недоразумение, а не паттерн, он нарушает сразу несколько солид принципов, о чём и был смысл данного обсуждения.
                          4. Обязательно уберите и код из контроллера тоже.


                          1. ellrion
                            14.09.2016 17:04
                            -2

                            Я мог бы еще с вами спорить (даже писать начал) но мне лень. Я считаю что Вы не правы почти по всем пунктам.


            1. andrewnester
              14.09.2016 13:06

              AmdY в общем-то у декоратора и декорируемого один и тот же интерфейс должен быть, но расширять они его могут без проблем
              image


              1. AmdY
                14.09.2016 14:01

                Да, декораторы могут наследовать несколько интерфейсов, но не плодить свои. Это всё же динамический паттерн.


  1. true2trance
    14.09.2016 11:47

    Не понимаю зачем добавлять лишний слой абстракции для Laravel когда он сам изначально реализует данный шаблон.
    Eloquent: Accessors & Mutators

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


    1. ellrion
      14.09.2016 11:57

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


      1. G-M-A-X
        14.09.2016 13:45
        -1

        Модели в Ларе это AR то они и так часто перегружены логикой

        Потому что у фреймворков дебильное понимание MVC. :)
        Но мне говорят постоянно, что я работал только с Yii, хотя это касается всех (мейнстримовых) :)


        1. ellrion
          14.09.2016 13:57
          -1

          С кем то другим я бы подискутировал на тему AR и хорошо это или плохо, но только не с вами.


  1. ellrion
    14.09.2016 12:15
    -1

    Выше уже написал для чего «нравятся» презенторы. И вот решил проиллюстрировать: Gist


  1. G-M-A-X
    14.09.2016 13:43
    -1

    public function __call($method, $args)
    {
    return call_user_func_array([$this->model, $method], $args);
    }


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

    Что ж Вы такое советуете? :)


  1. symbix
    14.09.2016 18:43

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

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

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

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

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