Laravel 的服务容器(应用容器,依赖注入容器, 控制反转容器.. 怎么这么多名字),几乎是所有其他功能的核心。容器是一种简单的工具,可用于绑定和解析类和接口的具体实例,同时它也是相互依赖关系网络的管理者。

依赖注入简介

依赖注入意味着, 不是在一个类的内部 new 一个实例,而是持有一个,即从外部注入这个类的依赖关系。最常见的是在构造函数中注入,也就意味着对象的依赖关系在创建对象时就被注入。也可以在 setter时注入,该类提供了专门用于注入依赖关系的方法,还有方法 injection,其中有一个或多个方法希望在调用它们时注入其依赖项。

  1. class UserMail
  2. {
  3. protected $mailer;
  4. public function __contruct(Mailer $mailer)
  5. {
  6. $this->mailer = $mailer;
  7. }
  8. public function welcome($user)
  9. {
  10. return $this->mailer->mail($user->email, 'Welcome!');
  11. }
  12. }

依赖项注入的主要好处是,它给了我们更改注入的内容,模拟要测试的依赖项,以及只需实例化一次共享的依赖项以供之后共享使用的自由。

依赖注入和 laravel

简单依赖注入的例子:

$mailer = new MailgunMailer($mailgunKey, $mailgunSecret, $mailgunOption);
$usermail = new UserMail($mailer);

$userMail->welcome($user);

随着业务逻辑复杂

$mailer = new MailgunMailer($mailgunKey, $mailgunSecret, $mailgunOptions); // 邮件
$logger = new Logger($logPath, $minimumLogLevel); // 记录日志
$slack = new Slack($slackKey, $slackSecret, $channelName, $channelIcon); // slack 频道
$userMailer = new UserMailer($mailer, $logger, $slack);

$userMailer->welcome($user);

随着依赖的增多,依赖注入在实例化时也会变的很复杂。

app() 全局函数

从容器中拿一个对象。

// 传递参数 FQCN (fully qualified class name)有效的类名 或者 laravel 快捷方式
$logger = app(Logger::class);

在上边的例子中,$logger 有两个参数,$logPath$minimumLogLevel, 那么容器是怎么知道,应该传递什么参数呢?

其实,它不知道。容器可以创建一个,构造函数没有参数的类的实例,所以,那样你不如直接 new Logger 。当构造函数很复杂,我们需要确切的弄清楚容器怎么利用构造函数的参数构造类时,使用容器就会大放异彩。

容器是怎么连接的

在深入 Logger 类之前,看个例子

class Bar
{
    public function __construct() {}
}

class Baz
{
    public function __construct() {}
}

class Foo
{
    public function __contruct(Bar $bar, Baz $baz) {}
}

$foo = app(Foo::class);

对比上边邮件( mailer )的例子,不同之处在于,这里的依赖项( BarBaz)都很简单,简单到不需要任何额外的信息容器就可以解析它们。容器会读取Foo构造函数中的类型提示,解析一个BarBaz的实例,然后在创建 Foo 实例时将它们注入。这就叫做 autowiring:基于类型提示解析实例,而开发人员无需在容器中显式绑定这些类。

autowiring 意味着,一个类没有被显式的绑定到容器上(就像这里的 Foo , Bar , or Baz), 如果容器可以弄清怎么解析它,那么容器就可以解析它。这就意味着,没有任何外部依赖的的简单类(就像 BarBaz)或者所有的依赖都可以被容器解析的类(就像 Foo),是可以被容器解析的。

Yii2 中的DI 容器可以解析的 要么是一个没有外部依赖的简单类型,要么是一个容器自身可以自动解析其依赖关系的类型。 异曲同工。!

剩下的只有 绑定构造参数无法被解析的类了,如 $logger, 它有参数 日志路径和日志级别 两个参数。

绑定类到容器

把类绑定到容器,实质上就是告诉容器,“如果开发者需要一个 Logger 实例, 这就是需要运行的代码(这些代码带有正确的参数和依赖,并且返回一个实例)”。

我们告诉容器,当某人请求特定字符串时(通常是 FQCN),应该这样解析它。

绑定闭包

让我们看看怎么绑定到容器。 注意 绑定容器的合适位置是服务提供者的 register() 方法。

// 可以在任何服务提供者中 (maybe LoggerServiceProvider)
public function register(){
    $this->app->bind(Logger::class, function($app){
        return new Logger('\log\path\here', 'error');
    })
}

