翻译进度
14
分块数量
1
参与人数

服务容器

这是一篇协同翻译的文章,你可以点击『我来翻译』按钮来参与翻译。


服务容器

介绍

Laravel 服务容器是一个用于管理类依赖关系和执行依赖注入的强大工具。依赖注入(Dependency Injection)是一个听起来比较高级的术语,本质上它的意思是:类的依赖会通过构造函数,或者在某些情况下,通过 “setter” 方法被注入到类中。

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

<?php

namespace App\Http\Controllers;

use App\Services\AppleMusic;
use Illuminate\View\View;

class PodcastController extends Controller
{
    /**
     * 创建一个新的控制器实例。
     */
    public function __construct(
        protected AppleMusic $apple,
    ) {}

    /**
     * 显示指定播客的信息。
     */
    public function show(string $id): View
    {
        return view('podcasts.show', [
            'podcast' => $this->apple->findPodcast($id)
        ]);
    }
}

在这个示例中,PodcastController 需要从类似 Apple Music 的数据源中获取播客。因此,我们将会注入(inject)一个能够获取播客的服务。由于该服务是通过注入的方式提供的,因此在测试应用程序时,我们可以轻松地对 AppleMusic 服务进行 “mock(模拟)”,或者创建一个虚拟实现(dummy implementation)。

深入理解 Laravel 服务容器,对于构建强大且大型的应用程序至关重要,同时对于参与 Laravel 核心开发也非常重要。

无与伦比 翻译于 3天前

零配置解析

如果一个类没有依赖,或者它只依赖于其他具体类(而不是接口),那么容器无需被告知如何解析该类。例如,你可以在 routes/web.php 文件中放置以下代码:

<?php

class Service
{
    // ...
}

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

在这个示例中,访问应用程序的 / 路由时,Laravel 会自动解析 Service 类,并将其注入到路由处理程序中。这是一个颠覆性的特性。这意味着你可以在开发应用程序时利用依赖注入,而无需担心臃肿的配置文件。

幸运的是,在构建 Laravel 应用程序时,你编写的许多类都会自动通过容器接收它们的依赖,包括 控制器事件监听器中间件 等等。此外,你还可以在队列任务(queued jobs)handle 方法中进行依赖类型提示。一旦体验过自动化、零配置依赖注入的强大功能,你会感觉离开它几乎无法开发。

何时使用容器

得益于零配置解析,你通常可以在路由、控制器、事件监听器以及其他地方通过类型提示依赖,而无需手动与容器交互。例如,你可能会在路由定义中对 Illuminate\Http\Request 对象进行类型提示,以便轻松访问当前请求。尽管我们从未需要直接与容器交互来编写这段代码,但实际上容器正在幕后管理这些依赖的注入:

use Illuminate\Http\Request;

Route::get('/', function (Request $request) {
    // ...
});
无与伦比 翻译于 3天前

在很多情况下,由于自动依赖注入和 facades 的存在,你可以构建 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));
});

请注意,我们将容器本身作为解析器的参数接收。然后,我们可以使用容器解析正在构建对象的子依赖。

无与伦比 翻译于 2天前

如前所述,你通常会在服务提供者中与容器进行交互;但是,如果你希望在服务提供者之外与容器交互,也可以通过 App facade 来实现:

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

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

你可以使用 bindIf 方法,仅在给定类型尚未注册绑定时注册容器绑定:

$this->app->bindIf(Transistor::class, function (Application $app) {
    return new Transistor($app->make(PodcastParser::class));
});

为了方便起见,你可以省略单独提供要注册的类或接口名称,而是让 Laravel 根据你传递给 bind 方法的闭包返回类型自动推断类型:

App::bind(function (Application $app): Transistor {
    return new Transistor($app->make(PodcastParser::class));
});

[!注意]
如果类不依赖任何接口,则无需将其绑定到容器中。容器无需被告知如何构建这些对象,因为它可以通过反射自动解析这些对象。

