31%
翻译进度
13
分块数量
1
参与人数

服务容器

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


服务容器#

简介#

Laravel 服务容器是一个用于管理类依赖以及实现依赖注入的强有力工具。依赖注入这个名词表面看起来花哨,实质上是指:通过构造函数,或者某些情况下通过「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)检索播客。 因此,我们 注入 一个可以检索博客的服务。 因为服务是被注入的,所以我们可以很容易地 “模拟”,或者在测试应用程序时创建一个 AppleMusic 服务的虚拟实现。

深入理解 Laravel 服务容器,对于构建一个强大的、大型的应用,以及对 Laravel 核心本身的贡献都是至关重要的。

寞小陌 翻译于 1 周前

零配置解决方案#

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

<?php

class Service
{
    // ...
}

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

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

值得庆幸的是,在构建 Laravel 应用程序时,你将要编写的许多类都可以通过容器自动接收它们的依赖关系,包括 控制器 , 事件监听者 , 中间件等等。此外,你可以在 队列 里的 handle 方法中键入提示依赖项。一旦你尝到了自动和零配置依赖注入的力量,你就会觉得没有它是不可以开发的。

何时使用容器#

得益于零配置解决方案,通常情况下,你只需要在路由、控制器、事件监听器和其他地方键入提示依赖项,而不必手动与容器打交道。例如,你可以在路由定义中键入 Illuminate\Http\Request 对象,以便轻松访问当前的请求对象。尽管我们不必与容器交互来编写此代码,但它在幕后管理着这些依赖项的注入:

use Illuminate\Http\Request;

Route::get('/', function (Request $request) {
    // ...
});
寞小陌 翻译于 1 周前

在许多情况下,多亏了自动依赖注入和 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));
});

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

寞小陌 翻译于 1 周前

如前所述,你通常会在服务提供者内部与容器进行交互;但是,如果你希望在服务提供者外部与容器进行交互,则可以通过 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));
});

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

绑定单例#

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));
});
寞小陌 翻译于 1 周前

You may use the singletonIf method to register a singleton container binding only if a binding has not already been registered for the given type:

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

Binding Scoped Singletons#

The scoped method binds a class or interface into the container that should only be resolved one time within a given Laravel request / job lifecycle. While this method is similar to the singleton method, instances registered using the scoped method will be flushed whenever the Laravel application starts a new "lifecycle", such as when a Laravel Octane worker processes a new request or when a Laravel queue worker processes a new job:

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

You may use the scopedIf method to register a scoped container binding only if a binding has not already been registered for the given type:

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

Binding Instances#

You may also bind an existing object instance into the container using the instance method. The given instance will always be returned on subsequent calls into the container:

use App\Services\Transistor;
use App\Services\PodcastParser;

$service = new Transistor(new PodcastParser);

$this->app->instance(Transistor::class, $service);

Binding Interfaces to Implementations#

A very powerful feature of the service container is its ability to bind an interface to a given implementation. For example, let's assume we have an EventPusher interface and a RedisEventPusher implementation. Once we have coded our RedisEventPusher implementation of this interface, we can register it with the service container like so:

use App\Contracts\EventPusher;
use App\Services\RedisEventPusher;

$this->app->bind(EventPusher::class, RedisEventPusher::class);

This statement tells the container that it should inject the RedisEventPusher when a class needs an implementation of EventPusher. Now we can type-hint the EventPusher interface in the constructor of a class that is resolved by the container. Remember, controllers, event listeners, middleware, and various other types of classes within Laravel applications are always resolved using the container:

use App\Contracts\EventPusher;

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

Contextual Binding#

Sometimes you may have two classes that utilize the same interface, but you wish to inject different implementations into each class. For example, two controllers may depend on different implementations of the Illuminate\Contracts\Filesystem\Filesystem contract. Laravel provides a simple, fluent interface for defining this behavior:

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

Contextual Attributes#

Since contextual binding is often used to inject implementations of drivers or configuration values, Laravel offers a variety of contextual binding attributes that allow to inject these types of values without manually defining the contextual bindings in your service providers.

For example, the Storage attribute may be used to inject a specific 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
    )
    {
        // ...
    }
}

In addition to the Storage attribute, Laravel offers Auth, Cache, Config, Context, DB, Give, Log, RouteParameter, and Tag attributes:

<?php

namespace App\Http\Controllers;

use App\Models\Photo;
use App\Contracts\Repository;
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,
        #[DB('mysql')] protected Connection $connection,
        #[Give(DatabaseRepository::class)] protected Repository $users,
        #[Log('daily')] protected LoggerInterface $log,
        #[RouteParameter('photo')] protected Photo $photo,
        #[Tag('reports')] protected iterable $reports,
    ) {
        // ...
    }
}

Furthermore, Laravel provides a CurrentUser attribute for injecting the currently authenticated user into a given route or class:

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

Route::get('/user', function (#[CurrentUser] User $user) {
    return $user;
})->middleware('auth');

Defining Custom Attributes#

