为什么我们需要 Laravel IoC 容器?

image

IOC 容器是一个实现依赖注入的便利机制 - Taylor Otwell

文章转自:https://learnku.com/laravel/t/26922

Laravel 是当今最流行、最常使用的开源现代 web 应用框架之一。它提供了一些独特的特性,比如 Eloquent ORM, Query 构造器,Homestead 等时髦的特性,这些特性只有 Laravel 中才有。

我喜欢 Laravel 是由于它犹如建筑风格一样的独特设计。Laravel 的底层使用了多设计模式,比如单例、工厂、建造者、门面、策略、提供者、代理等模式。随着本人知识的增长,我越来越发现 Laravel 的美。Laravel 为开发者减少了苦恼,带来了更多的便利。

学习 Laravel,不仅仅是学习如何使用不同的类,还要学习 Laravel 的哲学,学习它优雅的语法。Laravel 哲学的一个重要组成部分就是 IoC 容器,也可以称为服务容器。它是一个 Laravel 应用的核心部分,因此理解并使用 IoC 容器是我们必须掌握的一项重要技能。

IoC 容器是一个非常强大的类管理工具。它可以自动解析类。接下来我会试着去说清楚它为什么如此重要,以及它的工作原理。

首先,我想先谈下依赖反转原则,对它的了解会有助于我们更好地理解 IoC 容器的重要性。

该原则规定:

高层次的模块不应该依赖于低层次的模块,两者都应该依赖于抽象接口。

抽象接口不应该依赖于具体实现。而具体实现则应该依赖于抽象接口。

一言以蔽之: 依赖于抽象而非具体

class MySQLConnection
{

   /**
   * 数据库连接
   */
   public function connect()
   {
      var_dump(‘MYSQL Connection’);
   }

}


class PasswordReminder
{    
    /**
     * @var MySQLConnection
     */
     private $dbConnection;

    public function __construct(MySQLConnection $dbConnection) 
    {
      $this->dbConnection = $dbConnection;
    }
}

大家常常会有一个误解,那就是依赖反转就只是依赖注入的另一种说法。但其实二者是不同的。在上面的代码示例中,尽管在 PasswordReminder 类中注入了 MySQLConnection 类,但它还是依赖于 MySQLConnection 类。

然而,高层次模块 PasswordReminder 是不应该依赖于低层次模块 MySQLConnection 的。

如果我们想要把 MySQLConnection 改成 MongoDBConnection,那我们就还得手动修改 PasswordReminder 类构造函数里的依赖。

PasswordReminder 类应该依赖于抽象接口,而非具体类。那我们要怎么做呢?请看下面的例子:

interface ConnectionInterface
{
   public function connect();
}

class DbConnection implements ConnectionInterface
{
 /**
  * 数据库连接
  */
 public function connect()
 {
   var_dump(‘MYSQL Connection’);
 }
}

class PasswordReminder
{
    /**
    * @var DBConnection
    */
    private $dbConnection;
    public function __construct(ConnectionInterface $dbConnection)
    {
      $this->dbConnection = $dbConnection;
    }
}

通过上面的代码,如果我们想把 MySQLConnection 改成 MongoDBConnection,根本不需要去修改 PasswordReminder 类构造函数里的依赖。因为现在 PasswordReminder 类依赖的是接口,而非具体类。

如果你对接口的概念还不是很了解,可以看下 这篇文章 。它会帮助你理解依赖反转原则和 IoC 容器等。

现在我要讲下 IoC 容器里到底发生了什么。我们可以把 IoC 容器简单地理解为就是一个容器,里面装的是类的依赖。

OrderRepositoryInterface 接口:

namespace App\Repositories;

interface OrderRepositoryInterface 
{
   public function getAll();
}

DbOrderRepository 类:

namespace App\Repositories;

class DbOrderRepository implements OrderRepositoryInterface
{

  function getAll()
  {
    return 'Getting all from mysql';
  }
}

OrdersController 类:

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Http\Requests;
use App\Repositories\OrderRepositoryInterface;

class OrdersController extends Controller
{
    protected $order;

   function __construct(OrderRepositoryInterface $order)
   {
     $this->order = $order;
   }

   public function index()
   {
     dd($this->order->getAll());
     return View::make(orders.index);
   }
}

路由:

Route::resource('orders', 'OrdersController');

现在,在浏览器中输入这个地址 <http://localhost:8000/orders>

报错了吧,错误的原因是服务容器正在尝试去实例化一个接口,而接口是不能被实例化的。解决这个问题,只需把接口绑定到一个具体的类上:

image

把下面这行代码加在路由文件里就搞定了:

App::bind('App\Repositories\OrderRepositoryInterface', 'App\Repositories\DbOrderRepository');

现在刷新浏览器看看:

image

我们可以这样定义一个容器类:

