服务容器


介绍

Laravel 服务容器是一种管理类依赖和执行依赖注入的强大工具。依赖注入是一种高级技术,它本质上意味着类依赖通过构造函数或者某些情况下的“setter”方法“注入”到类中。

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

namespace App\Http\Controllers;
 
use App\Http\Controllers\Controller;
use App\Repositories\UserRepository;
use App\Models\User;
use Illuminate\View\View;
 
class UserController extends Controller
{
    /**
     * Create a new controller instance.
     */
    public function __construct(
        protected UserRepository $users,
    ) {}
 
    /**
     * Show the profile for the given user.
     */
    public function show(string $id): View
    {
        $user = $this->users->find($id);
 
        return view('user.profile', ['user' => $user]);
    }
}

在这个例子中,UserController 需要从数据源中检索用户。因此,我们将注入一个能够检索用户的服务。在这个上下文中,我们的 UserRepository 很可能使用 Eloquent 从数据库中检索用户信息。然而,由于该存储库是注入的,我们可以很容易地将其替换为另一个实现。我们也可以在测试应用程序时轻松地“模拟”或创建一个 UserRepository 的虚拟实现。

深入了解 Laravel 服务容器对于构建强大的大型应用程序以及为 Laravel 核心做出贡献都是至关重要的。

零配置解析

如果一个类没有依赖或只依赖于其他具体类(而不是接口),容器就不需要指示如何解析该类。例如,您可以在 routes/web.php 文件中放置以下代码:

class Service
{
    // ...
}
 
Route::get('/', function (Service $service) {
    die(get_class($service));
});

在这个例子中,访问您的应用程序的 / 路由将自动解析 Service 类并将其注入到您的路由处理程序中。这是一个重大的改变。这意味着您可以开发应用程序并利用依赖注入,而不必担心臃肿的配置文件。

值得庆幸的是,在构建 Laravel 应用程序时,您将编写的许多类都会自动通过容器接收它们的依赖项,包括控制器事件监听器中间件等等。此外,您可以在排队的作业handle 方法中声明依赖关系。一旦尝试了自动和零配置的依赖注入,就会感到不可思议,无法再开发没有它的应用程序。

何时使用容器

由于零配置解析,您通常会在路由、控制器、事件监听器等地方声明依赖关系,而无需手动与容器交互。例如,您可能在路由定义中声明 Illuminate\Http\Request 对象的依赖关系,以便可以轻松地访问当前请求。即使我们不必与容器交互来编写此代码,它仍在幕后管理这些依赖关系的注入:

use Illuminate\Http\Request;
 
Route::get('/', function (Request $request) {
    // ...
});

在许多情况下,由于自动依赖注入和门面,您可以构建 Laravel 应用程序,而无需手动绑定或解析容器中的任何内容。那么,什么时候才需要手动与容器交互呢?让我们看两种情况。

首先,如果您编写了一个实现接口的类,并希望在路由或类构造函数中声明对该接口的依赖关系,您必须告诉容器如何解析该接口。其次,如果您正在编写一个 Laravel ,计划与其他 Laravel 开发人员共享它,您可能需要将包的服务绑定到容器中。

绑定

绑定基础

简单绑定

几乎所有的服务容器绑定都将在服务提供者中进行注册,因此这些示例大多数都将演示在该上下文中使用容器。

在服务提供者中,您始终可以通过 $this->app 属性访问容器。我们可以使用 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 方法将一个类或接口绑定到容器中,该类或接口只应在解析时被解析一次。一旦单例绑定被解析,容器下一次调用该类或接口时,将返回同一个对象实例:

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));
});

绑定作用域单例

scoped 方法将一个类或接口绑定到容器中,该类或接口在给定的 Laravel 请求/作业生命周期内只能被解析一次。尽管此方法类似于 singleton 方法,但使用 scoped 方法注册的实例将在 Laravel 应用程序启动新的“生命周期”(例如当 Laravel Octane 工作进程处理新的请求或当 Laravel 队列工作进程处理新的作业)时被清除:

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));
});

绑定实例

您也可以使用 instance 方法将现有的对象实例绑定到容器中。容器下一次调用该类或接口时,将始终返回给定的实例:

use App\Services\Transistor;
use App\Services\PodcastParser;
 
$service = new Transistor(new PodcastParser);
 
$this->app->instance(Transistor::class, $service);

绑定接口到实现类

服务容器的一个非常强大的功能是它能够将一个接口绑定到一个特定的实现类。例如,假设我们有一个 EventPusher 接口和一个 RedisEventPusher 实现。一旦我们编写了这个接口的 RedisEventPusher 实现,我们可以像这样在服务容器中注册它:

use App\Contracts\EventPusher;
use App\Services\RedisEventPusher;
 
$this->app->bind(EventPusher::class, RedisEventPusher::class);

这个语句告诉容器,在一个类需要 EventPusher 接口的实现时注入 RedisEventPusher。现在我们可以在由容器解析的类的构造函数中类型提示 EventPusher 接口。请记住,控制器、事件监听器、中间件和 Laravel 应用程序中的各种其他类型的类始终是使用容器解析的。

例如:

use App\Contracts\EventPusher;
 
