Сервис-провайдеры в laravel для самых маленьких

Для меня достаточно стандартная история, когда для понимания чего-либо, естественно сложнее отвертки, приходится долго и много изучать. Зато потом наступает миг просветления и счастья и такой: «Ну ё-моё! Семён Семенёч! Как же это все просто и самое главное удобно». С ларавельскими сервис-провайдерами была такая же история. Концепция-то простая как кирпич, но я перечитал полинтернета. Хочется верить, что я не один такой ущербный и кому-нибудь мой опус станет отправной точко =) Ну так начнем-с.

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

Лично я потратил 2 дня за курением доков на русском, английском, несколько туториалов переведенных с совсем нерусского на менее нерусский, это когда чуваки пишут русскими буквами, но в русские несмогли. Попадались несколько на немецком и парочка на испанском, там только смотреть на код можно, благо код — интернациональен! Проблема всех официальных и около оффициальных мануалов — они копируют друг друга. И задача в тысяче источников рассматривается однобоко.  А ещё я посмотрел несколько видео обучалок, ненавижу видео-туторы, это полчаса времени про то, что можно в 5 абзацев вместить. Это я и попробую сделать.

Понятийный аппарат

Главная проблема — нифига не понятно кто это такие Сервис-провайдеры и как сними работать.

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

А вот и ещё одно прикольное понятие Сервис-контейнер. Попробую на пальцах — это «ящик», в котором хранятся все зарегистрированные сервисы, чтобы быстро к ним доступаться это раз, а самое главное:

Сервис-контейнер в Laravel — это мощное средство для управления зависимостями классов и внедрения зависимостей.

Так нам рассказывает официальная документация. Если не совсем понятно, то лучше бы тебе, милый дружок, почитать обзорные статьи на понятия контейнер внедрения зависимостей. Если очень грубо: иногда наш объект должен иметь доступ к чему-то из вне. Например, есть объект пользователь, который учится в школе. Можно все поля связанные со школой уместить внутрь пользователя. Но это, очевидно, будет избыточно, 2 разных пользователя могут учиться в одной школе, поэтому создаем отдельный объект школа. А чтобы пользователь мог получить инфу о школе, мы в конструктор передаем зависимость от некоего хранилища школы, интерфейс, который по запросу может вернуть объект школы. Т.е. теперь при создании пользователя он сможет дергать внешнее хранилище и получать требуемые данные. Таким образом объект замкнут сам в себе. Понимаешь?! Это как нарисовать сову! Ой, всё! Давай потом на эту тему поговорим.

Вернемся к нашему предмету.Сервис-контейнер хранит внутри себя некие сервисы. А что такое сервис? А вот это как раз тот самый функциональный блок, который нам нужен. Отправка смс, вывод статистики по чему-либо и т.д. Все зарегистрированные сервисы кэшируются в /var/www/bootstrap/cache/services.php, можно глянуть кто и что туда добавились

Нафига это надо? Можно ж по старинке всё делать, нужен класс, дернул, не нужен не дернул. Так-то оно так, но одно дело когда перфоратор у тебя на балконе лежит, ты пошел и взял, а другое дело, когда он в гараже, у отца, в другом городе, в Белоруссии, а ты в Гремании. Ну ты понял. Скорость и удобство.

Кодовое выражение

Допустим, мы поняли что такое сервис-провайдеры. А как с ними жить и как это выражается в печатных символах в любимой IDE’шке?

Есть короткий ответ, вот так:

namespace App\Providers;

use Riak\Connection;
use Illuminate\Support\ServiceProvider;

class RiakServiceProvider extends ServiceProvider
{
    /**
     * Register bindings in the container.
     *
     * @return void
     */
    public function register()
    {
        $this->app->singleton(Connection::class, function ($app) {
            return new Connection(config('riak'));
        });
    }
}

Именно так написано в документации. И ты такой: «Аааа! Тут же вон оно… И чё?» А я был готов к этому вопросу, не зря ж уже 2ю неделю пишу этот опус.

Собственно, мы говорим: Мне нужно, чтобы в сервис контейнере лежал экземпляр класса Riak\Connection (за это отвечает строка — return new Connection(config(‘riak’))) с именем равным исходному имени класса (а за это отвечает Connection::class) и я хочу чтобы это был единственный инстанс ($this->app->singleton). И теперь, если в любом месте приложения я попытаюсь дернуть что-то с именем Connection, то мне вернется уже созданный объект из контейнера зависимостей.

Всё это прекрасно, но главная боль всё ещё в силе — ЗАЧЕМ?

Зачем?

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

