• Larvel 服务容器学习

    一个好的系统并不在代码多么复杂,框架多么多么大,在我的理解中框架跑得快不一定成功,不跌跟头才叫赢。

    我们需要弄懂这么几个概念:依赖注入,服务容器

    • 依赖注入: 这是一个花哨的名词,其实质上是通过【构造方法】或者【setter】来对本类中需要用到的以来进行注入。

      下面是官方的样例:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      <?php

      namespace App\Http\Controllers;

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

      class UserController extends Controller
      {
      /**
      * User Repository 的实现。
      *
      * @var UserRepository
      */
      protected $users;

      /**
      * 创建新的控制器实例。
      *
      * @param UserRepository $users
      * @return void
      */
      public function __construct(UserRepository $users)
      {
      $this->users = $users;
      }

      /**
      * 显示指定用户的详细信息。
      *
      * @param int $id
      * @return Response
      */
      public function show($id)
      {
      $user = $this->users->find($id);

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

      讲解:

      在这个例子中,控制器 UserController 需要从数据源中获取 users 。
      因此,我们要 注入 可以获取 users 的服务。
      在这种情况下, UserRepository 可能是通过使用 Eloquent 来从数据库中获取 user 信息。
      因为 UserRepository 是通过注入获取,所以我们可以容易地切换为其他实现。当测试应用程序时,我们还可以轻松地 「mock」 ,或创建假的 UserRepository 实例。
      
    • 服务容器: 管理类的以来和运行依赖注入的有效管理工具,

      Laravel服务容器主要承担两个作用:绑定与解析,服务容器的结构如下:

      服务容器

绑定

所谓的绑定就是将接口与实现建立对应关系。几乎所有的服务容器绑定都是在服务提供者中完成,也就是在服务提供者中绑定。

如果一个类没有基于任何接口那么就没有必要将其绑定到容器。容器并不需要被告知如何构建对象,因为它会使用 PHP 的反射服务自动解析出具体的对象。

也就是说,如果需要依赖注入的外部资源如果没有接口,那么就不需要绑定,直接利用服务容器进行解析就可以了,服务容器会根据类名利用反射对其进行自动构造。

  • bind绑定

    绑定有多种方法,首先最常用的是bind函数的绑定

    • 绑定自身

      1
      $this->app->bind('App\Services\RedisEventPusher', null);
    • 绑定闭包

      1
      2
      3
      4
      5
      6
      7
      $this->app->bind('HelpSpot\API', function ($app) {
      return new HelpSpot\API();
      });//闭包直接提供实现方式

      $this->app->bind('HelpSpot\API', function ($app) {
      return new HelpSpot\API($app->make('HttpClient'));
      });//需要依赖注入
    • 绑定接口

      1
      2
      3
      4
      $this->app->bind(
      'App\Contracts\EventPusher',
      'App\Services\RedisEventPusher'
      );
  • singleton绑定

    singleton 方法绑定一个只需要解析一次的类或接口到容器,然后接下来对容器的调用将会返回同一个实例:

    1
    2
    3
    $this->app->singleton('HelpSpot\API', function ($app) {
    return new HelpSpot\API($app->make('HttpClient'));
    });
  • instance绑定

    我们还可以使用 instance 方法绑定一个已存在的对象实例到容器,随后调用容器将总是返回给定的实例:

    1
    2
    $api = new HelpSpot\API(new HttpClient);
    $this->app->instance('HelpSpot\Api', $api);
  • Context绑定

    有时侯我们可能有两个类使用同一个接口,但我们希望在每个类中注入不同实现,例如,两个控制器依赖 Illuminate\Contracts\Filesystem\Filesystem 契约的不同实现。Laravel 为此定义了简单、平滑的接口:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    use Illuminate\Support\Facades\Storage;
    use App\Http\Controllers\VideoController;
    use App\Http\Controllers\PhotoControllers;
    use Illuminate\Contracts\Filesystem\Filesystem;

    $this->app->when(StorageController::class)
    ->needs(Filesystem::class)
    ->give(function () {
    Storage::class
    });//提供类名

    $this->app->when(PhotoController::class)
    ->needs(Filesystem::class)
    ->give(function () {
    return new Storage();
    });//提供实现方式

    $this->app->when(VideoController::class)
    ->needs(Filesystem::class)
    ->give(function () {
    return new Storage($app->make(Disk::class));
    });//需要依赖注入
  • 原始值绑定
    我们可能有一个接收注入类的类,同时需要注入一个原生的数值比如整型,可以结合上下文轻松注入这个类需要的任何值:

    1
    2
    3
    $this->app->when('App\Http\Controllers\UserController')
    ->needs('$variableName')
    ->give($value);
  • 数组绑定

    1
    2
    3
    app()['service'] = function(){
    return new Service();
    };
  • 标签绑定

    少数情况下,我们需要解析特定分类下的所有绑定,例如,你正在构建一个接收多个不同 Report 接口实现的报告聚合器,在注册完 Report 实现之后,可以通过 tag 方法给它们分配一个标签:

    1
    2
    3
    4
    5
    6
    7
    8
    9
      $this->app->bind('SpeedReport', function () {
    //
    });

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

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

    这些服务被打上标签后,可以通过 tagged 方法来轻松解析它们:

    1
    2
    3
    $this->app->bind('ReportAggregator', function ($app) {
    return new ReportAggregator($app->tagged('reports'));
    });
  • extend扩展

    extend是在当原来的类被注册或者实例化出来后,可以对其进行扩展:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    public function testExtendInstancesArePreserved()
    {
    $container = new Container;
    $container->bind('foo', function () {
    $obj = new StdClass;
    $obj->foo = 'bar';

    return $obj;
    });

    $obj = new StdClass;
    $obj->foo = 'foo';
    $container->instance('foo', $obj);

    $container->extend('foo', function ($obj, $container) {
    $obj->bar = 'baz';
    return $obj;
    });

    $container->extend('foo', function ($obj, $container) {
    $obj->baz = 'foo';
    return $obj;
    });

    $this->assertEquals('foo', $container->make('foo')->foo);
    $this->assertEquals('baz', $container->make('foo')->bar);
    $this->assertEquals('foo', $container->make('foo')->baz);
    }
  • Rebounds与Rebinding

    绑定是针对接口的,是为接口提供实现方式的方法。我们可以对接口在不同的时间段里提供不同的实现方法,一般来说,对同一个接口提供新的实现方法后,不会对已经实例化的对象产生任何影响。但是在一些场景下,在提供新的接口实现后,我们希望对已经实例化的对象重新做一些改变,这个就是 rebinding 函数的用途。
    下面就是一个例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    abstract class Car
    {
    public function __construct(Fuel $fuel)
    {
    $this->fuel = $fuel;
    }

    public function refuel($litres)
    {
    return $litres * $this->fuel->getPrice();
    }

    public function setFuel(Fuel $fuel)
    {
    $this->fuel = $fuel;
    }

    }

    class JeepWrangler extends Car
    {
    //
    }

    interface Fuel
    {
    public function getPrice();
    }

    class Petrol implements Fuel
    {
    public function getPrice()
    {
    return 130.7;
    }
    }

    我们在服务容器中是这样对car接口和fuel接口绑定的:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    $this->app->bind('fuel', function ($app) {
    return new Petrol;
    });

    $this->app->bind('car', function ($app) {
    return new JeepWrangler($app['fuel']);
    });

    $this->app->make('car');

    如果car被服务容器解析实例化成对象之后,有人修改了 fuel 接口的实现,从 Petrol 改为 PremiumPetrol:

    1
    2
    3
    $this->app->bind('fuel', function ($app) {
    return new PremiumPetrol;
    });

    由于 car 已经被实例化,那么这个接口实现的改变并不会影响到 car 的实现,假若我们想要 car 的成员变量 fuel 随着 fuel 接口的变化而变化,我们就需要一个回调函数,每当对 fuel 接口实现进行改变的时候,都要对 car 的 fuel 变量进行更新,这就是 rebinding 的用途:

1
2
3
4
5
$this->app->bindShared('car', function ($app) {
return new JeepWrangler($app->rebinding('fuel', function ($app, $fuel) {
$app['car']->setFuel($fuel);
}));
});

今天先到这里,未来会继续学习和总结。