Сервисный контейнер Laravel
Сервисный контейнер Laravel

Сервисный контейнер Laravel



Введение

Сервисный контейнер Laravel - это мощный инструмент для управления зависимостями классов и выполнения внедрения зависимостей. Внедрение зависимостей - это причудливая фраза, которая по существу означает следующее: зависимости классов «вводятся» (injected) в класс через конструктор или, в некоторых случаях, методы «установки» (setter).

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

<?php

namespace App\Http\Controllers;

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

class UserController extends Controller
{
    /**
     * Реализация пользовательского репозитория.
     *
     * @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 для извлечения информации о пользователях из базы данных. Однако, поскольку репозиторий внедрен, мы можем легко заменить его другой реализацией. Мы также можем легко «имитировать» (mock) или создать фиктивную реализацию UserRepository при тестировании нашего приложения.

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

Привязка [Binding]

Основы биндинга

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

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

Простые биндинги

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

$this->app->bind('HelpSpot\API', function ($app) {
    return new \HelpSpot\API($app->make('HttpClient'));
});

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

Биндинг синглтона

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

$this->app->singleton('HelpSpot\API', function ($app) {
    return new \HelpSpot\API($app->make('HttpClient'));
});

Биндинг экземпляров [Binding Instances]

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

$api = new \HelpSpot\API(new HttpClient);

$this->app->instance('HelpSpot\API', $api);

Биндинг интерфейсов к реализациям

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

$this->app->bind(
    'App\Contracts\EventPusher',
    'App\Services\RedisEventPusher'
);

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

use App\Contracts\EventPusher;

/**
 * Создайте новый экземпляр класса.
 *
 * @param  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);

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

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

Биндинг типизированных переменных

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

class Firewall
{
    protected $logger;
    protected $filters;

    public function __construct(Logger $logger, Filter ...$filters)
    {
        $this->logger = $logger;
        $this->filters = $filters;
    }
}

Используя контекстную привязку (contextual binding), вы можете разрешить эту зависимость, предоставив методу give метод Closure, которое возвращает массив разрешенных экземпляров 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). Используя методы need и giveTagged, вы можете легко внедрить все привязки контейнеров с этим тегом для данной зависимости:

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

Теггирование

Иногда может потребоваться разрешить все привязки определенной «категории». Например, возможно, вы создаете агрегатор отчетов, который получает массив из множества различных реализаций интерфейса Report. После регистрации реализаций 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'));
});

Расширение биндингов [Extending Bindings]

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

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

Разрешение [Resolving]

Метод make

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

$api = $this->app->make('HelpSpot\API');

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

$api = resolve('HelpSpot\API');

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

$api = $this->app->makeWith('HelpSpot\API', ['id' => 1]);

Автоматический впрыск [Automatic Injection]

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

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

<?php

namespace App\Http\Controllers;

use App\Models\Users\Repository as UserRepository;

class UserController extends Controller
{
    /**
     * Экземпляр пользовательского репозитория.
     */
    protected $users;

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

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

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

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

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

$this->app->resolving(\HelpSpot\API::class, function ($api, $app) {
    // Вызывается, когда контейнер разрешает объекты типа "HelpSpot\API"...
});

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

PSR-11

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

use Psr\Container\ContainerInterface;

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

    //
});

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

Перевод:
https://laravel.com/docs/8.x/container

Заберите ссылку на статью к себе, чтобы потом легко её найти!
Раз уж досюда дочитали, то может может есть желание рассказать об этом месте своим друзьям, знакомым и просто мимо проходящим?
Не надо себя сдерживать! ;)

Старт! Горячий старт на просторы интернета
Старт! Горячий старт на просторы интернета
Старт! Меню