Имеем нечто, нууу… например… чтобы такое подобрать чтобы не банально, но показательно… Ладно, допустим у нас имеется некая система оповещения пользователя, например,  об ураганах. Отлично зашли, главное не банально =) Ураганы бывают разной интенсивности и если это легкий ветерок, то можно и просто на почту отправить сообщение, мол, одевайся потеплее, зонт оставь дома ,он тебя не спасёт. А вот если ветерок такой, что твои губы колышет, то наверное лучше бы всем как минимум позвонить и роботом рассказать: «А ну-ка резче (!) собрали паспорта и бегом в подвалы!»

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

<?php

namespace App\Contracts;

/**
 * Interface ContractAlarm
 *
 * @package App\Contracts
 */
interface ContractAlarm
{
    public function send();
}

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

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

Так.. И как я это хочу подвязать к сервис-провайдеру? Немного увлекся. Ах вот! Теперь мы создаем наш сервис провайдер, который… А лучше я сначала напишу, а оптом объясню

namespace App\Providers;

use App\Entity\Hurricane;
use App\Services\Alarm\EmailAlarm;
use App\Services\Alarm\PhoneAlarm;
use App\Services\Alarm\ArmyAlarm;
use Illuminate\Support\ServiceProvider;

class AlarmServiceProvider extends ServiceProvider
{
    public function register(Hurricane $hurricane): void
    {
        $this->app->singleton('ContractAlarm', function () use ($hurricane) {

            if( $hurricane->force <= 4 ) {
                return new EmailAlarm();
            }

            if( $hurricane->force > 4 && $hurricane->force < 7 ) {
                return new PhoneAlarm();
            }

            if( $hurricane->force <= 10 ) {
                return new ArmyAlarm();
            }

            throw new \InvalidArgumentException('Бегите, глупцы!');
        });
    }
}

Пояснения нужны? Ну лааадно уж. В зависимости от силы урагана будет создан тот или иной инстанс по оповещению, если всё слабенько, то по почте, если сильно — по телефону, при этом в коде мы будем везде вызывать ContractAlarm->send(). Ну круто же! Теперь нашему приложению совершенно пофигу что там и как реализуется, оно просто дергает некий абстрактный ContractAlarm, который имеем нужный метод send (кстати, имя можно было дать и более приличное, типа Alarmer, но это по желанию, говорят, что принято имя в контейнере и имя исходного класса лучше делать одинаковыми) Поэтому и удобно все реализации наследовать от контракта, чтобы всё было хорошо.

Надо ещё не забыть в config/app.php в секцию providers добавить наш новый сервис провайдер.

Выводы

Все эти штуки, дело сугубо добровольное. Никто тебя палками бить не будет если тебе впадлос юзать общепринятые решения. Хочешь, откажись от ООП вообще, сейчас это модно всякие извращения, можешь вообще всё внутри одной функции писать, чего ж нет? Типа очень жесткий Front Controller, который реально «Всё в одном месте» =) Но на счет «палками бить», ты подумай, а то вдруг всё же кто-то придет ;)

Всем рок!

Update

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

Регистрация сервис-провайдеров происходит через файл конфига config/app.php, т.е. нет никакой явной передачи параметров. И вообще, как говорит документация и редит, в методе register должна быть исключительно привязка реализации к интерфейсу.

Первое что мне пришло в голову, это в другом сервисе, или даже в этом же сделать отдельную инициализацию объекта Hurricane. Но в этом случае фреймворк не гарантирует, что всё будет ок, потому что загрузка сервисов происходит в произвольном порядке. И не факт, что при инициализации ContractAlarm Ураган будет определён. Но (!) у сервис провайдера есть метод boot(), и вот этот метод однозначно гарантирует, что все другие сервисы уже инициированы. Поэтому весь код по определению Hurricane нужно из register() перенести в boot(). А Hurricane инициировать отдельным сервис-провайдером.

Правда, если включить голову, то становится понятно, что всё это избыточно и многословно. Сейчас я бы явно создал экземпляр внутри метода register(), т.е. прям DB::get. Либо же через файлы конфига достать нужные значения. Последнее это каноничное решение.

Итого: Передавать параметры в метод register() не надо. Нужны параметры, либо залезай в конфиги, либо в базу напрямую. Посылаю голову пеплом, спасибо за конструктив

