跳至内容

服务容器

介绍

Laravel 服务容器是一个强大的工具,用于管理类依赖关系并执行依赖注入。依赖注入是一个很花哨的术语,其本质含义是:类依赖关系通过构造函数(在某些情况下是“setter”方法)“注入”到类中。

我们来看一个简单的例子:

1<?php
2 
3namespace App\Http\Controllers;
4 
5use App\Services\AppleMusic;
6use Illuminate\View\View;
7 
8class PodcastController extends Controller
9{
10 /**
11 * Create a new controller instance.
12 */
13 public function __construct(
14 protected AppleMusic $apple,
15 ) {}
16 
17 /**
18 * Show information about the given podcast.
19 */
20 public function show(string $id): View
21 {
22 return view('podcasts.show', [
23 'podcast' => $this->apple->findPodcast($id)
24 ]);
25 }
26}

在此示例中,PodcastController需要从数据源(例如 Apple Music)检索播客。因此,我们将注入一个能够检索播客的服务。由于已注入该服务,因此在测试应用程序时,我们可以轻松地“模拟”或创建该服务的虚拟实现AppleMusic

深入了解 Laravel 服务容器对于构建强大的大型应用程序以及对 Laravel 核心本身的贡献至关重要。

零配置解决方案

如果一个类没有依赖项,或者仅依赖于其他具体类(而非接口),则容器无需了解如何解析该类。例如,你可以在routes/web.php文件中放置以下代码:

1<?php
2 
3class Service
4{
5 // ...
6}
7 
8Route::get('/', function (Service $service) {
9 dd($service::class);
10});

在这个例子中,访问应用程序的/路由将自动解析该类Service并将其注入到路由的处理程序中。这改变了游戏规则。这意味着您可以开发应用程序并利用依赖注入,而不必担心配置文件臃肿。

值得庆幸的是,构建 Laravel 应用程序时编写的许多类都会自动通过容器接收依赖项,包括控制器事件监听器中间件等等。此外,您还可以在队列任务handle的方法中使用类型Prompts来指定依赖项。一旦您体验到自动化和零配置依赖注入的强大功能,您就会觉得没有它开发就不可能了。

何时使用容器

得益于零配置解析,您通常可以在路由、控制器、事件监听器和其他位置使用类型Prompts依赖项,而无需手动与容器交互。例如,您可以Illuminate\Http\Request在路由定义中使用类型Prompts对象,以便轻松访问当前请求。即使我们无需与容器交互即可编写此代码,但它会在后台管理这些依赖项的注入:

1use Illuminate\Http\Request;
2 
3Route::get('/', function (Request $request) {
4 // ...
5});

很多时候,得益于自动依赖注入和Facades ,你无需手动绑定或解析容器中的任何内容即可构建 Laravel 应用程序。那么,什么时候需要手动与容器交互呢?让我们来分析两种情况。

首先,如果你编写了一个实现接口的类,并且希望在路由或类构造函数上对该接口进行类型Prompts,则必须告诉容器如何解析该接口。其次,如果你正在编写一个 Laravel 包并计划与其他 Laravel 开发者共享,则可能需要将包中的服务绑定到容器中。

绑定

绑定基础知识

简单绑定

几乎所有服务容器绑定都将在服务提供商内注册,因此大多数示例将演示在该上下文中使用容器。

在服务提供商中,你始终可以通过属性访问容器$this->app。我们可以使用方法注册一个绑定bind,传递我们希望注册的类或接口名称以及返回该类实例的闭包:

1use App\Services\Transistor;
2use App\Services\PodcastParser;
3use Illuminate\Contracts\Foundation\Application;
4 
5$this->app->bind(Transistor::class, function (Application $app) {
6 return new Transistor($app->make(PodcastParser::class));
7});

注意,我们将容器本身作为解析器的参数接收。然后,我们可以使用该容器来解析正在构建的对象的子依赖项。

