1. 什么是 Laravel 的 IoC 容器?
Laravel 的 IoC 全称是 Inversion of Control,即控制反转,表示将对象创建的控制权交给了容器,由容器来主动注入需要的对象实例。而 Laravel 为我们提供的 IoC 容器则是一个能够自动化管理类依赖的工具,它为我们创建和提供类对象实例,而无需手动实例化依赖。
1.1 IoC 容器的使用场景
IoC 容器一般用于解决以下两个问题:
减少类之间的耦合度
更方便地在类中使用外部其它类的实例
1.2 Laravel IoC 容器的实现
Laravel 的 IoC 容器实现了顶级容器、上下文容器和绑定解析过程。其中,顶级容器是 Laravel 中最基础的容器,它管理全局范围的实例,而上下文容器则用于衍生自顶级容器的变量和上下文相关的实例。
// 示例代码:
interface PaymentGateway {
public function pay($amount);
}
class Stripe implements PaymentGateway {
public function pay($amount) {
// stripe payment implementation
}
}
class Paypal implements PaymentGateway {
public function pay($amount) {
// paypal payment implementation
}
}
class Shop {
private $paymentGateway;
public function __construct(PaymentGateway $paymentGateway) {
$this->paymentGateway = $paymentGateway;
}
public function purchase($amount) {
$this->paymentGateway->pay($amount);
}
}
以上代码中 Shop 类需要依赖 PaymentGateway 接口,但是并没有实例化该接口。如果直接用 new 去创建 PaymentGateway,这个类就不好维护和扩展,我们就可以利用 Laravel 容器来解决这个问题。
use App\Http\Controllers\Payment\Stripe;
use App\Http\Controllers\Payment\Paypal;
app()->bind(PaymentGateway::class, function() {
return new Stripe(config('services.stripe.secret'));
});
app()->bind(PaymentGateway::class, function() {
return new Paypal(config('paypal'));
});
// 示例代码
$gateway = app()->make(PaymentGateway::class);
$shop = new Shop($gateway);
$shop->purchase(1000);
以上代码通过 app() 函数实例化 PaymentGateway 接口。这样,当需要 PaymentGateway 实例时,Laravel 就会自动创建一个与 Stripe 或 Paypal 相关的实例。
2. IoC 容器的基本使用方法
2.1 绑定
在 Laravel 中,IoC 容器的绑定过程是通过 bind 方法实现的。它告诉容器什么时候需要调用一个实例生成器来创建一个实例。
app()->bind('App\Contracts\EventLogger', function () {
return new FileLogger(storage_path('logs/events.log'));
});
// 通过 Laravel 自带的 helper 函数 app() 去获取容器实例
$logger = app('App\Contracts\EventLogger');
$logger->log('Event');
以上代码中,IoC 容器会创建 FileLogger 的实例去接收 log() 函数传递过来的 event 对象。如果继续传递其他对象,IoC 容器会根据绑定的服务提供者继续创建其它实例。
2.2 依赖注入
依赖注入(Dependency Injection)是在对象初始化时将依赖项传递到该对象中。在 Laravel 中,依赖注入是由 IoC 容器实现的。
use App\Repository\VoucherRepository;
class OrderController extends Controller
{
protected $voucherRepository;
public function __construct(VoucherRepository $voucherRepository) {
$this->voucherRepository = $voucherRepository;
}
public function checkout() {
$voucher = $this->voucherRepository->getRandomVoucher();
return view('checkout', compact('voucher'));
}
}
以上代码中,OrderController 类构造函数接收 VoucherRepository 对象的实例。由于该对象已经通过 IoC 容器绑定,因此容器将会自动创建类对象实例,回避了手动实例化的问题。
2.3 上下文绑定
上下文绑定(Contextual Binding)是一种特殊的机制,可以让我们根据不同的上下文自动绑定不同的依赖项。
class UserController extends Controller
{
public function store(Request $request, StripePaymentGateway $gateway) {
// 使用订阅 ID 创建新用户
}
}
class SubscriptionController extends Controller
{
public function create() {
$gateway = app(StripePaymentGateway::class);
$user = auth()->user();
$gateway->createSubscription($user->paymentMethodId, $user, $planId);
}
}
以上代码中,UserController 类需要一个 StripePaymentGateway 实例,而 SubscriptionController 类也需要一个 StripePaymentGateway 实例,但它们上下文不同类需要不同的 StripePaymentGateway 实例,这时就需要使用上下文绑定了。
use App\Payment\StripePaymentGateway;
app()->when(UserController::class)
->needs(StripePaymentGateway::class)
->give(function () {
return new StripePaymentGateway(auth()->user()->paymentGatewayConfig());
});
app()->when(SubscriptionController::class)
->needs(StripePaymentGateway::class)
->give(function () {
return new StripePaymentGateway(config('services.stripe.secret'));
});
以上代码我们使用了 when 方法代替 bind 绑定函数,通过 needs 方法指定依赖接口,再使用 give 方法提供具体的实例化方法。
2.4 方法注入
通过使用方法注入,我们可以在控制方法生成时向方法传递参数,而不是在类构造函数中注入实例。
class UserController extends Controller
{
public function index(Request $request) {
$user = auth()->user();
$orders = $user->orders();
return view('profile', compact('user', 'orders'));
}
public function checkout(Request $request, PaymentGateway $gateway) {
$voucher = $request->get('voucher');
$amount = $request->get('amount');
$gateway->charge($voucher, $amount);
return redirect()->route('profile');
}
}
Route::get('checkout', 'UserController@checkout')->middleware('auth');
以上代码中,在 checkout 方法中我们需要使用 PaymentGateway 实例,这时我们可以通过 Laravel 的自动解析特性,直接在函数参数列表中添加 PaymentGateway $gateway 这个参数,Laravel 会自动解析 PaymentGateway 的相关实现并注入该参数中,前提是 PaymentGateway 的相关实现已经注册到 IoC 容器中。
3. IoC 容器的高级用法
3.1 容器与服务提供者
服务提供者是 Laravel 提供的一种水平切面编程模式,主要是为了解耦和提供更好的扩展性。服务提供者可以注册容器中的类实例、定义命令、暴露配置、甚至可以在框架启动时运行代码。
namespace App\Providers;
use App\Contracts\EventLogger;
use App\Loggers\FileLogger;
use Illuminate\Support\ServiceProvider;
class LogServiceProvider extends ServiceProvider
{
public function register() {
$this->app->singleton(EventLogger::class, function (app) {
return new FileLogger(storage_path('logs/events.log'));
});
}
}
// 在类中使用
$order = new Order;
$order->setLogger(app(EventLogger::class));
$order->log('Purchase Successful');
以上代码中,我们创建了一个 LogServiceProvider 服务提供者,在该服务提供者中注册了 EventLogger 的 IoC 容器绑定,当需要 EventLogger 实例时,容器会自动创建 FileLogger 类对象实例,并把对象注入到类实例中。
3.2 容器和外部函数
在使用 IoC 容器时,我们也可以使用 callable 函数、字符串等方式来实现外部函数的调用。通常情况下,外部函数调用需要编写对应的服务提供者进行注册。
app()->bind(SomeClass::class, function () {
return new SomeClass(new ExternalService());
});
class SomeController extends Controller
{
public function index(SomeClass $someClass)
{
return $someClass->someMethod();
}
}
可以直接在 Controller 中使用 ExternalService 的实例化,但是如果需要使用多个 Controller 或多个模块时,就需要特别指定该函数所在的模块及路径等信息,生成成型的代码比较繁琐。
3.3 内部并不是唯一的
Laravel 的 IoC 设计中容器并不是唯一的,意味着开发者可以同时创建多个容器,且每个容器各自管理着独立的类实例。
$containerA = new Container;
$containerA->bind(SomeClassA::class, function () {
return new SomeClassA(config('some_value_a');
});
$containerB = new Container;
$containerB->bind(SomeClassB::class, function () {
return new SomeClassB(config('some_value_b');
});
$containerA->when(SomeController::class)->needs(SomeClassA::class)->give(SomeClassA::class, function ($container) {
return $container->makeInContext(SomeClassA::class, ['context' => 'foo']);
});
$someController = $containerA->make(SomeController::class);
$someController->index();
$someClass = $containerB->make(SomeClassB::class);
以上代码中,我们创建了 $containerA 和 $containerB 两个容器,分别绑定了 SomeClassA 和 SomeClassB。在使用 $containerA 时,我们使用 when 方法对 SomeController 类进行了特别处理,但是在 $containerB 中即使 SomeClassB 存在上下文中,也不会调用 $containerA 的 SomeClassA 实例。
4. 总结
本文围绕 Laravel IoC 容器的使用、实现和高级应用展开了详细的解读,希望可以帮助 PHP 开发者更好地理解并掌握 IoC 容器。实际上,IoC 容器并不仅仅只局限于 Laravel 框架,它已经成为了面向对象编程的一个标配,无论何种编程语言都需要开发者在编写程序的过程中掌握其中的核心概念。希望大家可以在编程之时,能够融入此一编码理念,并派生出更多适用性更强的编程思路。