Пайплайны в Laravel. Часть 3 — Хабы

Пайплайны в Laravel. Часть 3 — Хабы

Итак, мы в предыдущих статьях мы узнали, как использовать Пайплайн. Теперь настало время познакомиться с Хабом (Hub).

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

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

namespace Illuminate\Pipeline;

use Closure;
use Illuminate\Contracts\Container\Container;
use Illuminate\Contracts\Pipeline\Hub as HubContract;

class Hub implements HubContract
{
    /**
     * Реализация контейнера
     *
     * @var \Illuminate\Contracts\Container\Container|null
     */
    protected $container;

    /**
     * Все доступные пайплайны
     *
     * @var array
     */
    protected $pipelines = [];

    /**
     * Создание нового экземпляра Хаба
     *
     * @param  \Illuminate\Contracts\Container\Container|null  $container
     * @return void
     */
    public function __construct(Container $container = null)
    {
        $this->container = $container;
    }

    /**
     * Определение дефолтного пайплайна
     *
     * @param  \Closure  $callback
     * @return void
     */
    public function defaults(Closure $callback)
    {
        return $this->pipeline('default', $callback);
    }

    /**
     * Определение нового пайплайна
     *
     * @param  string  $name
     * @param  \Closure  $callback
     * @return void
     */
    public function pipeline($name, Closure $callback)
    {
        $this->pipelines[$name] = $callback;
    }

    /**
     * Отправление объекта по одному из имеющихся пайплайнов
     *
     * @param  mixed  $object
     * @param  string|null  $pipeline
     * @return mixed
     */
    public function pipe($object, $pipeline = null)
    {
        $pipeline = $pipeline ?: 'default';

        return call_user_func(
            $this->pipelines[$pipeline], new Pipeline($this->container), $object
        );
    }
}

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

Метод pipeline() позволит вам зарегистрировать Пайплайн по имени, по которому вы можете использовать его позже. Метод defaults() будет содержать Паплайн, который будет использоваться по дефолту, если вы не указали конкретный Пайплайн.

$hub = app(\Illuminate\Pipeline\Hub::class);

$hub->defaults(function ($pipeline, $object) {
    return $pipeline
        ->send($object)
        ->through([
            EncodeToHtml::class
            AddPunctutation::class,
            RemoveDuplicatesCharacters::class,
            UppercaseFirstCharacterOfEachPhrase::class,
        ])
        ->thenReturn();
});

$hub->pipeline('encode-only', function ($pipeline, $object) {
    return $pipeline
        ->send($object)
        ->through(EncodeToHtml::class)
        ->thenReturn();
});

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

В нашем коде мы можем просто использовать метод pipe() для передачи объекта в пайплайн. Если мы не введем имя в качестве второго аргумента, то будет использоваться дефолтный пайплайн.

/**
 * Обновление Статьи (Article)
 *
 * @param \Illuminate\Http\Request $request
 * @param \App\Article $article
 * @param \Illuminate\Pipeline\Hub $hub
 * @return \Illuminate\Http\Response
 */
public function save(Request $request, Article $article, Hub $hub)
{
    $request->validate([
        'message' => 'required|string',
    ]);    
    
    $article->message = $hub->pipe($request->message);    
    
    $article->save();    
    
    return response()
        ->view('article.saved')
        ->with('article', $article);
}

Как вы уже поняли, применение Хаба может быть очень удобным.

Что насчёт сервис-провайдера?

Да, один есть — PipelineServiceProvider. Он делает только одно: регистрирует синглтон класса Hub. Другими словами, каждый раз, когда приложению требуется класс, реализующий HubContract, он резолвит экземпляр класса Hub этого пакета.

Обратите внимание на кое-что очень специфическое: этот PipelineServiceProvider нигде не зарегистрирован. То есть ваше приложение загружается, а сервис-провайдер не подхватывается. И тут уже зависит от разработчика, регистрировать ли его в массиве providers в файле app.php, нужно ли это ему.

