服务容器
Laravel 服务容器是一个用来管理类依赖和实现依赖注入的强力工具。"依赖注入"听起来高大上,说白了就一件事:类需要的依赖不是自己在内部 new,而是通过构造函数(有时是 setter 方法)从外部"注入"进来。
看个最简单的例子:
<?php
namespace App\Http\Controllers;
use App\Services\AppleMusic;
use Illuminate\View\View;
class PodcastController extends Controller
{
/**
* Create a new controller instance.
*/
public function __construct(
protected AppleMusic $apple,
) {}
/**
* Show information about the given podcast.
*/
public function show(string $id): View
{
return view('podcasts.show', [
'podcast' => $this->apple->findPodcast($id)
]);
}
}
这个例子里,PodcastController 需要从数据源(比如 Apple Music)获取播客数据,所以我们在构造函数里注入了一个能查播客的服务。因为是注入的,测试时可以轻松"mock"掉 AppleMusic,换成假的实现,不需要真去调 Apple Music。
深入理解服务容器是构建大型 Laravel 应用的必备技能,也是参与 Laravel 核心开发的基础。
零配置解析
如果一个类没有依赖,或者只依赖其他具体类(不是接口),容器不需要你教它怎么解析——它自己能搞定。比如在 routes/web.php 里写:
<?php
class Service
{
// ...
}
Route::get('/', function (Service $service) {
dd($service::class);
});
访问 / 路由时,Service 类会被自动解析并注入到路由处理函数里。这就是零配置依赖注入——你不用维护一大堆绑定配置,写代码时直接类型提示就行。
好消息是,Laravel 中大部分你需要写的类——控制器、事件监听器、中间件等——都会自动通过容器获取依赖。你还可以在队列任务的 handle 方法里类型提示依赖。一旦用过这种自动依赖注入,就回不去了。
什么时候需要手动操作容器
有了零配置解析,绝大多数时候你只需要在路由、控制器、事件监听器等地方写类型提示就行,根本不用手动碰容器。比如在路由里写 Illuminate\Http\Request 就能拿到当前请求对象——虽然你没碰容器,但背后是容器在帮你注入:
use Illuminate\Http\Request;
Route::get('/', function (Request $request) {
// ...
});
实际上,靠着自动依赖注入和门面,你完全可以在不手动绑定或解析任何东西的情况下写完整个 Laravel 应用。那什么情况下才需要手动操作容器呢, 有两种情况:
第一,如果你写了一个实现接口的类,并想在路由或构造函数里用这个接口做类型提示,你就得告诉容器这个接口应该解析成哪个实现。
第二,如果你在写 Laravel 扩展包准备分享给别人,可能需要把你包里的服务绑定到容器里。
绑定
基础绑定
简单绑定
几乎所有的容器绑定都在服务提供者里注册,所以后面的例子也都放在服务提供者的上下文中演示。
在服务提供者里,通过 $this->app 属性随时能拿到容器实例。用 bind 方法注册绑定,传类名或接口名和一个返回类实例的闭包:
namespace App\Providers;
use App\Services\Transistor;
use App\Services\PodcastParser;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
$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) {
// ...
});
什么时候会在服务提供者之外绑定?
"服务提供者之外"指的是 AppServiceProvider 的 register() / boot() 以外的地方,比如路由文件、控制器、Artisan 命令、测试代码等。
正常写业务放服务提供者就够了,但有几个场景必须在外面临时绑定:
- 测试时替换实现:测试里把真实支付换成假的
App::bind(PaymentGateway::class, FakePayment::class),不可能为一次测试去改AppServiceProvider - 依赖运行时信息:绑定逻辑依赖当前用户或请求参数(如 VIP 用户走不同实现),服务提供者在请求到来之前就执行完了,拿不到这些信息
- 扩展包开发:写 Laravel 包的开发者不知道用户项目的服务提供者结构,直接在包的任何地方用
App::bind就能注册
简单说:服务提供者是"编译期"静态绑定,App::bind 是"运行时"动态绑定——大部分场景用前者就够了,后者是特殊需求时的逃生出口。
更方便的写法——省略第一个参数,让 Laravel 根据闭包的返回值类型自动推断:
App::bind(function (Application $app): Transistor {
return new Transistor($app->make(PodcastParser::class));
});
注意
如果类不依赖任何接口,就不需要往容器里绑。容器不需要你教它怎么构建这些对象——它用反射就能自动搞定。总结一句话:能不能直接 new 是唯一判断标准。具体类能 new,不用绑;接口和抽象类不能new, 需要绑定。
bindIf 方法只在没有绑定过时才注册:
$this->app->bindIf(Transistor::class, function (Application $app) {
return new Transistor($app->make(PodcastParser::class));
});
`bindIf` 和 `bind` 有什么区别?
bind 是"给我绑,覆盖之前已有的";bindIf 是"没绑过才绑,绑过了就当没看见"。
// 第一次
$this->app->bindIf(Transistor::class, fn() => new TransistorA()); // ✅ 注册成功
// 第二次——静默忽略,还是 TransistorA,不会变成 TransistorB
$this->app->bindIf(Transistor::class, fn() => new TransistorB()); // ❌ 不生效
bindIf 唯一合理的场景是扩展包开发——包提供一个默认实现,但允许用户在 AppServiceProvider 里提前绑一个覆盖掉它。写业务代码老老实实用 bind 就行:万一不小心在两处写了同名 bindIf,第二处被静默忽略,查 bug 能查一下午。
绑定单例
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));
});
bind 和 singleton 怎么选?
核心区别:bind 每次要都 new 一个新对象,singleton 只 new 一次、后面全返回同一个。
// bind:每次解析都是新对象
$this->app->bind(Transistor::class, fn() => new Transistor());
$a = $this->app->make(Transistor::class);
$b = $this->app->make(Transistor::class);
// $a !== $b ——两个不同实例
// singleton:全应用共享同一个
$this->app->singleton(Transistor::class, fn() => new Transistor());
$a = $this->app->make(Transistor::class);
$b = $this->app->make(Transistor::class);
// $a === $b ——同一个实例
用 singleton 的场景:
- 无状态的服务——比如 API 客户端封装、日志服务、配置读取器,构造一次后就没有变化的理由
- 需要共享状态——比如事件分发器,多个地方都要往上面挂监听器,必须用同一个实例才能生效
- 构造开销大——比如建立数据库连接、加载大文件,new 一次就够了
用 bind 的场景:
- 有状态的、每次请求不同的——比如一个"当前订单处理器",每个请求的订单 ID 不同,必须 new 新的
- 短生命周期的——比如 DTO、请求校验器,用完就丢,没必要缓存
简单判断:问自己"这个类在同一个请求里,注入两次需不需要是同一个对象?"——需要同一个就 singleton,无所谓或必须不同就 bind。
单例属性
还可以用 #[Singleton] 属性标记一个接口或类,告诉容器只解析一次:
<?php
namespace App\Services;
use Illuminate\Container\Attributes\Singleton;
#[Singleton]
class Transistor
{
// ...
}
为什么需要单例属性?
这个属性的存在意义很简单:让不需要手动绑定的具体类,也能声明自己是单例。
具体类不依赖接口,容器用反射就能自动 new,你本来就不需要去服务提供者里绑它。但零配置解析默认是"每次 new 一个新对象"——如果你想让一个具体类走单例模式,没有属性的时候只能专门去 AppServiceProvider 里多写一行:
// 没有属性——你被迫在服务提供者里写一行纯"声明"代码
$this->app->singleton(Logger::class, fn() => new Logger());
这行代码跟业务逻辑毫无关系,纯粹是"告诉容器这是单例"。#[Singleton] 把这句话从服务提供者挪到了类自身上面——声明归位,服务提供者保持干净:
// 有属性——类自己声明,服务提供者什么都不用写
#[Singleton]
class Logger { /* ... */ }
至于接口或抽象类,本来就要在服务提供者里绑实现,写法是 singleton() 还是 bind() 就差一个单词,属性的确可有可无。但也不冲突——你在接口上写 #[Singleton],下面写 bind(),容器解析时会自动读取属性并缓存实例,等同于 singleton() 的效果。
一句话:#[Singleton] 消灭的是"具体类想当单例却得跑到服务提供者里额外声明"这个样板代码。
绑定作用域单例
scoped 方法绑一个类或接口到容器,保证在单次 Laravel 请求 / 任务生命周期内只解析一次。跟 singleton 类似,但 scoped 绑定的实例在 Laravel 开启新"生命周期"时会被清掉——比如 Laravel Octane worker 处理新请求时,或 队列 worker 处理新任务时:
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));
});
scopedIf 只在没有绑定过时才注册:(业务代码基本不用,开发扩展包才用)
$this->app->scopedIf(Transistor::class, function (Application $app) {
return new Transistor($app->make(PodcastParser::class));
});
作用域属性
也可以用 #[Scoped] 属性标记一个接口或类,告诉容器在单次请求 / 任务生命周期内只解析一次:
<?php
namespace App\Services;
use Illuminate\Container\Attributes\Scoped;
#[Scoped]
class Transistor
{
// ...
}
什么时候用 `scoped`?
先说结论:如果你的应用只跑传统 php-fpm(每个请求独立进程),scoped 和 singleton 没有任何区别——请求结束进程就死,什么缓存都没了。全用 singleton 也不会出问题。
scoped 真正的用武之地是长驻进程——进程不重启,singleton 实例会跨请求 / 跨任务残留:
- 队列 worker(
queue:work):进程处理完任务1后不退出,接着处理任务2。singleton的实例一直活着,任务1的状态会污染任务2 - Laravel Octane:跟队列类似,HTTP 请求之间进程不重启,
singleton跨请求残留
一句话判断:这个依赖的实例,上一个请求/任务用完,下一个还能直接用吗?
| 类型 | 用什么 | 举例 |
|---|---|---|
| 无状态、跨任务复用没副作用 | singleton |
日志、配置、HTTP 客户端、数据库连接 |
| 携带当前请求/任务相关状态 | scoped |
当前订单上下文、请求级缓存、用户信息容器 |
| 每次都要新的 | bind |
DTO、表单校验器 |
用错会怎样? 不会报错,但会出现诡异的数据串扰——比如任务2看到了任务1的订单 ID、用户A 的数据出现在用户B 的请求里。这种 bug 在本地开发(传统模式)根本复现不了,极难排查。
如果不用队列和 Octane,这段可以跳过——singleton 就够用了。
绑定已有实例
用 instance 方法把已有的对象实例绑定到容器。后续每次取都是这个实例:
use App\Services\Transistor;
use App\Services\PodcastParser;
$service = new Transistor(new PodcastParser);
$this->app->instance(Transistor::class, $service);
什么时候用 instance?
bind 和 singleton 是"给你一个配方,容器自己调"——传的是闭包或类名,容器在第一次要的时候才执行。instance 是"对象已经好了,直接拿走"——你提前 new 好塞进去。
// bind:容器自己 new,每次要都执行闭包
$this->app->bind(Transistor::class, fn() => new Transistor(new PodcastParser()));
// instance:你自己 new 好,容器直接存起来
$service = new Transistor(new PodcastParser());
$this->app->instance(Transistor::class, $service);
实际场景:
- 测试里替换实现:mock 对象已经建好了,直接
$this->app->instance(PaymentGateway::class, $mock)省事 - 构造函数参数容器搞不定:比如
new Foo('some-string', 42)这种裸值,容器没法自动解析,不如自己 new 好再instance - 服务提供者
boot()里:对象在register()里已经构造好了,不想再包一层闭包
简单说:能做 new 但不能(或不想)写成闭包让容器帮你 new 的时候,就用 instance。
接口绑定到实现
服务容器一个超强功能是把接口绑定到具体实现上。比如我们有一个 EventPusher 接口和一个 RedisEventPusher 实现。写好 RedisEventPusher 后,在容器里这样注册:
use App\Contracts\EventPusher;
use App\Services\RedisEventPusher;
$this->app->bind(EventPusher::class, RedisEventPusher::class);
这句话告诉容器:谁要 EventPusher,就给它 RedisEventPusher。之后在构造函数里类型提示 EventPusher 接口就行——控制器、事件监听器、中间件等都是容器解析的:
use App\Contracts\EventPusher;
/**
* Create a new class instance.
*/
public function __construct(
protected EventPusher $pusher,
) {}
Bind 属性
Laravel 还提供了 Bind 属性更方便地做接口绑定。把属性加到接口上,告诉 Laravel 需要这个接口时自动注入哪个实现,连服务提供者里都不用写注册代码了。
还可以在接口上放多个 Bind 属性,按环境指定不同实现:
<?php
namespace App\Contracts;
use App\Services\FakeEventPusher;
use App\Services\RedisEventPusher;
use Illuminate\Container\Attributes\Bind;
#[Bind(RedisEventPusher::class)]
#[Bind(FakeEventPusher::class, environments: ['local', 'testing'])]
interface EventPusher
{
// ...
}
再配合 Singleton 和 Scoped 属性,控制绑定是全局单例还是一次请求内单例:
use App\Services\RedisEventPusher;
use Illuminate\Container\Attributes\Bind;
use Illuminate\Container\Attributes\Singleton;
#[Bind(RedisEventPusher::class)]
#[Singleton]
interface EventPusher
{
// ...
}
上下文绑定
有时候两个类用了同一个接口,但你想给它们注入不同的实现。比如两个控制器需要不同 Filesystem 的实现。Laravel 用 when → needs → give 的链式调用来搞定:
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;
class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
$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');
});
}
}
上下文属性
上下文绑定经常用来注入驱动实现或配置值,所以 Laravel 内置了一系列上下文属性,不用手动在服务提供者里写上下文绑定。
比如 Storage 属性直接指定注入哪个存储磁盘:
namespace App\Http\Controllers;
use Illuminate\Container\Attributes\Storage;
use Illuminate\Contracts\Filesystem\Filesystem;
class PhotoController extends Controller
{
public function __construct(
#[Storage('local')] protected Filesystem $filesystem
) {
// ...
}
}
除了 Storage,Laravel 还提供了 Auth、Cache、Config、Context、DB、Give、Log、RouteParameter 和 Tag 属性:
namespace App\Http\Controllers;
use App\Contracts\UserRepository;
use App\Models\Photo;
use App\Repositories\DatabaseRepository;
use Illuminate\Container\Attributes\Auth;
use Illuminate\Container\Attributes\Cache;
use Illuminate\Container\Attributes\Config;
use Illuminate\Container\Attributes\Context;
use Illuminate\Container\Attributes\DB;
use Illuminate\Container\Attributes\Give;
use Illuminate\Container\Attributes\Log;
use Illuminate\Container\Attributes\RouteParameter;
use Illuminate\Container\Attributes\Tag;
use Illuminate\Contracts\Auth\Guard;
use Illuminate\Contracts\Cache\Repository;
use Illuminate\Database\Connection;
use Psr\Log\LoggerInterface;
class PhotoController extends Controller
{
public function __construct(
#[Auth('web')] protected Guard $auth,
#[Cache('redis')] protected Repository $cache,
#[Config('app.timezone')] protected string $timezone,
#[Context('uuid')] protected string $uuid,
#[Context('ulid', hidden: true)] protected string $ulid,
#[DB('mysql')] protected Connection $connection,
#[Give(DatabaseRepository::class)] protected UserRepository $users,
#[Log('daily')] protected LoggerInterface $log,
#[RouteParameter] protected Photo $photo,
#[Tag('reports')] protected iterable $reports,
) {
// ...
}
}
RouteParameter 属性会解析跟参数名匹配的路由参数。需要的话也可以显式指定参数名:#[RouteParameter('photo')]。
Laravel 还提供了 CurrentUser 属性,直接把当前认证用户注入到路由或类中:
use App\Models\User;
use Illuminate\Container\Attributes\CurrentUser;
Route::get('/user', function (#[CurrentUser] User $user) {
return $user;
})->middleware('auth');
自定义属性
你可以实现 Illuminate\Contracts\Container\ContextualAttribute 契约来创建自己的上下文属性。容器会调用属性上的 resolve 方法,解析出要注入的值。下面用重写 Laravel 内置 Config 属性来演示:
<?php
namespace App\Attributes;
use Attribute;
use Illuminate\Contracts\Container\Container;
use Illuminate\Contracts\Container\ContextualAttribute;
use ReflectionParameter;
#[Attribute(Attribute::TARGET_PARAMETER)]
class Config implements ContextualAttribute
{
/**
* Create a new attribute instance.
*/
public function __construct(public string $key, public mixed $default = null)
{
}
/**
* Resolve the configuration value.
*
* @param self $attribute
* @param \Illuminate\Contracts\Container\Container $container
* @param \ReflectionParameter $parameter
* @return mixed
*/
public static function resolve(self $attribute, Container $container, ReflectionParameter $parameter)
{
return $container->make('config')->get($attribute->key, $attribute->default);
}
}
绑定原始值
有时候一个类既要注入其他类,又需要一个原始值(比如整数)。用上下文绑定也能轻松注入:
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');
什么场景会用到绑定原始值?
容器能自动解析类依赖,但遇到 int、string 这种裸值就蒙了——它不知道该传多少。比如:
class ReportGenerator
{
public function __construct(
protected Logger $logger, // ← 容器能搞定
protected int $maxRows, // ← 容器蒙了:这该传什么?
) {}
}
不做绑定的做法——每次都自己 makeWith 手动传:
$report = $this->app->makeWith(ReportGenerator::class, ['maxRows' => 500]);
能用,但每次都要手动传,而且别人在构造函数里类型提示 ReportGenerator 时容器不会自动带这个值。
绑定原始值的做法——在 AppServiceProvider::register() 里一次性配好:
// 从配置文件读
$this->app->when(ReportGenerator::class)
->needs('$maxRows')
->giveConfig('reports.max_rows');
// 或者直接写死
$this->app->when(ReportGenerator::class)
->needs('$maxRows')
->give(500);
之后任何地方类型提示 ReportGenerator,容器自动带上 500。
不想绑怎么办? 在类里直接用 config() 辅助函数读,但这样就绕开了依赖注入,测试时不好 mock。绑定原始值是保持"所有依赖都走容器"这个原则的最干净做法。
绑定可变长参数
有时候一个类用可变长构造函数参数接收一组同类型对象:
<?php
use App\Models\Filter;
use App\Services\Logger;
class Firewall
{
/**
* The filter instances.
*
* @var array
*/
protected $filters;
/**
* Create a new class instance.
*/
public function __construct(
protected Logger $logger,
Filter ...$filters,
) {
$this->filters = $filters;
}
}
用上下文绑定 + 闭包返回解析好的 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),
];
});
简化写法——直接给类名数组,容器自己解析:
$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 接口的实现。注册好各个实现后,用 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);
});
extend 什么场景用?
extend 的核心价值是:不动原始绑定代码,在解析后额外包一层。典型场景:
1. 给第三方的服务加日志——包已经绑了 PaymentGateway,你没法改包的代码,但想记录每次调用:
// AppServiceProvider::register()
$this->app->extend(PaymentGateway::class, function ($gateway, $app) {
return new LoggingPaymentGateway($gateway);
});
// 之后 resolve 出来的 PaymentGateway 全部自带日志,原始绑定代码一行不动
2. 给数据库查询加缓存层——已有 UserRepository,想加 Redis 缓存但不想改类本身:
$this->app->extend(UserRepository::class, function ($repo, $app) {
return new CachedUserRepository($repo, $app->make('cache.store'));
});
3. 配置或修改已解析的实例——某个服务解析出来后你想调个 setter:
$this->app->extend(HttpClient::class, function ($client) {
$client->setTimeout(30);
$client->setDefaultHeader('X-App', 'MyApp');
return $client;
});
跟 bind 的区别:用 bind 重新绑定会覆盖原始注册;用 extend 是"原样解析出来 → 我加工一下 → 返回",原始逻辑不动。当你不拥有绑定的代码时(第三方包、Laravel 内部服务),extend 是唯一选择。
解析
make 方法
用 make 方法从容器中解析一个类实例,传类名或接口名就行:
use App\Services\Transistor;
$transistor = $this->app->make(Transistor::class);
如果某些依赖不能通过容器解析,用 makeWith 方法手动传关联数组:
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);
如果你想要容器实例本身也被注入到一个由容器解析的类里,在构造函数里类型提示 Illuminate\Container\Container:
use Illuminate\Container\Container;
/**
* Create a new class instance.
*/
public function __construct(
protected Container $container,
) {}
Container 和 App 有什么区别?这里怎么用?
本质:Container 和 App 是同一个东西。App 是门面,底层代理的就是 Illuminate\Container\Container 实例。App::make() 等价于 $this->container->make()。
// 三种写法,效果完全一样
$service = App::make(Transistor::class); // 门面
$service = app(Transistor::class); // 辅助函数
$service = $this->container->make(Transistor::class); // 直接拿容器
代码示例的意思:在构造函数里类型提示 Container,意思不是"帮我把 Transistor 注进来",而是"把容器本身给我"——之后我可以随时用它解析任何东西。
class ReportFactory
{
public function __construct(
protected Container $container, // 拿容器本身,不是拿某个服务
) {}
public function make(string $type): Report
{
// 运行时动态解析,类型由参数决定
return $this->container->make('App\\Reports\\' . $type . 'Report');
}
}
实际场景:
- 动态解析:不知道运行时具体要哪个类,根据参数拼接类名再
make() - 策略模式 / 工厂模式:根据输入决定实例化哪个实现
- 需要调用容器的其他方法:
tagged()、bound()、extend()等
为什么不直接 use App? 构造函数里类型提示 Container 是纯依赖注入,好测试——测试时 mock 一个容器就行。用 App::make() 是静态调用,绕开了容器,测试得用 App::partialMock() 才行,更麻烦。
自动注入
更重要也更常用的方式——在由容器解析的类(控制器、事件监听器、中间件等)的构造函数里直接类型提示依赖。你还可以在队列任务的 handle 方法里类型提示依赖。实际工作中,绝大多数对象都应该这样靠容器自动解析。
比如在控制器构造函数里类型提示一个服务,它会自动被解析和注入:
<?php
namespace App\Http\Controllers;
use App\Services\AppleMusic;
class PodcastController extends Controller
{
/**
* Create a new controller instance.
*/
public function __construct(
protected AppleMusic $apple,
) {}
/**
* Show information about the given podcast.
*/
public function show(string $id): Podcast
{
return $this->apple->findPodcast($id);
}
}
方法调用与注入
有时候你想调用一个对象的方法,同时让容器自动注入这个方法的依赖。比如有这样一个类:
<?php
namespace App;
use App\Services\AppleMusic;
class PodcastStats
{
/**
* Generate a new podcast stats report.
*/
public function generate(AppleMusic $apple): array
{
return [
// ...
];
}
}
通过容器来调用 generate 方法:
use App\PodcastStats;
use Illuminate\Support\Facades\App;
$stats = App::call([new PodcastStats, 'generate']);
call 方法接受任何 PHP callable。甚至可以用它调用闭包,自动注入闭包的依赖:
use App\Services\AppleMusic;
use Illuminate\Support\Facades\App;
$result = App::call(function (AppleMusic $apple) {
// ...
});
容器事件
服务容器每次解析对象时都会触发事件。用 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) {
// 容器每次解析任意类型时被调用...
});
被解析的对象会传给回调,你可以在它被交付给使用者之前设置额外的属性。
resolving 写在哪?什么场景用?
写在服务提供者的 register() 或 boot() 里,通常放 AppServiceProvider。
场景1——给每次解析的实例设默认值:比如每次解析 Transistor 都自动给它挂一个 logger:
// AppServiceProvider::register()
$this->app->resolving(Transistor::class, function (Transistor $t, $app) {
$t->setLogger($app->make(Logger::class));
});
场景2——监控/调试:想知道某个服务在请求中被解析了多少次:
$this->app->resolving(function (mixed $object, $app) {
Log::debug('容器解析了:' . $object::class);
});
场景3——给第三方服务的对象设值:包已经绑好了,没法改它的构造函数,用 resolving 在解析后给属性赋值:
$this->app->resolving(ExternalApiClient::class, function ($client) {
$client->setApiKey(config('services.external.key'));
});
注意:resolving 每次解析都触发,如果对象是 singleton,回调只执行一次(第一次),后续从缓存拿不会再触发。而且回调里不要再去解析同一个类——会死循环。
跟 extend 的区别:extend 是"解析出来 → 包一层新对象返回"(装饰器模式);resolving 是"解析出来 → 调个 setter / 记个日志"(副作用),不替换对象本身。
重新绑定
rebinding 用来监听"某个服务被重新绑定了"这个事件。比如你一开始绑了 SpotifyPublisher,后来又被覆盖成了 TransistorPublisher——rebinding 能让你在覆盖发生时做一些收尾工作,比如刷新其他依赖该服务的组件:
use App\Contracts\PodcastPublisher;
use App\Services\SpotifyPublisher;
use App\Services\TransistorPublisher;
use Illuminate\Contracts\Foundation\Application;
// 第一次绑定
$this->app->bind(PodcastPublisher::class, SpotifyPublisher::class);
// 注册 rebinding 监听
$this->app->rebinding(
PodcastPublisher::class,
function (Application $app, PodcastPublisher $newInstance) {
// 别人把 PodcasePublisher 换成了新实现,这里可以同步更新
// 比如清掉旧实现的缓存、重新注册路由等
},
);
// 后来有人在其他地方又 bind 了一次——rebinding 闭包被触发
$this->app->bind(PodcastPublisher::class, TransistorPublisher::class);
什么场景用 rebinding?
日常业务代码基本用不到。主要场景是扩展包开发——包注册了一个服务,但用户可以在 AppServiceProvider 里重新绑定覆盖它,包需要知道"被覆盖了"才能清理旧状态或适配新实现。
写普通业务不用管这个方法。
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。
什么是 PSR-11?为什么 Laravel 要实现它?
PSR-11 是 PHP 社区制定的一套容器标准接口。它只规定了两个方法:get($id) 获取服务,has($id) 判断是否存在。任何实现了这个接口的容器,用法都一样。
Laravel 实现它的目的是解耦:你写一个扩展包,如果类型提示 Psr\Container\ContainerInterface,这个包就能跑在任何支持 PSR-11 的框架里(Symfony、Slim 等),不绑死在 Laravel 身上。
示例代码写在路由文件里(routes/web.php),意思是"把容器以标准接口的方式注入给我":
// 类型提示 PSR-11 标准接口,不依赖 Laravel 专属的 Container
Route::get('/', function (ContainerInterface $container) {
$service = $container->get(Transistor::class); // 等价于 app(Transistor::class)
});
跟前面讲过的类型提示 Illuminate\Container\Container 效果一样,但 ContainerInterface 是标准接口——如果你以后想把这段逻辑抽成独立 PHP 包,不需要改任何 import。
实际项目里基本用不到——写 Laravel 应用就直接 app() 或类型提示具体类就行。这个主要是给写跨框架扩展包的人准备的。