Пайплайны в Laravel. Часть 2 — Пайпы

Laravel Pipes

Во второй части мы поговорим о Пайпах (Pipes). Они являются вторым, по важности, объектом Пайплайна. Как рассказывалось в предыдущей статье, в качестве пайпа мы можем использовать экземпляр объекта, invokable класс или имя класса, который должен быть определен и инстанцирован через сервис-контейнер.

Использовать имя класса очень полезно, когда работа вашего Пайпа зависит от Сервиса. Допустим, Пайпу нужна база данных для сравнения чего-либо или Кэш для временного хранения какого-либо результата, чтобы ускорить логику. Пайплайн при инстанцировании автоматически отрезолвит все нужные зависимости.

Базовый классический Пайп может выглядеть так:

namespace App\Pipes;

use Closure;
use Illuminate\Contracts\Cache\Store as Cache;

class Pipe
{
    /**
     * Дефолтный кэш
     *
     * @var \Illuminate\Contracts\Cache\Store
     */
    protected $cache;

    /**
     * Создает новый экземпляр Пайпа
     *
     * @return void
     */
    public function __construct(Cache $cache)
    {
      $this->cache = $cache;
    }
    
    /**
     * Переводит первый символ строки в верхний регистр
     *
     * @param string $string
     * @param Closure $next
     * @return mixed
     */
    public function handle($string, Closure $next)
    {
        $string = $this->cache->remember('string|' . md5($string), 60, function () use ($string) {
            return ucfirst($string);
        });
        
        return $next(ucfirst($string));
    }
}

Обработка данных

Когда Паплайн вызывает каждый пайп, то тот получает два аргумента: передаваемые данные и замыкание.

public function handle($passable, Closure $next)
{
    // Ваша логика
}

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

public function handle($passable, Closure $next)
{
    $passable = ucfirst($passable);
}

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

Для этого мы вызываем Замыкание, представляющее собой следующий пайп в конвейере, и передаем ему модифицированные данные в качестве аргумента.

public function handle($passable, Closure $next)
{
    $passable = ucfirst($passable);    
    
    return $next($passable);
}

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

Прыжок в конец

Один из магических трюков пайпа заключается в том, что он может автоматически «прыгать» в конец конвейера. Здесь его работа очень похожа на After Middleware: мы выполним наш код после того, как весь конвейер дойдет до конечного результата.

public function handle($passable, Closure $next)
{
    $result = $next($passable);
    
    // Сделать что-нибудь с результатом
    
    return $result;
}

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

Магия аргументов

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

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

Для этого можно использовать строку с синтаксисом Namespace\Class:foo,bar. Это заставит пайп инстанцироваться через сервис-контейнер (даже если он не использует какой-либо его сервис) и передаст их начиная с третьего аргумента. Парсинг выполняется автоматически самим классом Pipeline.

$result = app(\Illuminate\Pipeline\Pipeline::class)
    ->send(’this should be correctly formatted’)
    ->through([
        // ...
        'App\Strings\Pipes\RemoveWords:should,formatted'
     ])->thenReturn();
     
echo $result;  // "this be correctly"

Затем мы можем использовать аргументы в нашем классе RemoveWords:

/**
 * Удаляет список слов
 *
 * @param string $string
 * @param Closure $next
 * @param array $remove
 * @return string
 */
public function handle($string, Closure $next, ...$remove)
{
    return $next(str_replace($remove, '', $string));
}

Если класс был зарегистрирован в сервис-контейнере с удобным именем, например, image, replacer или remover, то мы можем использовать это имя вместо Class:

$result = app(\Illuminate\Pipeline\Pipeline::class)
    ->send('this should be correctly formatted')
    ->through([
        // ...
        'replacer:should,formatted'
     ]);

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

Route::get('podcast/{podcast}', 'PodcastController@show')
    ->middleware('auth:api');

Совет по работе с ответами (Responses)

Первоначально Пайплайн был создан для обработки HTTP-ответа,  но мой PR в Laravel 6 позволил ему обрабатывать что угодно. Он также стал более гибким в плане отладки и обработки исключений.

Примите во внимание, что, в Laravel 5.8, если вы используете класс Pipeline, а исходящий объект является экземпляром Responsable, то он будет автоматически преобразован в Response для следующей пайпа. Другими словами, вы должны избегать использования экземпляра Responsable, когда вы пользуетесь Пайплайном:

return $response instanceof Responsable 
    ? $response->toResponse($this->getContainer()->make(Request::class))
    : $response;

К счастью мой PR был одобрен и в новых версиях фреймворка больше нет необходимости избегать использования Responsable.

Сегодня мы поговорили о Пайпах, а следующая статья будет посвящена Хабам.

Автор: Italo Baeza Cabrera
Перевод: Алексей Широков

Наш Телеграм-канал — следите за новостями о Laravel.