服务容器
介绍
Laravel 服务容器是一个强大的工具,用于管理类的依赖关系。依赖注入是一个看似复杂的术语,实际上它的意思是:类的依赖关系通过构造函数或在某些情况下通过“setter”方法被“注入”到类中。
让我们来看一个简单的例子:
<?php namespace App\Handlers\Commands;
use App\User;
use App\Commands\PurchasePodcastCommand;
use Illuminate\Contracts\Mail\Mailer;
class PurchasePodcastHandler {
/**
* 邮件发送实现。
*/
protected $mailer;
/**
* 创建一个新的实例。
*
* @param Mailer $mailer
* @return void
*/
public function __construct(Mailer $mailer)
{
$this->mailer = $mailer;
}
/**
* 购买播客。
*
* @param PurchasePodcastCommand $command
* @return void
*/
public function handle(PurchasePodcastCommand $command)
{
//
}
}
在这个例子中,PurchasePodcast
命令处理器需要在购买播客时发送电子邮件。因此,我们将注入一个能够发送电子邮件的服务。由于服务是被注入的,我们可以轻松地将其替换为另一个实现。在测试应用程序时,我们也可以轻松地“模拟”或创建邮件发送的虚拟实现。
深入理解 Laravel 服务容器对于构建强大、大型应用程序以及为 Laravel 核心做出贡献至关重要。
基本用法
绑定
几乎所有的服务容器绑定都将在服务提供者中注册,因此所有这些示例都将在该上下文中演示如何使用容器。然而,如果您需要在应用程序的其他地方使用容器实例,例如在工厂中,您可以类型提示 Illuminate\Contracts\Container\Container
合约,容器的实例将被注入给您。或者,您可以使用 App
facade 来访问容器。
注册基本解析器
在服务提供者中,您始终可以通过 $this->app
实例变量访问容器。
服务容器可以通过多种方式注册依赖关系,包括闭包回调和将接口绑定到实现。首先,我们将探讨闭包回调。闭包解析器在容器中注册时使用一个键(通常是类名)和一个返回某个值的闭包:
$this->app->bind('FooBar', function($app)
{
return new FooBar($app['SomethingElse']);
});
注册单例
有时,您可能希望将某些东西绑定到容器中,只解析一次,并在后续调用中返回相同的实例:
$this->app->singleton('FooBar', function($app)
{
return new FooBar($app['SomethingElse']);
});
将现有实例绑定到容器
您还可以使用 instance
方法将现有对象实例绑定到容器中。给定的实例将在后续调用中始终返回:
$fooBar = new FooBar(new SomethingElse);
$this->app->instance('FooBar', $fooBar);
解析
有几种方法可以从容器中解析某些东西。首先,您可以使用 make
方法:
$fooBar = $this->app->make('FooBar');
其次,您可以在容器上使用“数组访问”,因为它实现了 PHP 的 ArrayAccess
接口:
$fooBar = $this->app['FooBar'];
最后,也是最重要的,您可以简单地在由容器解析的类的构造函数中“类型提示”依赖关系,包括控制器、事件监听器、队列作业、过滤器等。容器将自动注入依赖关系:
<?php namespace App\Http\Controllers;
use Illuminate\Routing\Controller;
use App\Users\Repository as UserRepository;
class UserController extends Controller {
/**
* 用户仓库实例。
*/
protected $users;
/**
* 创建一个新的控制器实例。
*
* @param UserRepository $users
* @return void
*/
public function __construct(UserRepository $users)
{
$this->users = $users;
}
/**
* 显示给定 ID 的用户。
*
* @param int $id
* @return Response
*/
public function show($id)
{
//
}
}
将接口绑定到实现
注入具体依赖
服务容器的一个非常强大的功能是能够将接口绑定到给定的实现。例如,假设我们的应用程序集成了 Pusher 网络服务,用于发送和接收实时事件。如果我们使用 Pusher 的 PHP SDK,我们可以将 Pusher 客户端的实例注入到类中:
<?php namespace App\Handlers\Commands;
use App\Commands\CreateOrder;
use Pusher\Client as PusherClient;
class CreateOrderHandler {
/**
* Pusher SDK 客户端实例。
*/
protected $pusher;
/**
* 创建一个新的订单处理器实例。
*
* @param PusherClient $pusher
* @return void
*/
public function __construct(PusherClient $pusher)
{
$this->pusher = $pusher;
}
/**
* 执行给定的命令。
*
* @param CreateOrder $command
* @return void
*/
public function execute(CreateOrder $command)
{
//
}
}
在这个例子中,注入类的依赖关系是好的;然而,我们与 Pusher SDK 紧密耦合。如果 Pusher SDK 方法发生变化,或者我们决定完全切换到新的事件服务,我们将需要更改我们的 CreateOrderHandler
代码。
面向接口编程
为了“隔离” CreateOrderHandler
免受事件推送的变化影响,我们可以定义一个 EventPusher
接口和一个 PusherEventPusher
实现:
<?php namespace App\Contracts;
interface EventPusher {
/**
* 向所有客户端推送一个新事件。
*
* @param string $event
* @param array $data
* @return void
*/
public function push($event, array $data);
}
一旦我们编写了这个接口的 PusherEventPusher
实现,我们可以像这样将其注册到服务容器中:
$this->app->bind('App\Contracts\EventPusher', 'App\Services\PusherEventPusher');
这告诉容器,当一个类需要 EventPusher
的实现时,它应该注入 PusherEventPusher
。现在我们可以在构造函数中类型提示 EventPusher
接口:
/**
* 创建一个新的订单处理器实例。
*
* @param EventPusher $pusher
* @return void
*/
public function __construct(EventPusher $pusher)
{
$this->pusher = $pusher;
}
上下文绑定
有时您可能有两个类使用相同的接口,但希望在每个类中注入不同的实现。例如,当我们的系统接收到一个新订单时,我们可能希望通过 PubNub 而不是 Pusher 发送一个事件。Laravel 提供了一个简单、流畅的接口来定义这种行为:
$this->app->when('App\Handlers\Commands\CreateOrderHandler')
->needs('App\Contracts\EventPusher')
->give('App\Services\PubNubEventPusher');
标记
有时,您可能需要解析某个“类别”的所有绑定。例如,假设您正在构建一个报告聚合器,它接收一个包含许多不同 Report
接口实现的数组。在注册 Report
实现后,您可以使用 tag
方法为它们分配一个标签:
$this->app->bind('SpeedReport', function()
{
//
});
$this->app->bind('MemoryReport', function()
{
//
});
$this->app->tag(['SpeedReport', 'MemoryReport'], 'reports');
一旦服务被标记,您可以通过 tagged
方法轻松解析它们:
$this->app->bind('ReportAggregator', function($app)
{
return new ReportAggregator($app->tagged('reports'));
});
实际应用
Laravel 提供了多种机会使用服务容器来增加应用程序的灵活性和可测试性。一个主要的例子是解析控制器时。所有控制器都是通过服务容器解析的,这意味着您可以在控制器构造函数中类型提示依赖关系,它们将自动被注入。
<?php namespace App\Http\Controllers;
use Illuminate\Routing\Controller;
use App\Repositories\OrderRepository;
class OrdersController extends Controller {
/**
* 订单仓库实例。
*/
protected $orders;
/**
* 创建一个控制器实例。
*
* @param OrderRepository $orders
* @return void
*/
public function __construct(OrderRepository $orders)
{
$this->orders = $orders;
}
/**
* 显示所有订单。
*
* @return Response
*/
public function index()
{
$orders = $this->orders->all();
return view('orders', ['orders' => $orders]);
}
}
在这个例子中,OrderRepository
类将自动注入到控制器中。这意味着在单元测试时,可以将“模拟” OrderRepository
绑定到容器中,从而轻松地对数据库层交互进行存根。
容器使用的其他示例
当然,如上所述,控制器并不是 Laravel 通过服务容器解析的唯一类。您还可以在路由闭包、过滤器、队列作业、事件监听器等上类型提示依赖关系。有关在这些上下文中使用服务容器的示例,请参阅它们的文档。
容器事件
注册解析监听器
每次容器解析对象时都会触发一个事件。您可以使用 resolving
方法监听此事件:
$this->app->resolving(function($object, $app)
{
// 当容器解析任何类型的对象时调用...
});
$this->app->resolving(function(FooBar $fooBar, $app)
{
// 当容器解析 "FooBar" 类型的对象时调用...
});
被解析的对象将被传递给回调。