class SimpleContainer
 {
    protected static $container = [];

    public static function bind($name, Callable $resolver)
    {   
        static::$container[$name] = $resolver;
    }

    public static function make($name)
    {
      if(isset(static::$container[$name])){
        $resolver = static::$container[$name] ;
        return $resolver();
    }

    throw new Exception("Binding does not exist in containeer");

   }
}

这里,我想告诉你服务容器解析依赖是多么简单的事。

class LogToDatabase 
{
    public function execute($message)
    {
       var_dump('log the message to a database :'.$message);
    }
}

class UsersController {

    protected $logger;

    public function __construct(LogToDatabase $logger)
    {
        $this->logger = $logger;
    }

    public function show()
    {
      $user = 'JohnDoe';
      $this->logger->execute($user);
    }
}

绑定依赖:

SimpleContainer::bind('Foo', function()
 {
   return new UsersController(new LogToDatabase);
 });

$foo = SimpleContainer::make('Foo');

print_r($foo->show());

输出:

string(36) "Log the messages to a file : JohnDoe"

Laravel 的服务容器源码:

  public function bind($abstract, $concrete = null, $shared = false)
    {
        $abstract = $this->normalize($abstract);

        $concrete = $this->normalize($concrete);

        if (is_array($abstract)) {
           list($abstract, $alias) = $this->extractAlias($abstract);

           $this->alias($abstract, $alias);
        }

        $this->dropStaleInstances($abstract);

        if (is_null($concrete)) {
            $concrete = $abstract;
        }


        if (! $concrete instanceof Closure) {
            $concrete = $this->getClosure($abstract, $concrete);
        }

        $this->bindings[$abstract] = compact('concrete', 'shared');


        if ($this->resolved($abstract)) {
            $this->rebound($abstract);
        }
    }

    public function make($abstract, array $parameters = [])
    {
        $abstract = $this->getAlias($this->normalize($abstract));

        if (isset($this->instances[$abstract])) {
            return $this->instances[$abstract];
        }

        $concrete = $this->getConcrete($abstract);

        if ($this->isBuildable($concrete, $abstract)) {
            $object = $this->build($concrete, $parameters);
        } else {
            $object = $this->make($concrete, $parameters);
        }


        foreach ($this->getExtenders($abstract) as $extender) {
            $object = $extender($object, $this);
        }

        if ($this->isShared($abstract)) {
            $this->instances[$abstract] = $object;
        }

       $this->fireResolvingCallbacks($abstract, $object);

       $this->resolved[$abstract] = true;

       return $object;
    }

    public function build($concrete, array $parameters = [])
    {

        if ($concrete instanceof Closure) {
            return $concrete($this, $parameters);
        }

       $reflector = new ReflectionClass($concrete);

        if (! $reflector->isInstantiable()) {
            if (! empty($this->buildStack)) {
                $previous = implode(', ', $this->buildStack);
                $message = "Target [$concrete] is not instantiable while building [$previous].";
            } else {
                $message = "Target [$concrete] is not instantiable.";
            }

          throw new BindingResolutionException($message);
        }

         $this->buildStack[] = $concrete;

         $constructor = $reflector->getConstructor();

        if (is_null($constructor)) {
            array_pop($this->buildStack);

           return new $concrete;
        }

        $dependencies = $constructor->getParameters();
        $parameters = $this->keyParametersByArgument(
            $dependencies, $parameters
        );

         $instances = $this->getDependencies($dependencies,$parameters);

         array_pop($this->buildStack);

         return $reflector->newInstanceArgs($instances);
    }

如果你想了解关于服务容器的更多内容,可以看下 vendor/laravel/framwork/src/Illuminate/Container/Container.php

简单的绑定

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

如果没有绑定,PHP 会利用反射机制来解析实例和依赖。

如果想了解更多细节,可以查看 官方文档

关于 Laravel 服务容器的练习代码, 可以从我的 *GitHub *(如果喜欢,烦请不吝 star )仓库获取。

感谢阅读。

文章转自:https://learnku.com/laravel/t/26922

更多文章:https://learnku.com/laravel/c/translations

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343

推荐阅读更多精彩内容

  • 导航 一、代码的演进二、butterKnife反射调用三、javapoet自动生成模板代码四、apt与注解五、注解...
    陈桐Caliburn阅读 1,605评论 0 0
  • 早上一来到公司,就看见大家都围着一位新来的小女生。看样子有事情发生啊。 果不其然,早上坐地铁的时候,手机被小偷给偷...
    慕星读者OR独者阅读 196评论 0 3
  • 文/张学彬 电话铃响的时候,我还在睡梦中。清华同学叫我一起吃早饭。我迷迷糊糊睁开眼,接起电话,“还早着呢,...
    茅居人家阅读 645评论 0 0