{{TOC}} {{DOCVER 5.3=c06d6a2352ed8c767633aab9c20f2bf7d880c967 28.01.2017 5:00:51, 5.2=6b0b057ae6de3c88cb29188459e38383c622ec23 8.12.2016 23:00:15, 5.1=cdc24ba7426c5b11eb4d050706bd78c3ea4913cc 19.06.2016 20:08:01, 5.0=5d10040a981deee82c0fde0e8e5d2ffc49eaaecb 8.02.2016 18:09:11}} == Введение == Сервис-контейнер в Laravel - это мощное средство для управления зависимостями классов и внедрения зависимостей. Внедрение зависимостей - это модный термин, который означает "внедрение" зависимостей класса в этот класс через конструктор или метод-сеттер. Давайте посмотрим на простой пример: %%(DOCNEW 5.3=c06d6a2352ed8c767633aab9c20f2bf7d880c967 28.01.2017 5:00:51) ~%% users = $users; } /** * Показать профиль данного пользователя. * * @param int $id * @return Response */ public function show($id) { $user = $this->users->find($id); return view('user.profile', ['user' => $user]); } } ~%% В этом примере %%(t)UserController%% должен получить пользователей из источника данных. Поэтому мы **внедрим** сервис, умеющий получать пользователей. В данном случае наш %%(t)UserRepository%% скорее всего использует ((//docs/v5/eloquent Eloquent)) для получения информации о пользователе из БД. Но поскольку репозиторий внедрён, мы можем легко подменить его другой реализацией. Мы также можем легко создать "mock" или фиктивную реализацию %%(t)UserRepository%% при тестировании нашего приложения. %% %%(DOCNEW 5.2=6b0b057ae6de3c88cb29188459e38383c622ec23 8.12.2016 23:00:15, 5.1=cdc24ba7426c5b11eb4d050706bd78c3ea4913cc 19.06.2016 20:08:01, 5.0=5d10040a981deee82c0fde0e8e5d2ffc49eaaecb 8.02.2016 18:09:11) ~%% mailer = $mailer; } /** * Покупка подкаста. * * @return void */ public function handle() { // } } ~%% В этом примере командный обработчик %%(t)PurchasePodcast%% должен отправить письмо пользователю для подтверждения покупки. Поэтому мы **внедрим** сервис, отправляющий электронные письма. Когда сервис внедрён, мы можем легко подменить его с другой реализацией. Мы также можем легко создать "mock" или фиктивную реализацию отправителя почтовых сообщений при тестировании нашего приложения. %% Глубокое понимание сервис-контейнера Laravel важно для создания мощного, высокопроизводительного приложения, а также для работы с ядром Laravel. == Связывание == === Основы связывания === Поскольку почти все ваши привязки сервис-контейнеров будут зарегистрированы в ((/docs/v5/providers сервис-провайдерах)), то все следующие примеры демонстрируют использование контейнеров в данном контексте. .(alert) Если классы не зависят от каких-либо интерфейсов, то нет необходимости связывать их в контейнере. Не нужно объяснять контейнеру, как создавать эти объекты, поскольку он автоматически извлекает такие объекты при помощи отражения (reflection). В сервис-провайдере всегда есть доступ к контейнеру через свойство %%$this->app%%. Зарегистрировать привязку можно методом %%bind()%%, передав имя того класса или интерфейса, который мы хотим зарегистрировать, вместе с замыканием, которое возвращает экземпляр класса: %% $this->app->bind('HelpSpot\API', function ($app) { return new HelpSpot\API($app->make('HttpClient')); }); %% Обратите внимание, что мы получаем сам контейнер в виде аргумента "резолвера". Затем мы можем использовать контейнер, чтобы получать под-зависимости создаваемого объекта. **Связывание синглтона** Метод %%singleton()%% привязывает класс или интерфейс к контейнеру, который должен быть создан только один раз. После создания связанного синглтона все последующие обращения к нему будут возвращать этот созданный экземпляр: %% $this->app->singleton('HelpSpot\API', function ($app) { return new HelpSpot\API($app->make('HttpClient')); }); %% **Связывание существующего экземпляра класса с контейнером** Вы можете также привязать существующий экземпляр объекта к контейнеру, используя метод %%instance()%%. Данный экземпляр будет всегда возвращаться при последующих обращениях к контейнеру: %% $api = new HelpSpot\API(new HttpClient); $this->app->instance('HelpSpot\Api', $api); %% %%(DOCNEW 5.3=c06d6a2352ed8c767633aab9c20f2bf7d880c967 28.01.2017 5:00:51, 5.2=6b0b057ae6de3c88cb29188459e38383c622ec23 8.12.2016 23:00:15) **Связывание примитивов** Иногда у вас есть такой класс, который получает другие внедрённые классы, но при этом ему нужны ещё и внедрённые примитивные значения, например, числа. Вы можете использовать контекстное связывание для внедрения любых необходимых вашему классу значений: ~%% $this->app->when('App\Http\Controllers\UserController') ->needs('$variableName') ->give($value); ~%% %% === Связывание интерфейса с реализацией === Довольно мощная функция сервис-контейнера - возможность связать интерфейс с реализацией. %%(DOCNEW 5.0=5d10040a981deee82c0fde0e8e5d2ffc49eaaecb 8.02.2016 18:09:11) Например, если наше приложение интегрировано с веб-сервисом для отправки и получения событий в реальном времени ((https://pusher.com/ Pusher)). И если мы используем Pusher PHP SDK, то можем внедрить экземпляр клиента Pusher в класс: ~%% pusher = $pusher; } /** * Выполнение данной команды. * * @param CreateOrder $command * @return void */ public function execute(CreateOrder $command) { // } } ~%% В этом примере хорошо то, что мы внедрили зависимости класса, но теперь мы жёстко привязаны к Pusher SDK. Если методы Pusher SDK изменятся, или мы решим полностью перейти на новый сервис событий, то нам надо будет переписывать %%(t)CreateOrderHandler%%. === Программа для интерфейса === Чтобы "изолировать" %%(t)CreateOrderHandler%% от изменений в сервисе событий, мы можем определить интерфейс %%(t)EventPusher%% и реализацию %%(t)PusherEventPusher%%: ~%% app->bind( 'App\Contracts\EventPusher', 'App\Services\RedisEventPusher' ); %% Так контейнер понимает, что должен внедрить %%(t)RedisEventPusher%%, когда классу нужна реализация %%(t)EventPusher%%. Теперь мы можем использовать указание типа интерфейса %%(t)EventPusher%% в конструкторе, или любом другом месте, где сервис-контейнер внедряет зависимости: %% use App\Contracts\EventPusher; /** * Создание нового экземпляра класса. * * @param EventPusher $pusher * @return void */ public function __construct(EventPusher $pusher) { $this->pusher = $pusher; } %% === Контекстное связывание === Иногда у вас может быть два класса, которые используют один интерфейс. Но вы хотите внедрить различные реализации в каждый класс. %%(DOCNEW 5.3=c06d6a2352ed8c767633aab9c20f2bf7d880c967 28.01.2017 5:00:51) Например, два контроллера могут работать на основе разных реализаций ((/docs/v5/contracts контракта)) %%(t)Illuminate\Contracts\Filesystem\Filesystem%%. Laravel предоставляет простой и гибкий интерфейс для описания такого поведения: ~%% use Illuminate\Support\Facades\Storage; use App\Http\Controllers\PhotoController; use App\Http\Controllers\VideoController; use Illuminate\Contracts\Filesystem\Filesystem; $this->app->when(PhotoController::class) ->needs(Filesystem::class) ->give(function () { return Storage::disk('local'); }); $this->app->when(VideoController::class) ->needs(Filesystem::class) ->give(function () { return Storage::disk('s3'); }); ~%% %% %%(DOCNEW 5.2=6b0b057ae6de3c88cb29188459e38383c622ec23 8.12.2016 23:00:15, 5.1=cdc24ba7426c5b11eb4d050706bd78c3ea4913cc 19.06.2016 20:08:01, 5.0=5d10040a981deee82c0fde0e8e5d2ffc49eaaecb 8.02.2016 18:09:11) Например, когда наша система получает новый заказ, нам может понадобиться отправка сообщения через ((http://www.pubnub.com/ PubNub)), а не через Pusher. Laravel предоставляет простой и гибкий интерфейс для описания такого поведения: ~%% $this->app->when('App\Handlers\Commands\CreateOrderHandler') ->needs('App\Contracts\EventPusher') ->give('App\Services\PubNubEventPusher'); ~%% Вы даже можете передать замыкание в метод %%give()%%: ~%% $this->app->when('App\Handlers\Commands\CreateOrderHandler') ->needs('App\Contracts\EventPusher') ->give(function () { // Извлечение зависимости... }); ~%% %% === Тегирование === Иногда вам может потребоваться получить все реализации в определенной категории. Например, вы пишете сборщик отчётов, который принимает массив различных реализаций интерфейса %%(t)Report%%. После регистрации реализаций %%(t)Report%% вы можете присвоить им тег, используя метод %%tag()%%: %% $this->app->bind('SpeedReport', function () { // }); $this->app->bind('MemoryReport', function () { // }); $this->app->tag(['SpeedReport', 'MemoryReport'], 'reports'); %% Теперь вы можете получить их по тегу методом %%tagged()%%: %% $this->app->bind('ReportAggregator', function ($app) { return new ReportAggregator($app->tagged('reports')); }); %% %%(DOCNEW 5.0=5d10040a981deee82c0fde0e8e5d2ffc49eaaecb 8.02.2016 18:09:11) == Использование на практике == Laravel предоставляет несколько способов использования сервис-контейнера для повышения гибкости и тестируемости вашего приложения. Одним из характерных примеров является получение контроллеров. Все контроллеры регистрируются через сервис-контейнер, и поэтому при получении класса контроллера из контейнера автоматически получаются все зависимости, указанные в аргументах конструктора и других методах контроллера. ~%% orders = $orders; } /** * Показать все заказы. * * @return Response */ public function index() { $orders = $this->orders->all(); return view('orders', ['orders' => $orders]); } } ~%% В этом примере класс %%(t)OrderRepository%% будет автоматически внедрён в контроллер. Это означает, что "mock" %%(t)OrderRepository%% может быть привязан к контейнеру во время ((/docs/v5/testing unit-тестирования)), позволяя сделать безболезненную заглушку для взаимодействия на уровне базы данных. **Другие примеры использования контейнера** Естественно, как уже упоминалось, контроллеры - это не единственные классы, которые Laravel получает из сервис-контейнера. Вы также можете использовать указание типов в функциях-замыканиях маршрутов, фильтрах, очередях, слушателях событий и т.д. Примеры использования сервис-контейнера приведены в соответствующих разделах документации. %% == Получение из контейнера == **Метод %%make()%%** Вы можете использовать метод %%make()%% для получения экземпляра класса из контейнера. Метод принимает имя класса или интерфейса, который вы хотите получить: %% $api = $this->app->make('HelpSpot\API'); %% Если вы находитесь в таком месте вашего кода, где нет доступа к переменной %%$app%%, вы можете использовать глобальный вспомогательный метод %%resolve()%%: %% $api = resolve('HelpSpot\API'); %% **Автоматическое внедрение** И самая важная возможность - вы можете просто указать тип зависимости в конструкторе класса, который имеется в контейнере, включая ((/docs/v5/controllers контроллеры)), ((/docs/v5/events слушатели событий)), ((/docs/v5/queues очереди задач)), ((/docs/v5/middleware посредники)) и т.д. Это тот способ, с помощью которого должно получатся большинство объектов из контейнера на практике. Например, вы можете указать тип репозитория, определённого вашим приложением в конструкторе контроллера. Репозиторий будет автоматически получен и внедрён в класс: %% users = $users; } /** * Показать пользователя с данным ID. * * @param int $id * @return Response */ public function show($id) { // } } %% .(tl_note) В этом примере для версии 5.1 и ранее использовался ещё один контроллер %%use Illuminate\Routing\Controller;%% — //прим. пер.// == События контейнера == Контейнер создаёт событие каждый раз, когда из него извлекается объект. Вы можете слушать эти события, используя метод %%resolving()%%: %% $this->app->resolving(function ($object, $app) { // Вызывается при извлечении объекта любого типа... }); $this->app->resolving(HelpSpot\API::class, function ($api, $app) { // Вызывается при извлечении объекта типа "HelpSpot\API"... }); %% Как видите, объект, получаемый из контейнера, передаётся в функцию обратного вызова, что позволяет вам задать любые дополнительные свойства для объекта перед тем, как отдать его тому, кто его запросил.