Понравилась статья? Поделиться с друзьями:
Комментарии: 17
  1. unuser

    Отличный слог, прекрасный юмор. Спасибо )

  2. Дмитрий

    А нужен ли сервис провайдер в случае, если реализация гарантированно одна?

    1. Ильдар (автор)

      Сервис провайдер не только про решение вопроса какую имплементацию интерфейса выбрать, это скорее приятный бонус, чем основная фича.

      На мой взгляд, основное — это положить блок часто используемого кода в быстродоступное место, в сервис контейнер. (ИМХО)

      Ну и потом можно использовать интересные штуки типа App::make(‘NeedService’); Или на время тестирования подменять реализацию.

      В общем-то, никто не мешает не использовать. Это ещё один приятный инструмент, которым пользоваться не обязательно, но в некоторых случаях бывает полезно. И если твой код будет развивать кто-то кроме тебя, то следовать некоторым общепринятым стандартам бывает полезно

  3. Олег

    Вопрос по статье: как вызвать провайдера с передачей параметров в register?

    1. Ильдар (автор)

      Есть короткий ответ: «Не знаю» =)

      А вообще, странный кейс. Если задача провайдера положить в IoC-контейнер некую реализацию функционального блока. То этот блок должен носить какой-то фундаментальный смысл. Например, «Система оповещения» или «сервис почтовой рассылки». И как следствие этот блок должен быть определен на момент выполнения основной бизнез-логики.

      Собственно, «параметры» можно определить в переменных окружения, либо в каких нибудь конфигурационных файлах. Это как раз те места, который ещё более низкоуровневы по отношению к основной логике

      1. Alex

        А $hurricane разве не является параметром в register? Или откуда он вообще берётся?

      2. Михаил

        про $huricane — выглядит то красиво — а откуда берется «не знаю». Статья неполная, поверхностная из разряда «я знаю много вумных слов»

  4. Дархан

    тоже вопрос с передачей параметра в register().Зачем писать и путать людей в том что вы не знаете? :evil:

  5. Руслан

    Для общего понимания, то что надо

  6. Valcxo

    Начал за здравие а закончил за упокой.
    1. Interface ContractAlarm — super
    2. Как использовать ContractAlarm в сервис провадере — хорошо но частично. так как есть альтернаивный паттерн — называется фабрика классов.
    3. Как использовать ContractAlarm->send() в коде т.е в Консоле или в Контролере полный отстой. При использовании фабрики классов точно понятно как обзуется инстанция в любом месте вызова этой фабрики через статический метод. Но как создается инстанция ContractAlarm или же $ContractAlarm в коде — это вообще не понятно.

    Твой пример вообще работал? А то такое впечатление что написано на коленке в тетради. Мое предложение переписать или удалить статью или переменовать «зачем нужны сервис провайдеры в ларавель самым маленьким». А то много критики в адрес других источников. Но твоя статья с точки зрения понимания использования (не зачем нужно, а именно применения) нет слов как плохо.

    1. Ильдар (автор)

      1. Спасибо, я старался

      2. Собственно, ничто не мешает использовать указанный паттерн (вероятнее всего речь идёт об фабричном методе или абстрактной фабрике?), но к чему в примере усложнение, если задача была в ином.

      3. Соглашусь, что вопрос получения я раскрыл вскользь, но мне казалось самоочевидным, что можно прокинуть зависимость в конструктор. Это называется внедрение зависимости. А в рамках Laravel можно использовать App::make(ContractAlarm::class);

  7. Юрий

    Тема не раскрыта.

    Юмор выдаёт очень молодого человека, но к нему я придираться не буду. Не за этим читал статью — так что просто отмечу, что лучше бы этого юмора не было — равно как предложений, не несущих никакого смысла («Понимаешь?! Это как нарисовать сову! Ой, всё! Давай потом на эту тему поговорим.»).

    Но нет ответа на главный вопрос — ну так почему не описать эту логику «по-нормальному» — сделать класс вот с этими ифами и где надо — создавать объект этого класса и…

    Вот оно! Вот тут-то основной прикол! Объект создан (синглетоном!) при загрузке и уже везде в коде доступен! Вот это-то в статье не подчеркнуто. А без этого все эти сервис-провайдеры — порождение больного ума.

    Рекомендую переписать статью.

    1. Ильдар (автор)

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

      Не всегда есть необходимость использовать единсвенный класс. А вот сбиндить интерфейс на реализацию, и везде по коду писать только название интерфейса (контракта в нотациях ларавел) — хорошая практика. Если реализаций в конкретном приложении может быть несколько.

  8. Давид

    Все вроде бы понятно, однако что такое EmailAlarm и PhoneAlarm , слишком абстрактно конечно, нужны пояснения)))
    Это классы , но где они лежат ?) Или Hurricane , это модель? Поясните, пожалуйста)

    1. Ильдар (автор)

      В рассматриваемом примере, EmailAlarm и PhoneAlarm я бы сделал сервисами, т.е. просто классы, которые лежат где-нибудь в app/Service/. Hurricane — это сущность (Entity — энтити), пытался изобразить ДДД архитектуру. Может быть и моделью, в рамках учебной задачи не принципиально.

  9. Галина

    Прикольный стиль написания! Весело — но понятней не стало)
    В каком случае надо писать свой сервис-провайдер, а в каком можно добавить сервис в AppServiceProvider.php?

  10. Андрей

    Долго объясняет тот, кто сам не понимает.

Добавить комментарий

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

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