绑定单例(Binding A Singleton)

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));
});
无与伦比 翻译于 2天前

你可以使用 singletonIf 方法,仅在给定类型尚未注册绑定时注册单例容器绑定:

$this->app->singletonIf(Transistor::class, function (Application $app) {
    return new Transistor($app->make(PodcastParser::class));
});

单例属性

另外,你可以使用 #[Singleton] 属性标记一个接口或类,以告知容器该对象只应被解析一次:

<?php

namespace App\Services;

use Illuminate\Container\Attributes\Singleton;

#[Singleton]
class Transistor
{
    // ...
}

绑定作用域单例

scoped 方法会将一个类或接口绑定到容器中,并且在单个 Laravel 请求 / 任务生命周期内只会被解析一次。虽然此方法与 singleton 方法类似,但通过 scoped 方法注册的实例会在 Laravel 应用启动新的“生命周期”时被清除,例如当 Laravel Octane worker 处理新的请求,或 Laravel 队列 worker 处理新的任务时:

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

你可以使用 scopedIf 方法,仅在给定类型尚未注册绑定时注册作用域容器绑定:

$this->app->scopedIf(Transistor::class, function (Application $app) {
    return new Transistor($app->make(PodcastParser::class));
});

作用域属性

另外,你可以使用 #[Scoped] 属性标记一个接口或类,以告知容器该对象在单个 Laravel 请求 / 任务生命周期内只应被解析一次:

<?php

namespace App\Services;

use Illuminate\Container\Attributes\Scoped;

#[Scoped]
class Transistor
{
    // ...
}
无与伦比 翻译于 2天前

绑定实例

你也可以使用 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,
) {}

绑定属性

Laravel 还提供了 Bind 属性以提高便利性。你可以将此属性应用于任何接口,以告知 Laravel 在请求该接口时应自动注入哪个实现。使用 Bind 属性时,无需在应用的服务提供者中执行任何额外的服务注册。

无与伦比 翻译于 2天前

此外,你可以在同一个接口上放置多个 Bind 属性,以便为不同环境配置应注入的不同实现:

<?php

namespace App\Contracts;

use App\Services\FakeEventPusher;
use App\Services\RedisEventPusher;
use Illuminate\Container\Attributes\Bind;

#[Bind(RedisEventPusher::class)]
#[Bind(FakeEventPusher::class, environments: ['local', 'testing'])]
interface EventPusher
{
    // ...
}

此外,还可以应用 SingletonScoped 属性,以指示容器绑定应当只解析一次,还是在每个请求 / 任务生命周期中解析一次:

use App\Services\RedisEventPusher;
use Illuminate\Container\Attributes\Bind;
use Illuminate\Container\Attributes\Singleton;

#[Bind(RedisEventPusher::class)]
#[Singleton]
interface EventPusher
{
    // ...
}

上下文绑定

有时,你可能有两个类都使用同一个接口,但你希望为每个类注入不同的实现。例如,两个控制器可能依赖于 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');
    });

上下文属性

由于上下文绑定通常用于注入驱动实现或配置值,Laravel 提供了多种上下文绑定属性,使你无需在服务提供者中手动定义上下文绑定,即可注入这些类型的值。

无与伦比 翻译于 2天前

例如,可以使用 Storage 属性来注入特定的 存储磁盘(storage disk)

<?php

namespace App\Http\Controllers;

use Illuminate\Container\Attributes\Storage;
use Illuminate\Contracts\Filesystem\Filesystem;

class PhotoController extends Controller
{
    public function __construct(
        #[Storage('local')] protected Filesystem $filesystem
    ) {
        // ...
    }
}

除了 Storage 属性外,Laravel 还提供了 AuthCacheConfigContextDBGiveLogRouteParameterTag 属性:

<?php

namespace App\Http\Controllers;

