Laravel 容器

简介

Laravel服务容器是用于管理类依赖关系和执行依赖关系注入的强大工具。依赖注入从本质上讲是这样的:可以将一个类所依赖的其他类通过构造函数或在某些情况下通过“set”方法“注入”到这个类中。

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

<?php

namespace App\Http\Controllers;

use App\Http\Controllers\Controller;
use App\Repositories\UserRepository;
use App\User;

class UserController extends Controller
{
    /**
     * The user repository implementation.
     *
     * @var UserRepository
     */
    protected $users;

    /**
     * Create a new controller instance.
     *
     * @param  UserRepository  $users
     * @return void
     */
    public function __construct(UserRepository $users)
    {
        $this->users = $users;
    }

    /**
     * Show the profile for the given user.
     *
     * @param  int  $id
     * @return Response
     */
    public function show($id)
    {
        $user = $this->users->find($id);

        return view('user.profile', ['user' => $user]);
    }
}

在此示例中,UserController需要从数据源检索用户。因此,我们将注入能够检索用户的服务。在这种情况下,我们的UserRepository最有可能使用Eloquent从数据库中检索用户信息。但是,由于注入了 repository,因此我们可以轻松使用另一个实现的实例。在测试我们的应用程序时,我们还可以轻松地“模拟”或创建UserRepository的实现。

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


绑定

绑定的基础

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

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

一些简单的绑定

在service provider中,可以一直使用$this->app属性访问容器。我们可以使用bind方法注册绑定,第一个参数是要注册的类或接口名称,第二个参数是一个Closure,用来返回类实例:

$this->app->bind('HelpSpot\API', function ($app) {
    return new \HelpSpot\API($app->make('HttpClient'));
});

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

绑定单例

singleton 方法将类或接口绑定到只能被解析一次的容器中。解析单例绑定后,在随后的容器调用中将返回相同的对象实例:

$this->app->singleton('HelpSpot\API', function ($app) {
    return new \HelpSpot\API($app->make('HttpClient'));
});

绑定实例

我们也可以使用 instance 方法将现有对象实例绑定到容器中

$api = new \HelpSpot\API(new HttpClient);

$this->app->instance('HelpSpot\API', $api);

将接口绑定到实现

服务容器的一个非常强大的功能是接口和该接口的实现绑定起来的能力。例如,假设我们有一个EventPusher接口和一个RedisEventPusher实现。完成RedisEventPusher后,我们可以将其注册到服务容器中,如下所示:

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

上面的代码告诉容器,当需要一个EventPusher接口的实现时,应注入RedisEventPusher。现在,我们可以在构造函数中或服务容器注入依赖项的任何其他位置键入EventPusher接口的提示:

use App\Contracts\EventPusher;

/**
 * Create a new class instance.
 *
 * @param  EventPusher  $pusher
 * @return void
 */
public function __construct(EventPusher $pusher)
{
    $this->pusher = $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');
          });

绑定原语

有时我们可能有一个类,该类可以接收某些注入的类,但是还需要一个注入的原始值,例如整数。我们可以使用上下文绑定来注入我们的类可能需要的任何值:

$this->app->when('App\Http\Controllers\UserController')
          ->needs('$variableName')
          ->give($value);

有时,一个类可能取决于一组带标签的实例。使用该giveTagged方法,我们可以轻松地使用该标签注入所有容器绑定:

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

绑定类型的可变参数

有时,我们可能有一个这样的类,该类的构造函数的参数不是固定的,如下所示

class Firewall
{
    protected $logger;
    protected $filters;

    public function __construct(Logger $logger, Filter ...$filters)
    {
        $this->logger = $logger;
        $this->filters = $filters;
    }
}

使用上下文绑定,我们可以通过向give方法提供一个Closure来解析此依赖关系,该Closure返回已解析Filter实例的数组:

$this->app->when(Firewall::class)
          ->needs(Filter::class)
          ->give(function ($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)。使用needs和giveTagged方法,您可以轻松地使用给定依赖项的该标记注入所有容器绑定:

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

标记

有时,我们可能需要解析所有特定的绑定“类别”。例如,正在构建一个report聚合器,该聚合器接收许多Report接口不同的实现的数组。将这些Report接口的实现之后,可以使用tag方法为它们分配标签:

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

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

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

标记了服务后,我们可以通过tagged方法轻松解决所有问题:

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

扩展绑定

extend方法允许对已解析的服务进行修改。例如,服务解析后我们可以运行其他代码来装饰或配置服务。extend方法是一个服务的类名,第二个参数是一个闭包,闭包应返回修改后的服务。Closure的参数是正在解析的服务和容器实例:

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

调用

make方法

我们可以使用make方法从容器中解析类的实例。make方法参数是要解析的类或接口的名称:

$api = $this->app->make('HelpSpot\API');

如果我们的代码所在位置无法访问$app变量,则可以使用全局resolve函数:

$api = resolve('HelpSpot\API');

如果某些类的依赖项无法通过容器解析,则可以通过将它们作为关联数组传递到makeWith方法中注入它们:

$api = $this->app->makeWith('HelpSpot\API', ['id' => 1]);

自动注入

另外,重要的是,我们可以在控制器、event listener、中间件等通过类型限定的方式注入类的实例。实际上,这就是容器应解决的大多数对象的方式。

例如,我们可以在控制器的构造函数中键入由应用程序定义的Repository的提示。Repository将自动解析并注入到类中:

<?php

namespace App\Http\Controllers;

use App\Users\Repository as UserRepository;

class UserController extends Controller
{
    /**
     * The user repository instance.
     */
    protected $users;

    /**
     * Create a new controller instance.
     *
     * @param  UserRepository  $users
     * @return void
     */
    public function __construct(UserRepository $users)
    {
        $this->users = $users;
    }

    /**
     * Show the user with the given ID.
     *
     * @param  int  $id
     * @return Response
     */
    public function show($id)
    {
        //
    }
}

容器事件

服务容器在每次解析对象时都会触发一个事件。可以使用resolving方法监听此事件:

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

$this->app->resolving(\HelpSpot\API::class, function ($api, $app) {
    // Called when container resolves objects of type "HelpSpot\API"...
});

如您所见,被解析的对象将传递给回调函数,使我们可以在将对象提供给使用者之前设置该对象的所有其他属性。


PSR-11

Laravel的服务容器实现了PSR-11接口。因此,我们可以在方法中输入PSR-11容器接口的类型限定以获取相关的容器实例:

use Psr\Container\ContainerInterface;

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

    //
});

如果无法解析给定的标识符,则会引发异常。该异常将是Psr\Container\NotFoundExceptionInterface接口的实例。如果标识符已绑定但无法解析,则会抛出一个Psr\Container\ContainerExceptionInterface接口的实例。

查看笔记

扫码一下
查看教程更方便