Laravel IOC容器深度剖析

1、基础知识预备

1.1 PHP 反射详解

什么是反射?直观理解就是根据到达地找到出发地和来源。比如,一个光秃秃的对象,我们可以仅仅通过这个对象就能知道它所属的类,拥有哪些方法。

PHP的反射是指在PHP运行状态过程中,扩展分析PHP程序,导出或提出关于类、方法、属性、参数等详细信息,包括注释。这种动态获取信息以及动态调用对象方法的功能成为反射API

下面是一个PHP反射的一个简单demo。
详细可参考PHP docuement http://php.net/manual/zh/book.reflection.php

<?php
class Person{
    public $name;
    public $gender;
    private $age;

    public function __construct($name,$gender){
        $this->name = $name;
        $this->gender = $gender;
    }

    public function __set($key, $value){
        $this->$key = $value;
    }

    public function __get($key){
        if(!isset($this->$key)){
            return "$key not exists";
        }
        return $this->$key;
    }
}
//用ReflectionClass得到A的反射类对象,通过反射类对象可以得到类的各种属性,
//包括类名空间,父类,类名等,使用newInstanceArgs可以传入构造函数的参数创建一个新的类的实例
$reflect = new ReflectionClass("Person");
//注入参数
$student = $reflect->newInstanceArgs(["xiaoming", "male"]);
echo $student->name;
echo "\n";
echo $student->gender;
echo "\n";
echo $student->age;
echo "\n";
$student->age = 20;
echo $student->age;
echo "\n";

output:

xiaoming
male
age not exists
20

1.2 依赖

程序的执行过程就是方法的调用过程,有方法调用,就必然会促使对象和对象之间产生依赖,除非一个对象不参与程序的运行,这样的对象就像一座孤岛,与其他对象没有任何交互,但是这样的对象也就没有任何存在的价值了。因此,在我们的程序代码中,任何一个对象必然会与其他一个甚至更多个对象产生依赖关系。

依赖产生的原因主要有:

  • 方法调用
  • 继承(子类依赖于父类)
  • 传递参数(一个类作为参数传递给另外一个类的成员方法时)
  • 临时变量引入

下面有两段代码。
第一段是直接在代码中引入了Train类,这时候就形成了依赖。但是假如旅客不想坐火车,想坐飞机,只能修改StupidTraveller中的代码。
第二段代码使用依赖注入的方式解除了上面StupidTraveller对Train的依赖。代码的可扩展性变强了很多,如果还有其他的交通工具比如Car,只需要将Car注入Traveller类中即可。此处的依赖注入是手动的,Laravel的IOC容器是自动注入的。

<?php
class Train{
    public function go(){
        echo "move by train \n";
    }
}

class Airplane{
    public function go(){
        echo "move by airplane \n";
    }
}

class StupidTraveller{
    private $trafficTool;
    public function __construct(){
        $this->trafficTool = new Train();
    }

    public function travel(){
        $this->trafficTool->go();
    }
}
$mike = new StupidTraveller();
$mike->travel();

<?php
interface TrafficTool{
    public function go();
}

class Train implements TrafficTool{
    public function go(){
        echo "move by train \n";
    }
}

class Airplane implements TrafficTool{
    public function go(){
        echo "move by airplane \n";
    }
}

class Traveller{
    private $trafficTool;
    public function __construct(TrafficTool $tool){
        $this->trafficTool = $tool;
    }

    public function travel(){
        $this->trafficTool->go();
    }
}
//在这里进行了依赖注入
$mike = new Traveller(new Train());
$mike->travel();

2、IOC容器

2.1 IOC容器是什么?

在理解IOC之前,首先从1.2的例子中理解下什么是DI(Dependency Injection)。Traveller类在初始化的时候注入了TrafficTool类,这就是一个典型的依赖注入的例子。依赖注入有两个好处,一个是解耦,将依赖之间解耦,显然Traveller类要比StupidTraveller要优雅很多;第二个是由于已经解耦了,方便做单元测试,尤其是Mock测试。如果TrafficTool是一个数据库的操作类(打个比方啊),这个时候你做单元测试的时候可以直接Mock一个虚拟的数据库类注入到Traveller中去来做单元测试。

IOC(Inversion of Control) 控制反转是一种面对对象编程的设计原则,用于降低代码之间的耦合度。其基本思想是借助第三方实现具有依赖关系的对象之间的解耦。


混乱的依赖关系
IOC容器

软件系统在没有IOC之前,对象之间相互依赖,那么对象A在初始化或者运行到某一个点的时候,自己必须主动去创建一个对象B,或者使用之间已经创建好的对象B。无论是创建还是使用对象B,控制权都在自己手上。

但是在有了IOC容器后,这种情况改变了,由于IOC的加入,对象之间的直接联系消失了。在对象A运行时需要对象B的时候,IOC会主动创建一个对象B注入到对象A需要的地方。这也是Laravel中实现的IOC容器的神奇之处,自动注入依赖。

2.2 一段比较经典的IOC容器的代码

下面这段代码源于《laravel框架关键技术解析》

      <?php
// 容器类,将接口与实现绑定
class Container
{
    // 保存与接口绑定的闭包,
    // 闭包必须能够返回接口的实例。
    protected $bindings = [];

