Нетрадиционный Laravel: ответственные классы

Иногда ваши контроллеры могут стать довольно большими, особенно если у вас есть много условий к форматированию данных. Вы когда-нибудь думали о том, чтобы перенести часть этой сложности в специальный класс ответов?

Эта статья является частью нетрадиционной серии Laravel.

В вашем типичном приложении Laravel вы, вероятно, привыкли использовать вспомогательные методы Laravel для возврата ответов.

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

Вот пример:

class PostController
{
    public function index()
    {
        return view('posts.index', [
            'post' => Post::published()->get(),
        ]);
    }
}

Мы рассмотрим, как такой контроллер может расти внутри приложения, а также как мы можем расширить эту концепцию и превратить приведенный выше код в приведенный ниже фрагмент кода:

class PostController
{
    public function index()
    {
        $posts = Post::published()->get();
        
        return new PostIndexResponse($posts);
    }
}

Согласование контента

В некоторых приложениях имеет смысл иметь один маршрут как для ваших HTML-ответов, так и для ваших API-ответов.

Этот метод известен как “согласование содержания”. Вы проверяете, какой запрос был сделан, и отправляете назад определенный тип ответа, основанный на этом типе запроса.

Возьмем пример выше. Если бы я хотел вернуть некоторый JSON, когда запрос требует такого типа контента, я бы сделал следующее:

class PostController
{
    public function index(Request $request)
    {
        $posts = Post::published()->get();
        
        if ($request->wantsJson()) {
            return $posts;
        }
        
        return view('posts.index', [
            'post' => $posts,
        ]);
    }
}

Благодаря методу Laravel Request::wantsJson() вы можете легко проверить, нужно ли возвращать JSON или HTML в ответе.

Для небольших сценариев, подобных этому, пользовательские классы ответов не принесут вам большой пользы. Есть только 2 различных условия, которые определяют тип ответа, который должен быть возвращен.

Давайте воспользуемся гипотетическим Request::wantsRss()методом.

class PostController
{
    public function index(Request $request)
    {
        $posts = Post::published()->get();
        
        if ($request->wantsRss()) {
            return RssFeed::from($posts)->create();
        }
        
        if ($request->wantsJson()) {
            return $posts;
        }
        
        return view('posts.index', [
            'post' => $posts,
        ]);
    }
}

По мере добавления все большего количества этих различных типов контента метод будет становиться все более и более переполненным. Давайте попробуем решить эту проблему с помощью пользовательских классов ответов.

Первый шаг к созданию пользовательского класса ответов-это реализация Responsable интерфейса и перемещение некоторой части нашей логики ответов в метод toResponse.

class PostIndexResponse implements Responsable
{
    public function toResponse(Request $request)
    {
        if ($request->wantsRss()) {
            return RssFeed::from($posts)->create();
        }
        
        if ($request->wantsJson()) {
            return $posts;
        }
        
        return view('posts.index', [
            'post' => $posts,
        ]);
    }
}

Первая проблема заключается в том, что наша переменная $posts больше не существует. К счастью, мы можем добавить конструктор в класс и присвоить ему свойство.

class PostIndexResponse implements Responsable
{
    private $posts;
    
    public function __construct($posts)
    {
        $this->posts = $posts;
    }
    
    public function toResponse(Request $request)
    {
        // ...
    }
}

Если мы вернемся к нашему контроллеру, то сможем заменить все эти if операторы одним экземпляром нашего нового PostIndexResponse класса.

class PostController
{
    public function index(Request $request)
    {
        $posts = Post::published()->get();
        
        return new PostIndexResponse($posts);
    }
}

Создание базового класса ответов

Логика, которую мы только что абстрагировали в отдельный класс, все еще немного длинна и утомительна для написания каждый раз.

Один из способов обойти это – создать абстрактный BaseResponse класс, который содержит эту логику.

Вот супер простая версия:

abstract class BaseResponse implements Responsable
{
    public function toResponse($request)
    {
        if ($request->wantsJson()) {
            return $this->toJson();
        }
        
        return $this->toHtml();
    }
}

Логика аналогична предыдущему классу, но теперь мы будем использовать некоторые обычные методы именования.

Для запросов, ожидающих ответа JSON, toJson метод следует использовать в дочернем классе. По умолчанию он будет использовать toHtml метод, так что вы можете вернуть представление отсюда или экземпляр HtmlString.

