Версия фреймворка: 8.x 5.4 4.2
Это скорректированный автоматический перевод, сделанный при помощи google translate.

Контейнер служб (service container)

Введение

Контейнер служб (service container, сервис-контейнер) Laravel – это мощный инструмент для управления зависимостями классов и выполнения внедрения зависимостей. Внедрение зависимостей – это причудливая фраза, которая по существу означает следующее: зависимости классов «вводятся» в класс через конструктор в виде аргументов или, в некоторых случаях, через методы-сеттеры. При создании класса или вызове методов фреймворк смотрит на список аргументов и, если нужно, создаёт экземпляры необходимых классов и сам подаёт их на вход конструктора или метода.

Давайте посмотрим на простой пример:

<?php

namespace App\Http\Controllers;

use App\Http\Controllers\Controller;
use App\Repositories\UserRepository;
use App\Models\User;

class UserController extends Controller
{
    /**
     * Реализация репозитория User.
     *
     * @var UserRepository
     */
    protected $users;

    /**
     * Создать новый экземпляр контроллера.
     *
     * @param  UserRepository  $users
     * @return void
     */
    public function __construct(UserRepository $users)
    {
        $this->users = $users;
    }

    /**
     * Показать профиль конкретного пользователя.
     *
     * @param  int  $id
     * @return Response
     */
    public function show($id)
    {
        $user = $this->users->find($id);

        return view('user.profile', ['user' => $user]);
    }
}

В этом примере UserController необходимо получить пользователей из источника данных. Итак, мы внедрим службу, которая может получать пользователей. В этом контексте наш UserRepository, скорее всего, использует Eloquent для получения информации о пользователе из базы данных. Однако, поскольку репозиторий внедрен, мы можем легко заменить его другой реализацией. Мы также можем легко «имитировать» или создать фиктивную реализацию UserRepository при тестировании нашего приложения.

Глубокое понимание контейнера служб Laravel необходимо для создания большого, мощного приложения, а также для внесения вклада в само ядро Laravel.

Неконфигурируемое внедрение

Если класс не имеет зависимостей или зависит только от других конкретных классов (не интерфейсов), контейнер не нужно инструктировать о том, как создавать этот класс. Например, вы можете поместить следующий код в свой файл routes/web.php:

<?php

class Service
{
    //
}

Route::get('/', function (Service $service) {
    die(get_class($service));
});

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

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

Когда использовать контейнер

Благодаря неконфигурируемому внедрению, вы часто будете объявлять типы зависимостей в маршрутах, контроллерах, слушателях событий и других местах, не взаимодействуя с контейнером напрямую. Например, вы можете указать объект Illuminate\Http\Request в определении вашего маршрута, чтобы вы могли легко получить доступ к текущему запросу. Несмотря на то, что нам никогда не нужно взаимодействовать с контейнером для написания этого кода, он управляет внедрением этих зависимостей за кулисами:

use Illuminate\Http\Request;

Route::get('/', function (Request $request) {
    // ...
});

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

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

Связывание

Основы связываний

Простое связывание

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

Внутри поставщика служб у вас всегда есть доступ к контейнеру через свойство $this->app. Мы можем зарегистрировать связывание, используя метод bind, передав имя класса или интерфейса, которые мы хотим зарегистрировать, вместе с замыканием, возвращающем экземпляр класса:

use App\Services\Transistor;
use App\Services\PodcastParser;

$this->app->bind(Transistor::class, function ($app) {
    return new Transistor($app->make(PodcastParser::class));
});

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

Как уже упоминалось, вы обычно будете взаимодействовать с контейнером внутри поставщиков служб; однако, если вы хотите взаимодействовать с контейнером вне поставщика услуг, вы можете сделать это через фасад App:

use App\Services\Transistor;
use Illuminate\Support\Facades\App;

App::bind(Transistor::class, function ($app) {
    // ...
});
Нет необходимости привязывать классы в контейнере, если они не зависят от каких-либо интерфейсов. Контейнеру не нужно указывать, как создавать эти объекты, поскольку он может автоматически извлекать эти объекты с помощью рефлексии.

Связывание одиночек