    // 为某个接口绑定一个实现,有两种情况:
    //
    // 第一种是绑定接口的实现的类名;
    // 第二种是绑定一个闭包,这个闭包应该返回接口的实例。
    // 不管哪种情况,实例化操作都是调用 build() 方法完成。
    // 第二种情况,主要是为了能够在实例化前后进行额外的操作,
    // 实例化的逻辑就书写在闭包中。
    public function bind($abstract, $concrete = null)
    {
        // 如果提供的参数不是闭包,而是一个类,
        // 则构建一个闭包,直接调用 build() 方法进行实例化
        if (! $concrete instanceof Closure) {
            // 调用闭包时,传入的参数是容器本身,即 $this
            $concrete = function ($c) use ($concrete) {
                return $c->build($concrete);
            };
        }
        $this->bindings[$abstract] = $concrete;
    }

    // 生成指定接口的实例
    public function make($abstract)
    {
        // 取出闭包
        $concrete = $this->bindings[$abstract];
        // 运行闭包,即可取得一个实例
        return $concrete($this);
    }

    public function build($class)
    {
        // 取得类的反射
        $reflector = new ReflectionClass($class);

        // 检查类是否可实例化
        if (! $reflector->isInstantiable()) {
            // 如果不能,意味着接口不能正常工作,报错
            echo $message = "Target [$class] is not instantiable";
        }

        // 取得构造函数的反射
        $constructor = $reflector->getConstructor();

        // 检查是否有构造函数
        if (is_null($constructor)) {
            // 如果没有,就说明没有依赖,直接实例化
            return new $class;
        }

        // 取得包含每个参数的反射的数组
        $parameters = $constructor->getParameters();
        // 返回一个真正的参数列表,那些被类型提示的参数已经被注入相应的实例
        $realParameters = $this->resolveDependencies($parameters);

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

    protected function resolveDependencies($parameters)
    {
        $realParameters = [];
        foreach($parameters as $parameter) {
            // 如果一个参数被类型提示为类 foo,
            // 则这个方法将返回类 foo 的反射
            $dependency = $parameter->getClass();
            if(is_null($dependency)) {
                $realParameters[] = NULL;
            } else {
                $realParameters[] = $this->make($dependency->name);
            }
        }

        return (array)$realParameters;
    }
}

下面在用上面的IOC容器来实现先前的Traveller

<?php
require __DIR__ . '/Container.php';

interface  TrafficTool
{
    public function go();
}

class Train implements TrafficTool
{
    public function go()
    {
        // TODO: Implement go() method.
        echo "move by train";
    }
}

class Airplane implements TrafficTool
{
    public function go()
    {
        // TODO: Implement go() method.
        echo "move by airplane\n";
    }

}

class Destination
{

    public function mark()
    {
        echo "I am here";
    }

}

class Traveller
{
    protected $trafficTool;
    protected $destination;

    public function __construct(TrafficTool $tool, Destination $destination)
    {
        $this->trafficTool = $tool;
        $this->destination = $destination;
    }

    public function travel()
    {
        $this->trafficTool->go();
        $this->destination->mark();
    }
}


// 实例化容器
$app = new Container();
//绑定某一功能到ioc
//将Train绑定到TrafficTool,abstract 是TrafficTool, concrete是Train
$app->bind('TrafficTool', 'AirPlane');
$app->bind("Destination", "Destination");
$app->bind('travellerA', 'Traveller');
//实例化对象
$tra = $app->make('travellerA');
$tra->travel();

注意$tra = $app->make('travellerA');这里并没有手动的注入依赖TrafficTool。这个依赖在container的resolveDependencies方法中获取,最后在$reflector->newInstanceArgs($realParameters)这行代码中间进行了注入。并且这一系列的注入都不是手动的,注入都是自动完成的。

2.3、laravel中的IOC容器

在laravel初始化的地方index.php中有这样一行代码

/*
|--------------------------------------------------------------------------
| Turn On The Lights
|--------------------------------------------------------------------------
|
| We need to illuminate PHP development, so let us turn on the lights.
| This bootstraps the framework and gets it ready for use, then it
| will load up this application so that we can run it and send
| the responses back to the browser and delight our users.
|
*/

$app = require_once __DIR__.'/../bootstrap/app.php';

开灯了!!!这里的app就是IOC容器,里面已经进行了一系列的绑定。具体的绑定如下。

<?php

$app = new Illuminate\Foundation\Application(
    realpath(__DIR__.'/../')
);


$app->singleton(
    Illuminate\Contracts\Http\Kernel::class,
    App\Http\Kernel::class
);

$app->singleton(
    Illuminate\Contracts\Console\Kernel::class,
    App\Console\Kernel::class
);

$app->singleton(
    Illuminate\Contracts\Debug\ExceptionHandler::class,
    App\Exceptions\Handler::class
);
return $app;

容器就是在vendor/laravel/framework/src/illuminate/Container/Container.php中实现的,具体的实现代码要比上面的那个简单版复杂很多,但是思想是一样的。

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

推荐阅读更多精彩内容

  • 土曾经是地球上最廉价的资源。我们脚下曾经到处都是土。记得小时候我家的房子所有房间地都是土筑的。春天天干时,地上就起...
    湖边人老刘阅读 659评论 0 3
  • 每年的毕业季,也是人潮涌动的时候,每个人似乎都开始了忙碌的步伐,有的人匆匆迈上了去北上广的路,有的悄悄回到了家乡所...
    况北痕阅读 263评论 0 2
  • 在我的记忆里, 井上村曾经有三盘水碓。 咯嘣咯嘣的碾米声, 曾经是1300多号人的井上 特有的旋律, 令人陶醉。 ...
    李逢亭阅读 585评论 0 1
  • 汪国真 总有些这样的时候 正是为了爱 才悄悄躲开 躲开的是身影 躲不开的却是那份 默默的情怀 月光下踯躅 睡梦里徘...
    小王子的狐狸先森阅读 215评论 0 1