如上所述,您通常会与服务提供商内的容器进行交互;但是,如果您想与服务提供商之外的容器进行交互,则可以通过App 外观进行交互:

1use App\Services\Transistor;
2use Illuminate\Contracts\Foundation\Application;
3use Illuminate\Support\Facades\App;
4 
5App::bind(Transistor::class, function (Application $app) {
6 // ...
7});

bindIf仅当尚未为给定类型注册绑定时,才可以使用该方法来注册容器绑定:

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

为了方便起见,您可以省略提供要注册为单独参数的类或接口名称,而是允许 Laravel 从您提供给方法的闭包的返回类型推断类型bind

1App::bind(function (Application $app): Transistor {
2 return new Transistor($app->make(PodcastParser::class));
3});

如果类不依赖任何接口,则无需将其绑定到容器中。容器无需了解如何构建这些对象,因为它可以使用反射自动解析这些对象。

绑定单例

singleton方法将一个类或接口绑定到容器中,该类或接口只需解析一次。一旦解析了单例绑定,后续调用该容器时将返回相同的对象实例:

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

singletonIf仅当尚未为给定类型注册绑定时,才可以使用该方法来注册单例容器绑定:

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

绑定作用域单例

scoped方法将一个类或接口绑定到容器中,该类或接口在给定的 Laravel 请求/作业生命周期内仅应解析一次。虽然此方法与singleton方法类似,但使用该scoped方法注册的实例将在 Laravel 应用程序启动新的“生命周期”时被刷新,例如当Laravel Octane工作进程处理新请求时,或当 Laravel队列工作进程处理新作业时:

1use App\Services\Transistor;
2use App\Services\PodcastParser;
3use Illuminate\Contracts\Foundation\Application;
4 
5$this->app->scoped(Transistor::class, function (Application $app) {
6 return new Transistor($app->make(PodcastParser::class));
7});

scopedIf仅当尚未为给定类型注册绑定时,您才可以使用该方法来注册作用域容器绑定:

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

绑定实例

你也可以使用 该方法将现有对象实例绑定到容器中instance。后续调用容器时,将始终返回给定的实例:

1use App\Services\Transistor;
2use App\Services\PodcastParser;
3 
4$service = new Transistor(new PodcastParser);
5 
6$this->app->instance(Transistor::class, $service);

将接口绑定到实现

服务容器的一个非常强大的功能是它能够将接口绑定到给定的实现。例如,假设我们有一个EventPusher接口和一个RedisEventPusher实现。一旦我们编写了RedisEventPusher该接口的实现,就可以像下面这样将其注册到服务容器中:

1use App\Contracts\EventPusher;
2use App\Services\RedisEventPusher;
3 
4$this->app->bind(EventPusher::class, RedisEventPusher::class);

这条语句告诉容器,RedisEventPusher当一个类需要实现 时,应该注入EventPusher。现在,我们可以在容器解析的类的构造函数中类型Prompts该EventPusher接口。记住,Laravel 应用程序中的控制器、事件监听器、中间件和其他各种类型的类始终使用容器解析:

1use App\Contracts\EventPusher;
2 
3/**
4 * Create a new class instance.
5 */
6public function __construct(
7 protected EventPusher $pusher,
8) {}

上下文绑定

有时你可能有两个类使用了相同的接口,但你希望为每个类注入不同的实现。例如,两个控制器可能依赖于Illuminate\Contracts\Filesystem\Filesystem contract的不同实现。Laravel 提供了一个简单、流畅的接口来定义这种行为:

1use App\Http\Controllers\PhotoController;
2use App\Http\Controllers\UploadController;
3use App\Http\Controllers\VideoController;
4use Illuminate\Contracts\Filesystem\Filesystem;
5use Illuminate\Support\Facades\Storage;
6 
7$this->app->when(PhotoController::class)
8 ->needs(Filesystem::class)
9 ->give(function () {
10 return Storage::disk('local');
11 });
12 
13$this->app->when([VideoController::class, UploadController::class])
14 ->needs(Filesystem::class)
15 ->give(function () {
16 return Storage::disk('s3');
17 });