You can create your own contextual attributes by implementing the Illuminate\Contracts\Container\ContextualAttribute contract. The container will call your attribute's resolve method, which should resolve the value that should be injected into the class utilizing the attribute. In the example below, we will re-implement Laravel's built-in Config attribute:

<?php

namespace App\Attributes;

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

#[Attribute(Attribute::TARGET_PARAMETER)]
class Config implements ContextualAttribute
{
    /**
     * Create a new attribute instance.
     */
    public function __construct(public string $key, public mixed $default = null)
    {
    }

    /**
     * Resolve the configuration value.
     *
     * @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);
    }
}

Binding Primitives#

Sometimes you may have a class that receives some injected classes, but also needs an injected primitive value such as an integer. You may easily use contextual binding to inject any value your class may need:

use App\Http\Controllers\UserController;

$this->app->when(UserController::class)
    ->needs('$variableName')
    ->give($value);

Sometimes a class may depend on an array of tagged instances. Using the giveTagged method, you may easily inject all of the container bindings with that tag:

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

If you need to inject a value from one of your application's configuration files, you may use the giveConfig method:

$this->app->when(ReportAggregator::class)
    ->needs('$timezone')
    ->giveConfig('app.timezone');

Binding Typed Variadics#

Occasionally, you may have a class that receives an array of typed objects using a variadic constructor argument:

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

Using contextual binding, you may resolve this dependency by providing the give method with a closure that returns an array of resolved Filter instances:

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

For convenience, you may also just provide an array of class names to be resolved by the container whenever Firewall needs Filter instances:

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

Variadic Tag Dependencies#

Sometimes a class may have a variadic dependency that is type-hinted as a given class (Report ...$reports). Using the needs and giveTagged methods, you may easily inject all of the container bindings with that tag for the given dependency:

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

Tagging#

Occasionally, you may need to resolve all of a certain "category" of binding. For example, perhaps you are building a report analyzer that receives an array of many different Report interface implementations. After registering the Report implementations, you can assign them a tag using the tag method:

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

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

$this->app->tag([CpuReport::class, MemoryReport::class], 'reports');

Once the services have been tagged, you may easily resolve them all via the container's tagged method:

$this->app->bind(ReportAnalyzer::class, function (Application $app) {
    return new ReportAnalyzer($app->tagged('reports'));
});

Extending Bindings#

The extend method allows the modification of resolved services. For example, when a service is resolved, you may run additional code to decorate or configure the service. The extend method accepts two arguments, the service class you're extending and a closure that should return the modified service. The closure receives the service being resolved and the container instance:

$this->app->extend(Service::class, function (Service $service, Application $app) {
    return new DecoratedService($service);
});

Resolving#

The make Method#

You may use the make method to resolve a class instance from the container. The make method accepts the name of the class or interface you wish to resolve:

use App\Services\Transistor;

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

If some of your class's dependencies are not resolvable via the container, you may inject them by passing them as an associative array into the makeWith method. For example, we may manually pass the $id constructor argument required by the Transistor service:

use App\Services\Transistor;

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

The bound method may be used to determine if a class or interface has been explicitly bound in the container:

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

If you are outside of a service provider in a location of your code that does not have access to the $app variable, you may use the App facade or the app helper to resolve a class instance from the container:

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

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

$transistor = app(Transistor::class);

If you would like to have the Laravel container instance itself injected into a class that is being resolved by the container, you may type-hint the Illuminate\Container\Container class on your class's constructor:

use Illuminate\Container\Container;

/**
 * Create a new class instance.
 */
public function __construct(
    protected Container $container,
) {}

Automatic Injection#

Alternatively, and importantly, you may type-hint the dependency in the constructor of a class that is resolved by the container, including controllers, event listeners, middleware, and more. Additionally, you may type-hint dependencies in the handle method of queued jobs. In practice, this is how most of your objects should be resolved by the container.

For example, you may type-hint a service defined by your application in a controller's constructor. The service will automatically be resolved and injected into the class:

<?php

namespace App\Http\Controllers;

use App\Services\AppleMusic;

class PodcastController extends Controller
{
    /**
     * Create a new controller instance.
     */
    public function __construct(
        protected AppleMusic $apple,
    ) {}

    /**
     * Show information about the given podcast.
     */
    public function show(string $id): Podcast
    {
        return $this->apple->findPodcast($id);
    }
}

Method Invocation and Injection#

Sometimes you may wish to invoke a method on an object instance while allowing the container to automatically inject that method's dependencies. For example, given the following class:

<?php

namespace App;

use App\Services\AppleMusic;

class PodcastStats
{
    /**
     * Generate a new podcast stats report.
     */
    public function generate(AppleMusic $apple): array
    {
        return [
            // ...
        ];
    }
}

You may invoke the generate method via the container like so:

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

$stats = App::call([new PodcastStats, 'generate']);

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 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。

《L03 构架 API 服务器》
你将学到如 RESTFul 设计风格、PostMan 的使用、OAuth 流程,JWT 概念及使用 和 API 开发相关的进阶知识。
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
贡献者:1
讨论数量: 2
发起讨论 只看当前版本


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