Skip to content

服务容器

介绍

Laravel 服务容器是一个强大的工具,用于管理类的依赖关系。依赖注入是一个看似复杂的术语,实际上它的意思是:类的依赖关系通过构造函数或在某些情况下通过“setter”方法被“注入”到类中。

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

php
<?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 实例变量访问容器。

服务容器可以通过多种方式注册依赖关系,包括闭包回调和将接口绑定到实现。首先,我们将探讨闭包回调。闭包解析器在容器中注册时使用一个键(通常是类名)和一个返回某个值的闭包:

php
$this->app->bind('FooBar', function($app)
{
	return new FooBar($app['SomethingElse']);
});

注册单例

有时,您可能希望将某些东西绑定到容器中,只解析一次,并在后续调用中返回相同的实例:

php
$this->app->singleton('FooBar', function($app)
{
	return new FooBar($app['SomethingElse']);
});

将现有实例绑定到容器

您还可以使用 instance 方法将现有对象实例绑定到容器中。给定的实例将在后续调用中始终返回:

php
$fooBar = new FooBar(new SomethingElse);

$this->app->instance('FooBar', $fooBar);

解析

有几种方法可以从容器中解析某些东西。首先,您可以使用 make 方法:

php
$fooBar = $this->app->make('FooBar');

其次,您可以在容器上使用“数组访问”,因为它实现了 PHP 的 ArrayAccess 接口:

php
$fooBar = $this->app['FooBar'];

最后,也是最重要的,您可以简单地在由容器解析的类的构造函数中“类型提示”依赖关系,包括控制器、事件监听器、队列作业、过滤器等。容器将自动注入依赖关系:

php
<?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
<?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
<?php namespace App\Contracts;

interface EventPusher {

	/**
	 * 向所有客户端推送一个新事件。
	 *
	 * @param  string  $event
	 * @param  array  $data
	 * @return void
	 */
	public function push($event, array $data);

}

一旦我们编写了这个接口的 PusherEventPusher 实现,我们可以像这样将其注册到服务容器中:

php
$this->app->bind('App\Contracts\EventPusher', 'App\Services\PusherEventPusher');

这告诉容器,当一个类需要 EventPusher 的实现时,它应该注入 PusherEventPusher。现在我们可以在构造函数中类型提示 EventPusher 接口:

php
/**
	 * 创建一个新的订单处理器实例。
	 *
	 * @param  EventPusher  $pusher
	 * @return void
	 */
	public function __construct(EventPusher $pusher)
	{
		$this->pusher = $pusher;
	}

上下文绑定

有时您可能有两个类使用相同的接口,但希望在每个类中注入不同的实现。例如,当我们的系统接收到一个新订单时,我们可能希望通过 PubNub 而不是 Pusher 发送一个事件。Laravel 提供了一个简单、流畅的接口来定义这种行为:

php
$this->app->when('App\Handlers\Commands\CreateOrderHandler')
          ->needs('App\Contracts\EventPusher')
          ->give('App\Services\PubNubEventPusher');

标记

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

php
$this->app->bind('SpeedReport', function()
{
	//
});

$this->app->bind('MemoryReport', function()
{
	//
});

$this->app->tag(['SpeedReport', 'MemoryReport'], 'reports');

一旦服务被标记,您可以通过 tagged 方法轻松解析它们:

php
$this->app->bind('ReportAggregator', function($app)
{
	return new ReportAggregator($app->tagged('reports'));
});

实际应用

Laravel 提供了多种机会使用服务容器来增加应用程序的灵活性和可测试性。一个主要的例子是解析控制器时。所有控制器都是通过服务容器解析的,这意味着您可以在控制器构造函数中类型提示依赖关系,它们将自动被注入。

php
<?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 方法监听此事件:

php
$this->app->resolving(function($object, $app)
{
	// 当容器解析任何类型的对象时调用...
});

$this->app->resolving(function(FooBar $fooBar, $app)
{
	// 当容器解析 "FooBar" 类型的对象时调用...
});

被解析的对象将被传递给回调。