/**
 * Create a new class instance.
 */
public function __construct(
    protected EventPusher $pusher
) {}

上下文绑定

有时候您可能有两个类使用相同的接口,但希望将不同的实现注入到每个类中。例如,两个控制器可能依赖于 Illuminate\Contracts\Filesystem\Filesystem 契约的不同实现。 Laravel 提供了一种简单的流畅接口来定义这种行为:

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;
 
$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');
          });

上述代码将不同的文件系统注入到不同的控制器中。这种方式称为上下文绑定,允许您根据类、接口和其他条件,将不同的实现绑定到容器中。

绑定基本类型

有时候,一个类需要接收一些注入的类,同时需要一个基本类型的注入,例如一个整数。您可以使用上下文绑定轻松地注入您的类所需的任何值:

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');

绑定类型的可变参数

有时,您可能有一个类,使用可变构造函数参数接收一个类型化对象数组:

<?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;
    }
}

使用上下文绑定,您可以通过提供 give 方法的闭包来解决此依赖关系,该闭包返回已解析的 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),
                ];
          });

为了方便,您还可以提供一个类名数组,以便在 Firewall 需要 Filter 实例时由容器解析:

$this->app->when(Firewall::class)
          ->needs(Filter::class)
          ->give([
              NullFilter::class,
              ProfanityFilter::class,
              TooLongFilter::class,
          ]);

Variadic Tag Dependencies(可变标记依赖)

有时,一个类可能有一个可变的依赖关系,它被类型提示为一个给定的类 (Report ...$reports)。使用 needsgiveTagged 方法,您可以轻松地为给定的依赖项注入具有该标记的所有容器绑定:

$this->app->when(ReportAggregator::class)
    ->needs(Report::class)
    ->giveTagged('reports');

标签

有时候,您可能需要解决所有特定类别的绑定。例如,也许您正在构建一个报告分析器,该分析器接收多个不同的 Report 接口实现的数组。注册 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);
});

解析

make 方法

您可以使用 make 方法从容器中解析类实例。make 方法接受您想要解析的类或接口的名称:

use App\Services\Transistor;

$transistor = $this->app->make(Transistor::class);

如果您的类的某些依赖关系无法通过容器解析,您可以将它们作为关联数组传递到 makeWith 方法中进行注入。例如,我们可以手动传递 Transistor 服务所需的 $id 构造函数参数:

use App\Services\Transistor;

$transistor = $this->app->makeWith(Transistor::class, ['id' => 1]);

如果您在一个无法访问 $app 变量的代码位置的服务提供程序外部,您可以使用 App 门面app 辅助函数从容器中解析类实例:

use App\Services\Transistor; 
use Illuminate\Support\Facades\App;

$transistor = App::make(Transistor::class);

$transistor = app(Transistor::class);

如果您想让 Laravel 容器实例本身被注入到由容器解析的类中,您可以在类的构造函数上使用 Illuminate\Container\Container 类型提示:

use Illuminate\Container\Container;
 
/**
 * Create a new class instance.
 */
public function __construct(
    protected Container $container
) {}

自动注入

你也可以在使用容器自动解析对象时,在类的构造函数中使用类型提示,例如在控制器、事件监听器中间件等类中。另外,在 Laravel 的队列任务的 handle 方法中也可以使用类型提示。实际上,这也是大部分情况下应该如何从容器中解析对象的方法。

例如,在控制器的构造函数中可以使用类型提示注入应用程序定义的存储库。存储库将自动被解析并注入该类:

namespace App\Http\Controllers;

use App\Repositories\UserRepository;
use App\Models\User;

class UserController extends Controller
{
    /**
     * 创建一个新的控制器实例
     */
    public function __construct(
        protected UserRepository $users,
    ) {}

    /**
     * 显示给定 ID 的用户。
     */
    public function show(string $id): User
    {
        $user = $this->users->findOrFail($id);

        return $user;
    }
}

方法调用和注入

有时您可能希望调用对象实例上的一个方法,同时允许容器自动注入该方法的依赖项。例如,给定以下类:

<?php
 
namespace App;
 
use App\Repositories\UserRepository;
 
class UserReport
{
    /**
     * Generate a new user report.
     */
    public function generate(UserRepository $repository): array
    {
        return [
            // ...
        ];
    }
}

您可以通过容器调用 generate 方法,如下所示:

use App\UserReport;
use Illuminate\Support\Facades\App;
 
$report = App::call([new UserReport, 'generate']);

call 方法接受任何 PHP 可调用项。容器的 call 方法甚至可以用于调用一个闭包并自动注入它的依赖项:

use App\Repositories\UserRepository;
use Illuminate\Support\Facades\App;
 
$result = App::call(function (UserRepository $repository) {
    // ...
});

服务容器事件

服务容器在解析对象时会触发一个事件。您可以使用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) {
    // 当容器解析任何类型的对象时调用...
});

可以看到,正在解析的对象将传递给回调,允许您在将对象提供给其使用者之前设置任何其他属性。

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 的实例。


点赞 取消点赞 收藏 取消收藏

<< 上一篇: 请求生命周期

>> 下一篇: 服务提供者