use App\Contracts\UserRepository;
use App\Models\Photo;
use App\Repositories\DatabaseRepository;
use Illuminate\Container\Attributes\Auth;
use Illuminate\Container\Attributes\Cache;
use Illuminate\Container\Attributes\Config;
use Illuminate\Container\Attributes\Context;
use Illuminate\Container\Attributes\DB;
use Illuminate\Container\Attributes\Give;
use Illuminate\Container\Attributes\Log;
use Illuminate\Container\Attributes\RouteParameter;
use Illuminate\Container\Attributes\Tag;
use Illuminate\Contracts\Auth\Guard;
use Illuminate\Contracts\Cache\Repository;
use Illuminate\Database\Connection;
use Psr\Log\LoggerInterface;

class PhotoController extends Controller
{
    public function __construct(
        #[Auth('web')] protected Guard $auth,
        #[Cache('redis')] protected Repository $cache,
        #[Config('app.timezone')] protected string $timezone,
        #[Context('uuid')] protected string $uuid,
        #[Context('ulid', hidden: true)] protected string $ulid,
        #[DB('mysql')] protected Connection $connection,
        #[Give(DatabaseRepository::class)] protected UserRepository $users,
        #[Log('daily')] protected LoggerInterface $log,
        #[RouteParameter('photo')] protected Photo $photo,
        #[Tag('reports')] protected iterable $reports,
    ) {
        // ...
    }
}

此外,Laravel 提供了一个 CurrentUser 属性,用于将当前已认证用户注入到指定路由或类中:

use App\Models\User;
use Illuminate\Container\Attributes\CurrentUser;

Route::get('/user', function (#[CurrentUser] User $user) {
    return $user;
})->middleware('auth');
无与伦比 翻译于 1天前

定义自定义属性

你可以通过实现 Illuminate\Contracts\Container\ContextualAttribute 契约来创建自己的上下文属性。容器将调用属性的 resolve 方法,该方法应解析出应注入到使用该属性类中的值。在下面的示例中,我们将重新实现 Laravel 内置的 Config 属性:

<?php

namespace App\Attributes;

use Attribute;
use Illuminate\Contracts\Container\Container;
use Illuminate\Contracts\Container\ContextualAttribute;

#[Attribute(Attribute::TARGET_PARAMETER)]
class Config implements ContextualAttribute
{
    /**
     * 创建一个新的属性实例。
     */
    public function __construct(public string $key, public mixed $default = null)
    {
    }

    /**
     * 解析配置值。
     *
     * @param  self  $attribute
     * @param  \Illuminate\Contracts\Container\Container  $container
     * @return mixed
     */
    public static function resolve(self $attribute, Container $container)
    {
        return $container->make('config')->get($attribute->key, $attribute->default);
    }
}

绑定原始值

有时,你可能有一个类既接收一些注入的类,同时也需要注入一个原始值,例如整数。你可以轻松地使用上下文绑定来注入类所需的任何值:

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');
无与伦比 翻译于 1天前

绑定类型化可变参数

有时,你可能有一个类通过可变参数构造函数接收一个类型化对象数组:

<?php

use App\Models\Filter;
use App\Services\Logger;

class Firewall
{
    /**
     * 过滤器实例。
     *
     * @var array
     */
    protected $filters;

    /**
     * 创建一个新的类实例。
     */
    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');

标签

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

$this->app->bind(CpuReport::class, function () {
    // ...
});

$this->app->bind(MemoryReport::class, function () {
    // ...
});

$this->app->tag([CpuReport::class, MemoryReport::class], 'reports');
无与伦比 翻译于 1天前

服务被打上标签后,你可以通过容器的 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]);

bound 方法可用于判断某个类或接口是否已经在容器中被显式绑定:

if ($this->app->bound(Transistor::class)) {
    // ...
}

如果你位于服务提供者之外,并且代码位置无法访问 $app 变量,你可以使用 App facadeapp 辅助函数(helper) 从容器中解析类实例:

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

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

$transistor = app(Transistor::class);
无与伦比 翻译于 1天前

