服务容器

Introduction

Laravel 服务容器是管理类依赖和执行依赖注入的强大工具。依赖注入是一个奇特的短语,本质上意味着:类依赖通过构造函数或在某些情况下通过“setter”方法“注入”到类中。

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

<?php

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 需要从数据源中检索用户。所以,我们将inject 能够检索用户的服务。在这种情况下,我们的UserRepository 最有可能使用Eloquent 从数据库中检索用户信息。然而,由于存储库是注入的,我们可以很容易地用另一个实现替换它。我们还可以轻松地“模拟”或创建一个虚拟实现UserRepository 在测试我们的应用程序时。

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

零配置解析

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

<?php

class Service
{
    // ...
}

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

在此示例中,点击您的应用程序的/ 路由会自动解析Service 类并将其注入到路由的处理程序中。这是改变游戏规则。这意味着您可以开发应用程序并利用依赖注入,而不必担心配置文件过大。

值得庆幸的是,您在构建 Laravel 应用程序时将编写的许多类会自动通过容器接收它们的依赖项,包括controllers,事件监听器,middleware, 和更多。此外,您可以在handle 的方法排队的工作.一旦您尝到了自动和零配置依赖注入的强大功能,就会觉得没有它就无法进行开发。

何时使用容器

由于零配置解析,您通常会在路由、控制器、事件侦听器和其他地方进行类型提示依赖,而无需手动与容器交互。例如,您可以键入提示Illuminate\Http\Request 在您的路由定义上添加对象,以便您可以轻松访问当前请求。即使我们永远不必与容器交互来编写这段代码,它也在幕后管理着这些依赖项的注入:

use Illuminate\Http\Request;

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

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

首先,如果你写了一个实现接口的类,并且你希望在路由或类构造函数上对该接口进行类型提示,你必须告诉容器如何解析该接口.其次,如果你是写一个 Laravel 包 如果你打算与其他 Laravel 开发人员共享,你可能需要将你的包的服务绑定到容器中。

Binding

绑定基础

简单绑定

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

在服务提供者中,您始终可以通过$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 facade:

use App\Services\Transistor;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Support\Facades\App;

App::bind(Transistor::class, function (Application $app) {
    // ...
});

Note
如果类不依赖于任何接口,则无需将它们绑定到容器中。不需要指示容器如何构建这些对象,因为它可以使用反射自动解析这些对象。

绑定单例

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 辛烷值 worker 处理一个新的请求或者当一个 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);

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

use App\Contracts\EventPusher;

/**
 * Create a new class instance.
 */
public function __construct(
    protected EventPusher $pusher
) {}

上下文绑定

有时您可能有两个使用相同接口的类,但您希望为每个类注入不同的实现。例如,两个控制器可能依赖于不同的实现Illuminate\Contracts\Filesystem\Filesystem contract. 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);

有时一个类可能依赖于一个数组tagged 实例。使用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,
          ]);

可变标签依赖

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

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

Tagging

有时,您可能需要解析所有某个“类别”的绑定。例如,也许您正在构建一个报告分析器,它接收许多不同的数组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);
});

Resolving

make 方法

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

use App\Services\Transistor;

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

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

use App\Services\Transistor;

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

如果您在您的代码所在位置的服务提供商之外,而该位置无权访问$app 变量,你可以使用App facade 或者app helper 从容器中解析类实例:

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

自动注射

或者,重要的是,您可以在由容器解析的类的构造函数中对依赖项进行类型提示,包括controllers,事件监听器,middleware, 和更多。此外,您可以在handle 的方法排队的工作.实际上,这就是容器应该解析的大多数对象的方式。

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

<?php

namespace App\Http\Controllers;

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

class UserController extends Controller
{
    /**
     * Create a new controller instance.
     */
    public function __construct(
        protected UserRepository $users,
    ) {}

    /**
     * Show the user with the given 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) {
    // Called when container resolves objects of type "Transistor"...
});

$this->app->resolving(function (mixed $object, Application $app) {
    // Called when container resolves object of any type...
});

如您所见,正在解析的对象将传递给回调,允许您在将对象提供给其使用者之前在该对象上设置任何其他属性。

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 将被抛出。

豫ICP备18041297号-2