Давайте возьмем этот новый абстрактный класс и применим его к нашему PostIndexResponse.

class PostIndexResponse extends BaseResponse
{
    private $posts;
    
    public function __construct($posts)
    {
        $this->posts = $posts;
    }
    
    public function toRss()
    {
        return RssFeed::from($this->posts)->create();
    }
    
    public function toJson()
    {
        return $this->posts;
    }
    
    public function toHtml()
    {
        return view('posts.index', [
            'posts' => $this->posts,
        ]);
    }
}

Добавляя немного магии

Если бы я хотел добавить новое условие wantsPng в свой BaseResponse класс, мне нужно было бы войти в метод toResponse, проверить его, добавить новый метод. Это утомительно, когда вы используете много различных типов контента, так почему бы нам не добавить немного магии и к этому.

Я собираюсь пойти по пути предположения, что всегда есть wants{format}метод для объекта запроса. Это, вероятно, будет иметь место, особенно если вы используете проверку в других местах вашего приложения, вы, вероятно, захотите макросить ее.

abstract class BaseResponse implements Responsable
{
    protected $accepts = [
        'json', 'rss', 'png', 'jpg',
    ];

    public function toResponse($request)
    {
        foreach ($this->accepts as $accept) {
            $requestMethod = 'wants'.Str::studly($accept);
            $responseMethod = 'to'.Str::studly($accept);
          
            if ($request->{$requestMethod}()) {
                return $this->{$responseMethod}();
            }
        }
      
        return $this->toHtml();
    }
}

Теперь вместо того, чтобы писать условие самостоятельно, вы можете просто добавить новый элемент в свойство $accepts, определить to{format} метод с помощью StudlyCase, и он должен “просто работать”.

Плюсы

Тонкие контроллеры

Одна из многих “лучших практик” сообщества Laravel-это всегда иметь тонкие контроллеры. Это просто означает, что ваши контроллеры всегда легкие и небольшие, что приводит к тому, что много логики перемещается наружу в модели и действия или в нашем случае, пользовательские классы ответов.

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

Ясность кода

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

Если вы знаете имя текущего маршрута / контекста или можете описать то, что вы просматриваете, при условии, что ваши пользовательские классы ответов названы соответствующим образом, вы можете выполнить быстрый поиск и найти именно то, что вы ищете.

Минусы

Скрытая логика

Несмотря на то, что я говорю, что ясность кода является профи для этого шаблона, вы также можете сделать раннюю абстракцию, то есть ваша логика и поток запросов скрыты под именованным объектом.

Для небольших проектов это может быть проблематично, особенно если вы не работаете над ними 24/7 и знаете структуру как свои пять пальцев.

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

Обслуживание принятых типов

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

С каждым типом, который вы добавляете, вы должны помнить, как он работает, что должно происходить с реакцией и откуда вообще берется эта магия.

Неожиданные ошибки

Если кто-то сделает запрос на ваш маршрут с запросом на неожиданный тип контента, ваш код, вероятно, не будет иметь никакой поддержки для него. Это означает, что пользователь получит неприятную ошибку 500, а вы будете сидеть и отлаживать проблему. Что еще хуже, в зависимости от типа ответа по умолчанию вы даже можете утечь данные, потому что он всегда будет возвращать JSON или HTML.

Убедитесь, что вы правильно защищаете свои маршруты, или ужесточите базовый класс ответов, чтобы по умолчанию возвращать пустой ответ.

Спасибо за чтение!

Рейтинг
( Пока оценок нет )
Maxyc Webber/ автор статьи
Мне 35 лет. Опыт профессиональной разработки 15 лет. Занимаюсь разработкой и поддержкой корпоративных систем автоматизации бизнеса, а также высоконагруженными проектами. Мне нравится решать нестандартные проблемы бизнеса. Имею опыт формирования команд под проект, налаживания процесса разработки, коммуникации программистов и заказчиков. Есть опыт работы с зарубежными заказчиками (ОАЭ, Польша, Германия, Швейцария).
Понравилась статья? Поделиться с друзьями:
Добавить комментарий

;-) :| :x :twisted: :smile: :shock: :sad: :roll: :razz: :oops: :o :mrgreen: :lol: :idea: :grin: :evil: :cry: :cool: :arrow: :???: :?: :!:

Этот сайт использует Akismet для борьбы со спамом. Узнайте, как обрабатываются ваши данные комментариев.