1. Архитектурные концепции
  2. Сервис-контейнер

Присоединяйся к нашему Telegram сообществу @webblend!

Здесь ты найдешь сниппеты по Laravel и полезные советы по веб-разработке.

Введение

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

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

<?php
 
namespace App\Http\Controllers;
 
use App\Http\Controllers\Controller;
use App\Repositories\UserRepository;
use App\Models\User;
use Illuminate\View\View;
 
class UserController extends Controller
{
/**
* Создание нового экземпляра контроллера.
*/
public function __construct(
protected UserRepository $users,
) {}
 
/**
* Показать профиль для заданного пользователя.
*/
public function show(string $id): View
{
$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($service::class);
});

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

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

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

Благодаря автоматическому внедрению зависимостей и фасадам, во многих случаях вы можете создавать приложения Laravel, никогда не взаимодействуя вручную с контейнером. Например, вы можете указать тип объекта 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;
use Illuminate\Contracts\Foundation\Application;
 
$this->app->bind(Transistor::class, function (Application $app) {
return new Transistor($app->make(PodcastParser::class));
});

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

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

use App\Services\Transistor;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Support\Facades\App;
 
App::bind(Transistor::class, function (Application $app) {
// ...
});

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

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

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

Привязка к синглтону

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

use App\Services\Transistor;
use App\Services\PodcastParser;
use Illuminate\Contracts\Foundation\Application;
 
$this->app->singleton(Transistor::class, function (Application $app) {
return new Transistor($app->make(PodcastParser::class));
});

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

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

Привязка Scoped Singleton

Метод scoped привязывает класс или интерфейс в контейнер, который должен быть разрешен только один раз в пределах одного жизненного цикла запроса / задания Laravel. Несмотря на то, что этот метод похож на метод singleton, экземпляры, зарегистрированные с использованием метода scoped, будут очищены при каждом запуске нового «жизненного цикла» Laravel, например, при обработке нового запроса Laravel Octane или при обработке нового задания Laravel очереди:

use App\Services\Transistor;
use App\Services\PodcastParser;
use Illuminate\Contracts\Foundation\Application;
 
$this->app->scoped(Transistor::class, function (Application $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);

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

use App\Contracts\EventPusher;
 
/**
* Создание нового экземпляра класса.
*/
public function __construct(
protected EventPusher $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');
});

Привязка примитивов

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

use App\Http\Controllers\UserController;
 
$this->app->when(UserController::class)
->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 array
*/
protected $filters;
 
/**
* Создание нового экземпляра класса.
*/
public function __construct(
protected Logger $logger,
Filter ...$filters,
) {
$this->filters = $filters;
}
}

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

$this->app->when(Firewall::class)
->needs(Filter::class)
->give(function (Application $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 (Application $app) {
return new ReportAnalyzer($app->tagged('reports'));
});

Расширение привязок

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

$this->app->extend(Service::class, function (Service $service, Application $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]);

Метод bound можно использовать для определения того, связан ли класс или интерфейс явно в контейнере:

if ($this->app->bound(Transistor::class)) {
// ...
}

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

use App\Services\Transistor;
use Illuminate\Support\Facades\App;
 
$transistor = App::make(Transistor::class);
 
$transistor = app(Transistor::class);

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

use Illuminate\Container\Container;
 
/**
* Создание нового экземпляра класса.
*/
public function __construct(
protected Container $container
) {}

Автоматическая инъекция

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

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

<?php
 
namespace App\Http\Controllers;
 
use App\Repositories\UserRepository;
use App\Models\User;
 
class UserController extends Controller
{
/**
* Создание нового экземпляра контроллера.
*/
public function __construct(
protected UserRepository $users,
) {}
 
/**
* Показать пользователя с заданным ID.
*/
public function show(string $id): User
{
$user = $this->users->findOrFail($id);
 
return $user;
}
}

Вызов и инъекция метода

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

<?php
 
namespace App;
 
use App\Repositories\UserRepository;
 
class UserReport
{
/**
* Создать новый отчет о пользователе.
*/
public function generate(UserRepository $repository): array
{
return [
// ...
];
}
}

Вы можете вызвать метод generate через контейнер следующим образом:

use App\UserReport;
use Illuminate\Support\Facades\App;
 
$report = App::call([new UserReport, 'generate']);

Метод call принимает любой PHP-вызываемый объект. Метод call контейнера может быть использован даже для вызова замыкания с автоматическим внедрением его зависимостей:

use App\Repositories\UserRepository;
use Illuminate\Support\Facades\App;
 
$result = App::call(function (UserRepository $repository) {
// ...
});

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

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

use App\Services\Transistor;
use Illuminate\Contracts\Foundation\Application;
 
$this->app->resolving(Transistor::class, function (Transistor $transistor, Application $app) {
// Вызывается, когда контейнер разрешает объекты типа "Transistor"...
});
 
$this->app->resolving(function (mixed $object, Application $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.