这有些重要的事情:

  1. 我们运行 $this->app->bind()$this->app 是一个容器实例,这个实例在每个服务提供者都可用。bind() 方法用于绑定到容器。
  2. bind() 的第一个参数是我们要绑定的 “key”。这里使用 FQCN 。第二个参数根据你设置的不同而不同,本质上是告诉容器怎么解析你绑定的 key
  3. 所以, 这里我们传入一个闭包。现在,如果某人运行 app(Logger::class),他们就会得到这个闭包。这个闭包传递了容器自身的实例($app)作为参数, 所以,如果你正在解析的类有一些依赖(需要容器解析),你可以直接使用它,如下:
$this->app->bind(UserMailer::class, function($app){
    return new UserMailer(
        $app->make(Mailer::class),
        $app->make(Logger::class),
        $app->make(Slack::class)
    );
})

注意,每次你请求一个新的实例,闭包都会运行,并返回新的实例。

绑定到单例,别名,实例

如果你想缓存闭包的输出,以免你每次请求实例时,都运行闭包, 这就是单例模式, 你可以运行 $this->app->singleton()

public function register(){
    $this->app->singleton(Logger::class, function(){
        return new Logger('\log\path\here', 'error');
    })
}

如果您已经有对象的实例,同时希望单例返回,也可以做类似的操作,如下

public function register(){
    $logger = new Logger('\log\path\here', 'error');
    $this->app->instance(Logger::class, $logger);
}

最后,如果您想将一个类别名为另一个类,将一个类绑定到一个快捷方式,或者将一个快捷方式绑定到一个类,则只需传递两个字符串, 如下:

// Asked for Logger, give FirstLogger
$this->app->bind(Logger::class, FirstLogger::class);

// Asked for log, give FirstLogger
$this->app->bind('log', FirstLogger::class);

// Asked for log, give FirstLogger
$this->app->alias(FirstLogger::class, 'log');

请注意,这些快捷方式在Laravel的核心中很常见;它使用易于记忆的键(如log)为提供核心功能的类提供了快捷方式系统。

将具体实例绑定到接口

就像绑定一个类到另一个类,或者绑定一个类到一个快捷方式一样,我们也可以绑定一个类到接口。它很强大,因为我们只需要将类型提示符从类名转换成接口名就行了。

use Interfaces\Mailer as MailerInterface;

class UserMailer
{
    protected $mailer;

    public function __contruct(MailerInterface $mailer)
    {
        $this->mailer = $mailer;
    }
}

// Service provider
public function register(){
    $this->app->bind(\Interfaces\Mailer::class, function(){
        return new MailgunMailer(....);
    })
}

你可以在你的整个代码中键入 MailerLogger , 然后你只需在一个服务提供者中选择使用那个特定的 mailer 和 logger 即可(实现接口的) 。 这就是控制反转。

使用该模式的优点之一就是,之后如果你不想使用 Mailgun 作为邮件提供程序了, 你只需一个实现 Mailer 接口的类,并在服务提供者中替换即可,其余的所有代码都可以正常运行。

语境绑定

有时内需要依据内容改变解析接口。你可能在一个地方需要本地的系统日志,另一个地方需要额外的服务。所以,让我们来教容器区分。如下:

// in a service provider
public function register()
{
    $this->app->when(FileWrangler::class)
        ->needs(Interfaces\Logger::class)
        ->give(Loggers\Syslog::class);

    $this->app->when(Job\SendWelcomeEmail::calss)
        ->needs(Interfaces\Logger::calss)
        ->give(Loggers\ParperTrail::class);
}

Laravel 框架文件中的构造注入

我们已经了解了构造注入的概念,并且我们也看了,容器的使用使从容器中解析一个类或接口的实例是变得容易。你也看到使用 app() 帮助函数创建实例和容器在创建类时解决类的构造函数依赖关系是多么容易。

我们还没了解到容器其实也负责解析很多应用程序的核心操作类。例如:每个控制器都是由容器实例化的。这意味着在控制器中你想要一个 logger 实例, 你只需在你的控制器构造函数中简单的指明 loggle类,当 laravel 创建控制器时,它将从容器中解析出来,并且它的实例在控制器中也可以使用。如下:

class MyController extends Controller
{
    protected $logger;

    public function __contruct(Logger $logger)
    {
        $this->logger = $logger;
    }

    public function index()
    {
        // Do something
        $this->logger->error('Something happened');
    }
}

容器负责解析 controller, middleware, queue jobs, event listeners 以及一些Laravel在应用程序的生命周期过程中自动生成的任何其他类。因此这些类中的任何一个在其构造函数中键入其依赖项,它们都会自动注入。

方法注入

在您的应用程序中,有一些地方Laravel不仅读取构造函数签名,还读取方法签名,并在那里也为您注入依赖项。