上下文属性

由于上下文绑定通常用于注入驱动程序或配置值的实现,因此 Laravel 提供了各种上下文绑定属性,允许注入这些类型的值,而无需在服务提供商中手动定义上下文绑定。

例如,该Storage属性可用于注入特定的存储磁盘

1<?php
2 
3namespace App\Http\Controllers;
4 
5use Illuminate\Container\Attributes\Storage;
6use Illuminate\Contracts\Filesystem\Filesystem;
7 
8class PhotoController extends Controller
9{
10 public function __construct(
11 #[Storage('local')] protected Filesystem $filesystem
12 )
13 {
14 // ...
15 }
16}

除了Storage属性之外,Laravel 还提供AuthCacheConfigContextTagDB属性LogRouteParameter

1<?php
2 
3namespace App\Http\Controllers;
4 
5use App\Models\Photo;
6use Illuminate\Container\Attributes\Auth;
7use Illuminate\Container\Attributes\Cache;
8use Illuminate\Container\Attributes\Config;
9use Illuminate\Container\Attributes\Context;
10use Illuminate\Container\Attributes\DB;
11use Illuminate\Container\Attributes\Log;
12use Illuminate\Container\Attributes\RouteParameter;
13use Illuminate\Container\Attributes\Tag;
14use Illuminate\Contracts\Auth\Guard;
15use Illuminate\Contracts\Cache\Repository;
16use Illuminate\Database\Connection;
17use Psr\Log\LoggerInterface;
18 
19class PhotoController extends Controller
20{
21 public function __construct(
22 #[Auth('web')] protected Guard $auth,
23 #[Cache('redis')] protected Repository $cache,
24 #[Config('app.timezone')] protected string $timezone,
25 #[Context('uuid')] protected string $uuid,
26 #[DB('mysql')] protected Connection $connection,
27 #[Log('daily')] protected LoggerInterface $log,
28 #[RouteParameter('photo')] protected Photo $photo,
29 #[Tag('reports')] protected iterable $reports,
30 ) {
31 // ...
32 }
33}

此外,Laravel 提供了一个CurrentUser属性,用于将当前经过身份验证的用户注入到给定的路由或类中:

1use App\Models\User;
2use Illuminate\Container\Attributes\CurrentUser;
3 
4Route::get('/user', function (#[CurrentUser] User $user) {
5 return $user;
6})->middleware('auth');

定义自定义属性

你可以通过实现契约来创建自己的上下文属性Illuminate\Contracts\Container\ContextualAttribute。容器将调用属性的resolve方法,该方法将解析出需要注入到使用该属性的类中的值。在下面的示例中,我们将重新实现 Laravel 的内置Config属性:

1<?php
2 
3namespace App\Attributes;
4 
5use Attribute;
6use Illuminate\Contracts\Container\Container;
7use Illuminate\Contracts\Container\ContextualAttribute;
8 
9#[Attribute(Attribute::TARGET_PARAMETER)]
10class Config implements ContextualAttribute
11{
12 /**
13 * Create a new attribute instance.
14 */
15 public function __construct(public string $key, public mixed $default = null)
16 {
17 }
18 
19 /**
20 * Resolve the configuration value.
21 *
22 * @param self $attribute
23 * @param \Illuminate\Contracts\Container\Container $container
24 * @return mixed
25 */
26 public static function resolve(self $attribute, Container $container)
27 {
28 return $container->make('config')->get($attribute->key, $attribute->default);
29 }
30}

绑定原语

有时你可能有一个类,它接收一些注入的类,但也需要注入一个原始值,比如整数。你可以轻松地使用上下文绑定来注入类可能需要的任何值:

