Документация Laravel 10.x
Здесь ты найдешь сниппеты по 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
привязывает класс или интерфейс в контейнер, который должен быть разрешен только один раз в пределах одного жизненного цикла запроса / задания 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
принимает имя класса или интерфейса, который вы хотите разрешить:
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) { // Вызывается, когда контейнер разрешает объект любого типа...});
Как видите, разрешаемый объект будет передан в колбэк, что позволяет устанавливать любые дополнительные свойства объекта, прежде чем он будет передан своему потребителю.
Сервис-контейнер 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
.