Метод singleton связывает класс или интерфейс в контейнере, который должен быть извлечен только один раз. После получения одиночки, тот же экземпляр объекта будет возвращен из контейнера и при последующих вызовах:

use App\Services\Transistor;
use App\Services\PodcastParser;

$this->app->singleton(Transistor::class, function ($app) {
    return new Transistor($app->make(PodcastParser::class));
});

Связывание экземпляров

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

use App\Services\Transistor;
use App\Services\PodcastParser;

$service = new Transistor(new PodcastParser);

$this->app->instance(Transistor::class, $service);

Связывание интерфейсов и реализаций

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

use App\Contracts\EventPusher;
use App\Services\RedisEventPusher;

$this->app->bind(EventPusher::class, RedisEventPusher::class);

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

use App\Contracts\EventPusher;

/**
 * Создать новый экземпляр класса.
 *
 * @param  \App\Contracts\EventPusher  $pusher
 * @return void
 */
public function __construct(EventPusher $pusher)
{
    $this->pusher = $pusher;
}

Контекстная привязка

Иногда, у вас может быть два класса, которые используют один и тот же интерфейс, но вы хотите внедрить разные реализации в каждый класс. Например, два контроллера могут зависеть от разных реализаций контракта Illuminate\Contracts\Filesystem\Filesystem. Laravel предлагает простой и понятный интерфейс для определения этого поведения:

use App\Http\Controllers\PhotoController;
use App\Http\Controllers\UploadController;
use App\Http\Controllers\VideoController;
use Illuminate\Contracts\Filesystem\Filesystem;
use Illuminate\Support\Facades\Storage;

$this->app->when(PhotoController::class)
          ->needs(Filesystem::class)
          ->give(function () {
              return Storage::disk('local');
          });

$this->app->when([VideoController::class, UploadController::class])
          ->needs(Filesystem::class)
          ->give(function () {
              return Storage::disk('s3');
          });

Связывание примитивов

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

$this->app->when('App\Http\Controllers\UserController')
          ->needs('$variableName')
          ->give($value);

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

$this->app->when(ReportAggregator::class)
    ->needs('$reports')
    ->giveTagged('reports');

Если вам нужно внедрить значение из одного из конфигурационных файлов вашего приложения, то вы можете использовать метод giveConfig:

$this->app->when(ReportAggregator::class)
    ->needs('$timezone')
    ->giveConfig('app.timezone');

Связывание типизированных вариаций

Иногда у вас может быть класс, который получает массив типизированных объектов с использованием переменного количества аргументов (прим. перев.: далее «вариации») конструктора:

<?php

use App\Models\Filter;
use App\Services\Logger;

class Firewall
{
    /**
     * Экземпляр регистратора.
     *
     * @var \App\Services\Logger
     */
    protected $logger;

    /**
     * Массив фильтров.
     *
     * @var array
     */
    protected $filters;

    /**
     * Создать новый экземпляр класса.
     *
     * @param  \App\Services\Logger  $logger
     * @param  array  $filters
     * @return void
     */
    public function __construct(Logger $logger, Filter ...$filters)
    {
        $this->logger = $logger;
        $this->filters = $filters;
    }
}

Используя контекстную привязку, вы можете внедрить такую зависимость, используя метод give с замыканием, которое возвращает массив внедряемых экземпляров Filter:

$this->app->when(Firewall::class)
          ->needs(Filter::class)
          ->give(function ($app) {
                return [
                    $app->make(NullFilter::class),
                    $app->make(ProfanityFilter::class),
                    $app->make(TooLongFilter::class),
                ];
          });

Для удобства вы также можете просто передать массив имен классов, которые будут предоставлены контейнером всякий раз, когда для Firewall нужны экземпляры Filter:

$this->app->when(Firewall::class)
          ->needs(Filter::class)
          ->give([
              NullFilter::class,
              ProfanityFilter::class,
              TooLongFilter::class,
          ]);

Метки вариативных зависимостей

Иногда класс может иметь вариативную зависимость, указывающую на тип как переданный класс (Report ...$reports). Используя методы needs и giveTagged, вы можете легко внедрить все привязки контейнера с этой меткой для указанной зависимости:

$this->app->when(ReportAggregator::class)
    ->needs(Report::class)
    ->giveTagged('reports');