1use App\Http\Controllers\UserController;
2 
3$this->app->when(UserController::class)
4 ->needs('$variableName')
5 ->give($value);

有时一个类可能依赖于一个带有标签的实例数组。使用该giveTagged方法,你可以轻松地注入所有带有该标签的容器绑定:

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

如果需要从应用程序的某个配置文件中注入值,则可以使用该giveConfig方法:

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

绑定类型变量

有时,您可能有一个使用可变构造函数参数接收类型对象数组的类:

1<?php
2 
3use App\Models\Filter;
4use App\Services\Logger;
5 
6class Firewall
7{
8 /**
9 * The filter instances.
10 *
11 * @var array
12 */
13 protected $filters;
14 
15 /**
16 * Create a new class instance.
17 */
18 public function __construct(
19 protected Logger $logger,
20 Filter ...$filters,
21 ) {
22 $this->filters = $filters;
23 }
24}

使用上下文绑定,您可以通过为give方法提供一个返回已解析Filter实例数组的闭包来解决此依赖关系:

1$this->app->when(Firewall::class)
2 ->needs(Filter::class)
3 ->give(function (Application $app) {
4 return [
5 $app->make(NullFilter::class),
6 $app->make(ProfanityFilter::class),
7 $app->make(TooLongFilter::class),
8 ];
9 });

Firewall为了方便起见,您也可以只提供一个类名数组,以便在需要实例时由容器解析Filter

1$this->app->when(Firewall::class)
2 ->needs(Filter::class)
3 ->give([
4 NullFilter::class,
5 ProfanityFilter::class,
6 TooLongFilter::class,
7 ]);

可变标签依赖关系

有时,一个类可能具有可变参数依赖项,该依赖项的类型Prompts为给定类(Report ...$reports)。使用needs和方法,你可以轻松地为给定依赖项giveTagged注入所有带有该标记的容器绑定:

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

标记

有时,您可能需要解析某个“类别”的绑定。例如,您可能正在构建一个报表分析器,它接收一个包含许多不同Report接口实现的数组。注册这些Report实现后,您可以使用以下方法为它们分配一个标签tag

1$this->app->bind(CpuReport::class, function () {
2 // ...
3});
4 
5$this->app->bind(MemoryReport::class, function () {
6 // ...
7});
8 
9$this->app->tag([CpuReport::class, MemoryReport::class], 'reports');

一旦服务被标记,您就可以通过容器的tagged方法轻松地解决它们:

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

扩展绑定

extend方法允许修改已解析的服务。例如,当服务解析完成后,您可以运行额外的代码来装饰或配置该服务。该extend方法接受两个参数:您正在扩展的服务类和一个返回已修改服务的闭包。闭包接收正在解析的服务和容器实例:

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

解析

方法make

你可以使用该make方法从容器中解析一个类实例。该make方法接受你想要解析的类或接口的名称:

1use App\Services\Transistor;
2 
3$transistor = $this->app->make(Transistor::class);

如果类的某些依赖项无法通过容器解析,则可以将它们作为关联数组传递给makeWith方法进行注入。例如,我们可以手动传递服务$id所需的构造函数参数Transistor

1use App\Services\Transistor;
2 
3$transistor = $this->app->makeWith(Transistor::class, ['id' => 1]);

bound方法可用于确定类或接口是否已在容器中明确绑定:

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

如果您在代码中服务提供商之外无法访问$app变量的位置,则可以使用App 外观app Helpers从容器中解析类实例:

1use App\Services\Transistor;
2use Illuminate\Support\Facades\App;
3 
4$transistor = App::make(Transistor::class);
5 
6$transistor = app(Transistor::class);

如果您希望将 Laravel 容器实例本身注入到由容器解析的类中,则可以Illuminate\Container\Container在类的构造函数上对该类进行类型Prompts:

1use Illuminate\Container\Container;
2 
3/**
4 * Create a new class instance.
5 */
6public function __construct(
7 protected Container $container,
8) {}