Например, если вы используете только один Хаб, регистрация сервис-провайдера позволит нам вызывать его откуда угодно и получать всё тот же Хаб, с готовыми пайплайнами. Мы могли бы использовать метод register() из AppServiceProvider, чтобы «расширить» экземпляр Hub с помощью наших собственных папйлайнов. Просто для иллюстрации:

/**
 * Регистрация любого сервиса приложения
 *
 * @return void
 */
public function register()
{
    $this->app->extend(
        \Illuminate\Contracts\Pipeline\Hub::class,
        function ($hub) {
            $hub->defaults(function ($pipeline, $object) {
                return $pipeline
                    ->send($object)
                    ->through(\App\Pipelines\EncodeToHtml::class)
                    ->thenReturn();
            });

            return $hub;
        }
     )
}

Но если вам нужно использовать несколько Хабов, каждый из которых имеет дело со множеством данных, то лучше всего создать свой собственный. В перспективе, ArticleHub будет иметь пайплайны для стандартной санитизации статьи и её превью, а CommentHub — строгой санитизации комментариев и имен пользователей.

/**
 * Регистрация любого сервиса приложения
 *
 * @return void
 */
public function register()
{
    $this->app->singleton(\App\Hubs\ArticleHub::class);
    $this->app->singleton(\App\Hubs\CommentHub::class);
}

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

namespace App\Hubs\ArticleHub;

use Illuminate\Pipeline\Hub;
use App\Pipelines\EncodeToHtml;
use App\Pipelines\AddPunctutation;
use App\Pipelines\RemoveDuplicatesCharacters;
use App\Pipelines\UppercaseFirstCharacterOfEachPhrase;

class ArticleHub extends Hub
{
    /**
     * Отправляем объект по одному из имеющихся пайплайнов
     *
     * @param  mixed  $object
     * @param  string|null  $pipeline
     * @return mixed
     */
    public function pipe($object, $pipeline = null)
    {
        // Если пайплайн был создан, но не зарегистрирован, то мы вызовем метод для его создания
        if ($pipeline && ! isset($this->pipelines[$pipeline])) {
            $this->{'register' . ucfirst($pipeline)}();
        }
        
        return parent::pipe($object, $pipeline);
    }

    /**
     * Регистрация пайплайна для статьи
     *
     * @return void
     */
    protected function registerBody()
    {
         $this->pipeline('body', function ($pipeline, $object) {
            return $pipeline->send($object)
                ->through([
                    EncodeToHtml::class
                    AddPunctutation::class,
                    RemoveDuplicatesCharacters::class,
                    UppercaseFirstCharacterOfEachPhrase::class,
                ])
                ->thenReturn();
        });
    }

    /**
     * Регистрация пайплайна для превью статьи
     *
     * @return void
     */
    protected function registerExcerpt()
    {
        $this->pipeline('excerpt', function ($pipeline, $object) {
            return $pipeline->send($object)
                ->through([
                    EncodeToHtml::class
                    RemoveDuplicatesCharacters::class,
                    function($excerpt, $next) {
                        return $next(ucfirst($excerpt));
                    }
                ])
                ->thenReturn();
        });
    }
}

Затем,в коде, вы просто вызываете пайплайны Хаба через pipeline(), с данными и именем Пайплайна.

/**
 * Обновление Статьи
 *
 * @param \Illuminate\Http\Request $request
 * @param \App\Article $article
 * @param \App\Hubs\ArticleHub $hub
 * @return \Illuminate\Http\Response
 */
public function save(Request $request, 
                     Article $article, 
                     ArticleHub $hub)
{
    $request->validate([
        'message' => 'required|string',
        'excerpt' => 'required|string|max:255'
    ]);    
    
    $article->message = $hub->pipe($request->message, 'body');
    $article->excerpt = $hub->pipe($request->excerpt, 'excerpt');    
    
    $article->save();    
    
    return response()
        ->view('article.saved')
        ->with('article', $article);
}

На этом про Хабы, всё!

В следующей статье мы залезем Пайплайнам под капот.

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

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