使用方法注入最常见的地方是在控制器方法中。如果你有一个依赖,你只想在单个控制器方法中使用,你可以在该方法中注入它。如下:

class Mycontroller extends Controller
{
    // Method dependencies can come after or before route parameters
    public function show(Logger $logger, $id)
    {
        // Do something
        $logger->error('xxx');
    }
}

使用 makeWith() 传递无法解析的构造函数参数

把类解析成具体实例的工具—— app() , $container->make()等, 都假设解析类的依赖关系是不需要传入任何参数的。但是,如果你的类的构造函数需要接受一个值,而不是一个容器可以解析的依赖项时?你需要使用 makeWith() 方法:

```php class Foo { public function __contruct($bar) { // … } }

$foo = $this->app->makeWith(Foo::class, [‘bar’=>’value’]);




> 这种情况很少,大多数你从容器中解析的类应该只有依赖项被注入到它们的构造函数中。


你可以在服务提供者的 `boot()` 方法中做同样的操作,你也可以使用容器调用任意类的任意方法,用于进行方法注入。

```php
// 例子: Manually calling a class method using the container’s call() method
class Foo 
{
    public function bar($parameter1){}
}

// Calls the 'bar' method on 'Foo' with a first parameter of 'value'
app()->call('Foo@bar', ['parameter1' => 'value']);

Facades and the Container

我们已经了解了部分 Facades,但我们还没探讨它是怎么工作的。

LaravelFacades 是可轻松访问 Laravel 功能核心部分的类。它有两个招牌特点:1.它提供全局的命名空间( Log\Illuminate\Support\Facades\Log 的别名); 2. 它使用静态方法访问非静态的资源。

让我们看一下 Log facade 。你可以在控制器或视图里这么用:
Log::error('some error message!')
你也可以这样用,效果是一样的:
$log = app('log');
$log->error('some error message!');

如你所见,facade 可以将类的静态调用转换成实例的常规方法。

facade 是怎么工作的

让我们以 Cache facade 为例,看看 facade 是这么工作的。

打开 Illuminate\Support\Facades\Cache.php 文件


<?php
namespace Illuminate\Support\Facades;

class Cache extends Facade
{
    protected static function getFacadeAccessor()
    {
        return 'cache';
    }
}

每个 facade 都有一个方法: getFacadeAccessor(). 它定义了 Laravel 从容器中寻找的 facade 背后实例时 所要用到的 key 。

在本例中,我们知道每次对 cache facade的调用都被代理到对容器中缓存快捷方式的一个实例的调用,当然,那(cache)并不是一个真正的类或者接口名,它也是快捷方式之一。

所以,这是实际发生的事情:

Cache::get('key');
// 等价于
app('cache')->get('key');

有一些方式可以查看,每个 facade 对应的真实的类, 看文档是最简单的。文档地址

有了参考资料,你可以做三件事。

  1. 首先,您可以找出 facade 上可用的方法。只需找到其支持类并查看该类的定义,它的任何公共方法都可以在 facade 上调用。
  2. 其次,您可以找出如何使用依赖项注入来注入 facade 的支持类。如果您想使用使用依赖项注入(而不是 facade)获取 facade 的功能,则只需键入 facade 的背后的类名或使用 app() 函数获取其实例,然后调用与 facade 上相同的方法即可。
  3. 最后,你可以创建自己的 facade。创建一个继承 Illuminate\Support\Facades\Facade 的类, 给它一个 getFacadeAccessor() 方法, 该方法返回一个字符串。使该字符串可用于从容器中解析您的支持类 - 可能只是该类的FQCN。 最后,您必须通过将其添加到 config/app.php 中的别名数组来注册 facade。 完成! 你刚刚建立了自己的facade

实时 facade

Laravel 5.4 介绍了一种新的概念,叫做 实时 facade (real-time facade)。 你不需要为了让你的类实例可以使用静态方法,而去创建一个类,你只需要在你的类的 FQCN 加上 Facades\前缀即可,如下:

namespace App;

class Charts
{
    public function burndown(){
        // ...
    }
}

// 使用
<h2> Burndown Chart </h2>
{{ Facades\App\Charts::burndown() }}

如你所见,我们通过在类的全名前加了 Facades\, 使非静态方法 burndown() 在 real-time facade 上可以作为静态方法使用。

服务提供者

我们在前一章中介绍了服务提供商的基础知识(请参阅“服务提供商”第255页)。 关于容器最重要的是你要记住在某个服务提供商的register()方法中注册你的绑定。

您可以将松散的绑定转储到 App\Providers\AppServiceProvider 中,这有点像 catchall,但通常最好为正在开发的每一组功能创建一个唯一的服务提供程序,并将其类绑定到其唯一的 register() 方法中。