自动注射

或者,更重要的是,你可以在容器解析的类的构造函数中类型Prompts依赖项,包括控制器事件监听器中间件等等。此外,你也可以在队列任务handle的方法中类型Prompts依赖项。实际上,容器应该以这种方式解析大多数对象。

例如,你可以在控制器的构造函数中对应用程序定义的服务进行类型Prompts。该服务将自动解析并注入到类中:

1<?php
2 
3namespace App\Http\Controllers;
4 
5use App\Services\AppleMusic;
6 
7class PodcastController extends Controller
8{
9 /**
10 * Create a new controller instance.
11 */
12 public function __construct(
13 protected AppleMusic $apple,
14 ) {}
15 
16 /**
17 * Show information about the given podcast.
18 */
19 public function show(string $id): Podcast
20 {
21 return $this->apple->findPodcast($id);
22 }
23}

方法调用和注入

有时你可能希望调用对象实例上的某个方法,同时允许容器自动注入该方法的依赖项。例如,给定以下类:

1<?php
2 
3namespace App;
4 
5use App\Services\AppleMusic;
6 
7class PodcastStats
8{
9 /**
10 * Generate a new podcast stats report.
11 */
12 public function generate(AppleMusic $apple): array
13 {
14 return [
15 // ...
16 ];
17 }
18}

您可以generate像这样通过容器调用该方法:

1use App\PodcastStats;
2use Illuminate\Support\Facades\App;
3 
4$stats = App::call([new PodcastStats, 'generate']);

call方法接受任何 PHP 可调用函数。容器的call方法甚至可以用于在自动注入依赖项的同时调用闭包:

1use App\Services\AppleMusic;
2use Illuminate\Support\Facades\App;
3 
4$result = App::call(function (AppleMusic $apple) {
5 // ...
6});

容器事件

服务容器每次解析一个对象时都会触发一个事件。你可以使用下面的resolving方法来监听该事件:

1use App\Services\Transistor;
2use Illuminate\Contracts\Foundation\Application;
3 
4$this->app->resolving(Transistor::class, function (Transistor $transistor, Application $app) {
5 // Called when container resolves objects of type "Transistor"...
6});
7 
8$this->app->resolving(function (mixed $object, Application $app) {
9 // Called when container resolves object of any type...
10});

如您所见,正在解析的对象将被传递给回调,从而允许您在将对象提供给使用者之前设置该对象的任何其他属性。

重新绑定

rebinding方法允许你监听服务何时重新绑定到容器,这意味着它在初始绑定后被再次注册或覆盖。当你需要在每次更新特定绑定时更新依赖项或修改行为时,这非常有用:

1use App\Contracts\PodcastPublisher;
2use App\Services\SpotifyPublisher;
3use App\Services\TransistorPublisher;
4use Illuminate\Contracts\Foundation\Application;
5 
6$this->app->bind(PodcastPublisher::class, SpotifyPublisher::class);
7 
8$this->app->rebinding(
9 PodcastPublisher::class,
10 function (Application $app, PodcastPublisher $newInstance) {
11 //
12 },
13);
14 
15// New binding will trigger rebinding closure...
16$this->app->bind(PodcastPublisher::class, TransistorPublisher::class);

PSR-11

Laravel 的服务容器实现了PSR-11接口。因此,你可以使用类型Prompts PSR-11 容器接口来获取 Laravel 容器的实例:

1use App\Services\Transistor;
2use Psr\Container\ContainerInterface;
3 
4Route::get('/', function (ContainerInterface $container) {
5 $service = $container->get(Transistor::class);
6 
7 // ...
8});

如果给定的标识符无法解析,则会抛出异常。如果该标识符从未绑定,则异常为 的实例Psr\Container\NotFoundExceptionInterface。如果该标识符已绑定但无法解析,则Psr\Container\ContainerExceptionInterface异常为 的实例。