什么是服务容器
Laravel 的服务容器是用来管理类依赖并进行依赖注入的机制。所谓依赖注入,指的是通过构造器或(在某些情况下)setter 方法,将类所需的依赖「注入」到类中。
来看下面的示例:
<?php
namespace App\Http\Controllers;
use App\Services\AppleMusic;
use Illuminate\View\View;
class PodcastController extends Controller
{
/**
* 创建新的控制器实例
*/
public function __construct(
protected AppleMusic $apple,
) {}
/**
* 显示指定 podcast 的信息
*/
public function show(string $id): View
{
return view('podcasts.show', [
'podcast' => $this->apple->findPodcast($id)
]);
}
}
此示例中,PodcastController 需要从 Apple Music 等数据源获取 Podcast。因此我们将能够获取 Podcast 的服务注入给它。这样一来,在测试时就可以很方便地替换 AppleMusic 服务的 mock(假实现)。
深入理解服务容器对于构建大型 Laravel 应用不可或缺,也有助于为 Laravel 核心贡献代码。
零配置解析
如果类只依赖其他具体类(而不是接口),就无需告诉容器如何解析它。例如你在 routes/web.php 中写下这段代码:
<?php
class Service
{
// ...
}
Route::get('/', function (Service $service) {
dd($service::class);
});
这个例子把类定义在路由文件内只是为了演示。真实应用中,服务类请放到 app/Services 目录下。
访问该路由时,Laravel 会自动解析 Service 类并注入到路由处理器中。你无需任何配置就能享受依赖注入。
Laravel 应用中编写的许多类(控制器、事件监听器、中间件等)都会通过容器自动注入依赖。
基本绑定
大部分绑定都会在服务提供者中注册。在服务提供者中可以通过 $this->app 属性访问容器。
bind
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) {
// ...
});
不依赖接口的类无需向容器注册绑定。容器可以通过反射自动解析这类对象。
singleton
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));
});
instance
也可以通过 instance 方法把已有的对象实例绑定到容器上。之后对该绑定的解析都会返回该实例。
use App\Services\Transistor;
use App\Services\PodcastParser;
$service = new Transistor(new PodcastParser);
$this->app->instance(Transistor::class, $service);
将接口绑定到实现
服务容器的一个强大功能是可以把接口绑定到特定实现。以 EventPusher 接口与 RedisEventPusher 实现为例:
use App\Contracts\EventPusher;
use App\Services\RedisEventPusher;
$this->app->bind(EventPusher::class, RedisEventPusher::class);
之后,容器就会给需要 EventPusher 实现的类注入 RedisEventPusher。你只需在构造器中类型提示 EventPusher 接口即可。
use App\Contracts\EventPusher;
/**
* 创建新的类实例
*/
public function __construct(
protected EventPusher $pusher,
) {}
依赖接口后,即使替换实现也无需修改代码,测试和未来的更改都更方便。
自动解析(通过类型提示进行 DI)
服务容器在解析控制器、事件监听器、中间件等类时,会读取构造器的类型提示并自动注入依赖。
<?php
namespace App\Http\Controllers;
use App\Repositories\UserRepository;
class UserController extends Controller
{
/**
* 创建新的控制器实例
*/
public function __construct(
protected UserRepository $users,
) {}
}
只要 UserRepository 不依赖接口,就无需向容器注册。当访问对应路由时,容器会自动解析依赖并注入控制器。
从容器解析
make 方法
使用 make 方法可以从容器中解析类实例。
use App\Services\Transistor;
$transistor = app()->make(Transistor::class);
若某个依赖无法被容器解析,可以使用 makeWith 方法额外传入参数。
$transistor = $this->app->makeWith(Transistor::class, ['id' => 1]);
自动注入
实际开发中很少直接调用 make。只要在容器负责解析的类(控制器、事件监听器、中间件等)的构造器上加上类型提示,容器就会自动注入依赖。
门面与容器的关系
Laravel 的门面为容器中的对象提供了静态接口。例如,Cache::get() 在内部会从容器获取 Cache 服务并调用。
use Illuminate\Support\Facades\Cache;
// 通过门面调用
Cache::get('key');
// 直接使用容器的等价调用
app('cache')->get('key');
门面是容器的便捷包装。测试时也可以把门面替换成 mock。
use Illuminate\Support\Facades\Cache;
Cache::shouldReceive('get')
->once()
->with('key')
->andReturn('value');
构造器注入实战示例
来看实际应用中一种典型模式。
定义接口
<?php
namespace App\Contracts;
interface PaymentGateway
{
public function charge(int $amount, string $token): bool;
}
创建实现类
<?php
namespace App\Services;
use App\Contracts\PaymentGateway;
class StripePaymentGateway implements PaymentGateway
{
public function charge(int $amount, string $token): bool
{
// 使用 Stripe API 进行扣款...
return true;
}
}
在服务提供者中绑定
use App\Contracts\PaymentGateway;
use App\Services\StripePaymentGateway;
$this->app->singleton(PaymentGateway::class, StripePaymentGateway::class);
在控制器中接收注入
<?php
namespace App\Http\Controllers;
use App\Contracts\PaymentGateway;
use Illuminate\Http\Request;
class OrderController extends Controller
{
public function __construct(
protected PaymentGateway $payment,
) {}
public function store(Request $request)
{
$this->payment->charge(
$request->amount,
$request->payment_token
);
// ...
}
}
采用这种模式后,即使把支付服务从 Stripe 换成其他供应商,也只需修改一处绑定。
下一步