Добавление меток

Иногда может потребоваться получить все привязки определенной «категории». Например, возможно, вы создаете анализатор отчетов, который получает массив из множества различных реализаций интерфейса Report. После регистрации реализаций Report вы можете назначить им метку с помощью метода tag:

$this->app->bind(CpuReport::class, function () {
    //
});

$this->app->bind(MemoryReport::class, function () {
    //
});

$this->app->tag([CpuReport::class, MemoryReport::class], 'reports');

После того, как службы помечены, вы можете легко все их получить с помощью метода tagged:

$this->app->bind(ReportAnalyzer::class, function ($app) {
    return new ReportAnalyzer($app->tagged('reports'));
});

Расширяемость связываний

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

$this->app->extend(Service::class, function ($service, $app) {
    return new DecoratedService($service);
});

Извлечение

Метод make

Вы можете использовать метод make для извлечения экземпляра класса из контейнера. Метод make принимает имя класса или интерфейса, который вы хотите получить:

use App\Services\Transistor;

$transistor = $this->app->make(Transistor::class);

Если некоторые зависимости вашего класса не могут быть разрешены через контейнер, вы можете ввести их, передав их как ассоциативный массив в метод makeWith. Например, мы можем вручную передать конструктору аргумент $id, требуемый службой Transistor:

use App\Services\Transistor;

$transistor = $this->app->makeWith(Transistor::class, ['id' => 1]);

Если вы находитесь за пределами поставщика служб и не имеете доступа к переменной $app, вы можете использовать фасад App для полуения экземпляра класса из контейнера:

use App\Services\Transistor;
use Illuminate\Support\Facades\App;

$transistor = App::make(Transistor::class);

Если вы хотите, чтобы сам экземпляр контейнера Laravel был внедрен в класс, извлекаемый контейнером, вы можете указать класс Illuminate\Container\Container в конструкторе вашего класса:

use Illuminate\Container\Container;

/**
 * Создать новый экземпляр класса.
 *
 * @param  \Illuminate\Container\Container  $container
 * @return void
 */
public function __construct(Container $container)
{
    $this->container = $container;
}

Автоматическое внедрение зависимостей

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

Например, вы можете объявить репозиторий, определенный вашим приложением, в конструкторе контроллера. Репозиторий будет автоматически получен и внедрен в класс:

<?php

namespace App\Http\Controllers;

use App\Repositories\UserRepository;

class UserController extends Controller
{
    /**
     * Экземпляр репозитория Пользователь.
     *
     * @var \App\Repositories\UserRepository
     */
    protected $users;

    /**
     * Создать новый экземпляр контроллера.
     *
     * @param  \App\Repositories\UserRepository  $users
     * @return void
     */
    public function __construct(UserRepository $users)
    {
        $this->users = $users;
    }

    /**
     * Показать пользователя с переданным идентификатором.
     *
     * @param  int  $id
     * @return \Illuminate\Http\Response
     */
    public function show($id)
    {
        //
    }
}

События контейнера

Контейнер служб инициирует событие каждый раз, когда извлекает объект. Вы можете прослушать это событие с помощью метода resolving:

use App\Services\Transistor;

$this->app->resolving(Transistor::class, function ($transistor, $app) {
    // Вызывается, когда контейнер извлекает объекты типа `Transistor` ...
});

$this->app->resolving(function ($object, $app) {
    // Вызывается, когда контейнер извлекает объект любого типа ...
});

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

PSR-11

Контейнер служб Laravel реализует интерфейс PSR-11. Поэтому вы можете объявить тип интерфейса контейнера PSR-11, чтобы получить экземпляр контейнера Laravel:

use App\Services\Transistor;
use Psr\Container\ContainerInterface;

Route::get('/', function (ContainerInterface $container) {
    $service = $container->get(Transistor::class);

    //
});

Исключение выбрасывается, если данный идентификатор не может быть получен. Исключением будет экземпляр Psr\Container\NotFoundExceptionInterface, если идентификатор никогда не был привязан. Если идентификатор был привязан, но не может быть извлечен, будет брошен экземпляр Psr \ Container \ ContainerExceptionInterface.