如果你希望将 Laravel 容器实例本身注入到一个由容器解析的类中,你可以在类的构造函数中对 Illuminate\Container\Container 类进行类型提示:

use Illuminate\Container\Container;

/**
 * 创建一个新的类实例。
 */
public function __construct(
    protected Container $container,
) {}

自动注入

另外,更重要的是,你可以在由容器解析的类构造函数中对依赖进行类型提示,包括 控制器(controllers)事件监听器(event listeners)中间件(middleware) 等。此外,你还可以在 队列任务(queued jobs)handle 方法中对依赖进行类型提示。在实际开发中,这也是你的大多数对象应通过容器解析的方式。

例如,你可以在控制器的构造函数中对应用定义的服务进行类型提示。该服务会被自动解析并注入到类中:

<?php

namespace App\Http\Controllers;

use App\Services\AppleMusic;

class PodcastController extends Controller
{
    /**
     * 创建一个新的控制器实例。
     */
    public function __construct(
        protected AppleMusic $apple,
    ) {}

    /**
     * 显示给定播客的信息。
     */
    public function show(string $id): Podcast
    {
        return $this->apple->findPodcast($id);
    }
}

方法调用与注入

有时,你可能希望调用对象实例上的某个方法,同时允许容器自动注入该方法的依赖。例如,假设有如下类:

<?php

namespace App;

use App\Services\AppleMusic;

class PodcastStats
{
    /**
     * 生成新的播客统计报告。
     */
    public function generate(AppleMusic $apple): array
    {
        return [
            // ...
        ];
    }
}

你可以像下面这样通过容器调用 generate 方法:

use App\PodcastStats;
use Illuminate\Support\Facades\App;

$stats = App::call([new PodcastStats, 'generate']);
无与伦比 翻译于 1天前

The call method accepts any PHP callable. The container's call method may even be used to invoke a closure while automatically injecting its dependencies:

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

$result = App::call(function (AppleMusic $apple) {
    // ...
});

Container Events

The service container fires an event each time it resolves an object. You may listen to this event using the resolving method:

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

As you can see, the object being resolved will be passed to the callback, allowing you to set any additional properties on the object before it is given to its consumer.

Rebinding

The rebinding method allows you to listen for when a service is re-bound to the container, meaning it is registered again or overridden after its initial binding. This can be useful when you need to update dependencies or modify behavior each time a specific binding is updated:

use App\Contracts\PodcastPublisher;
use App\Services\SpotifyPublisher;
use App\Services\TransistorPublisher;
use Illuminate\Contracts\Foundation\Application;

$this->app->bind(PodcastPublisher::class, SpotifyPublisher::class);

$this->app->rebinding(
    PodcastPublisher::class,
    function (Application $app, PodcastPublisher $newInstance) {
        //
    },
);

// New binding will trigger rebinding closure...
$this->app->bind(PodcastPublisher::class, TransistorPublisher::class);

PSR-11

Laravel's service container implements the PSR-11 interface. Therefore, you may type-hint the PSR-11 container interface to obtain an instance of the Laravel container:

use App\Services\Transistor;
use Psr\Container\ContainerInterface;

Route::get('/', function (ContainerInterface $container) {
    $service = $container->get(Transistor::class);

    // ...
});

An exception is thrown if the given identifier can't be resolved. The exception will be an instance of Psr\Container\NotFoundExceptionInterface if the identifier was never bound. If the identifier was bound but was unable to be resolved, an instance of Psr\Container\ContainerExceptionInterface will be thrown.

本文章首发在 LearnKu.com 网站上。

本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。

《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
《L04 微信小程序从零到发布》
从小程序个人账户申请开始,带你一步步进行开发一个微信小程序,直到提交微信控制台上线发布。
贡献者:1
讨论数量: 2
发起讨论 只看当前版本


Zealot
有人和我一样很难理解此章节吗?
4 个点赞 | 40 个回复 | 问答 | 课程版本 5.6