资讯专栏INFORMATION COLUMN

Laravel核心——服务容器的细节特性

AprilJ / 3339人阅读

摘要:前言首先欢迎关注我的博客在前面几个博客中,我详细讲了容器各个功能的使用绑定的源码解析的源码,今天这篇博客会详细介绍容器的一些细节,一些特性,以便更好地掌握容器的功能。

前言

首先欢迎关注我的博客: www.leoyang90.cn

在前面几个博客中,我详细讲了 Ioc 容器各个功能的使用、绑定的源码、解析的源码,今天这篇博客会详细介绍 Ioc 容器的一些细节,一些特性,以便更好地掌握容器的功能。

注:本文使用的测试类与测试对象都取自 laravel 的单元测试文件src/illuminate/tests/Container/ContainerTest.php

rebind绑定特性 rebind 在绑定之前

instance 和 普通 bind 绑定一样,当重新绑定的时候都会调用 rebind 回调函数,但是有趣的是,对于普通 bind 绑定来说,rebind 回调函数被调用的条件是当前接口被解析过:

public function testReboundListeners()
{
    unset($_SERVER["__test.rebind"]);

    $container = new Container;
    $container->rebinding("foo", function () {
        $_SERVER["__test.rebind"] = true;
    });
    $container->bind("foo", function () {
    });
    $container->make("foo");
    $container->bind("foo", function () {
    });

    $this->assertTrue($_SERVER["__test.rebind"]);
}

所以遇到下面这样的情况,rebinding 的回调函数是不会调用的:

public function testReboundListeners()
{
    unset($_SERVER["__test.rebind"]);

    $container = new Container;
    $container->rebinding("foo", function () {
        $_SERVER["__test.rebind"] = true;
    });
    $container->bind("foo", function () {
    });
    $container->bind("foo", function () {
    });

    $this->assertFalse(isset($_SERVER["__test.rebind"]));
}

有趣的是对于 instance 绑定:

public function testReboundListeners()
{
    unset($_SERVER["__test.rebind"]);

    $container = new Container;
    $container->rebinding("foo", function () {
        $_SERVER["__test.rebind"] = true;
    });
    $container->bind("foo", function () {
    });
    $container->instance("foo", function () {
    });

    $this->assertTrue(isset($_SERVER["__test.rebind"]));
}

rebinding 回调函数却是可以被调用的。其实原因就是 instance 源码中 rebinding 回调函数调用的条件是 rebound 为真,而普通 bind 函数调用 rebinding 回调函数的条件是 resolved 为真. 目前笔者不是很清楚为什么要对 instance 和 bind 区别对待,希望有大牛指导。

rebind 在绑定之后

为了使得 rebind 回调函数在下一次的绑定中被激活,在 rebind 函数的源码中,如果判断当前对象已经绑定过,那么将会立即解析:

public function rebinding($abstract, Closure $callback)
{
    $this->reboundCallbacks[$abstract = $this->getAlias($abstract)][] = $callback;
    
    if ($this->bound($abstract)) {
        return $this->make($abstract);
    }
}

单元测试代码:

public function testReboundListeners1()
{
    unset($_SERVER["__test.rebind"]);

    $container = new Container;
    $container->bind("foo", function () {
        return "foo";
    });

    $container->resolving("foo", function () {
        $_SERVER["__test.rebind"] = true;
    });

    $container->rebinding("foo", function ($container,$object) {//会立即解析
        $container["foobar"] = $object."bar";
    });

    $this->assertTrue($_SERVER["__test.rebind"]);

    $container->bind("foo", function () {
    });

    $this->assertEquals("bar", $container["foobar"]);
}
resolving 特性 resolving 回调的类型

resolving 不仅可以针对接口执行回调函数,还可以针对接口实现的类型进行回调函数。

public function testResolvingCallbacksAreCalledForType()
{
    $container = new Container;
    $container->resolving("StdClass", function ($object) {
        return $object->name = "taylor";
    });
    $container->bind("foo", function () {
          return new StdClass;
    });
    $instance = $container->make("foo");

    $this->assertEquals("taylor", $instance->name);
}
public function testResolvingCallbacksShouldBeFiredWhenCalledWithAliases()
{
    $container = new Container;
    $container->alias("StdClass", "std");
    $container->resolving("std", function ($object) {
        return $object->name = "taylor";
    });
    $container->bind("foo", function () {
        return new StdClass;
    });
    $instance = $container->make("foo");

    $this->assertEquals("taylor", $instance->name);
}
resolving 回调与 instance

前面讲过,对于 singleton 绑定来说,resolving 回调函数仅仅运行一次,只在 singleton 第一次解析的时候才会调用。如果我们利用 instance 直接绑定类的对象,不需要解析,那么 resolving 回调函数将不会被调用:

public function testResolvingCallbacksAreCalledForSpecificAbstracts()
{
    $container = new Container;
    $container->resolving("foo", function ($object) {
        return $object->name = "taylor";
    });
    $obj = new StdClass;
    $container->instance("foo", $obj);
    $instance = $container->make("foo");

    $this->assertFalse(isset($instance->name));
}
extend 扩展特性

extend 用于扩展绑定对象的功能,对于普通绑定来说,这个函数的位置很灵活:

在绑定前扩展
public function testExtendIsLazyInitialized()
{
    ContainerLazyExtendStub::$initialized = false;
    
    $container = new Container;      
    $container->extend("IlluminateTestsContainerContainerLazyExtendStub", function ($obj, $container) {
        $obj->init();
        return $obj;   
    });    
    $container->bind("IlluminateTestsContainerContainerLazyExtendStub"); 

    $this->assertFalse(ContainerLazyExtendStub::$initialized);   
    $container->make("IlluminateTestsContainerContainerLazyExtendStub");   
    $this->assertTrue(ContainerLazyExtendStub::$initialized);
}
在绑定后解析前扩展
public function testExtendIsLazyInitialized()
{
    ContainerLazyExtendStub::$initialized = false;
    
    $container = new Container;   
    $container->bind("IlluminateTestsContainerContainerLazyExtendStub");    
    $container->extend("IlluminateTestsContainerContainerLazyExtendStub", function ($obj, $container) {
        $obj->init();
        return $obj;   
    });    

    $this->assertFalse(ContainerLazyExtendStub::$initialized);   
    $container->make("IlluminateTestsContainerContainerLazyExtendStub");   
    $this->assertTrue(ContainerLazyExtendStub::$initialized);
}
在解析后扩展
public function testExtendIsLazyInitialized()
{
    ContainerLazyExtendStub::$initialized = false;
    
    $container = new Container;   
    $container->bind("IlluminateTestsContainerContainerLazyExtendStub");         
    
    $container->make("IlluminateTestsContainerContainerLazyExtendStub"); 
    $this->assertFalse(ContainerLazyExtendStub::$initialized);
    
    $container->extend("IlluminateTestsContainerContainerLazyExtendStub", function ($obj, $container) {
        $obj->init();
        return $obj;   
    });
    $this->assertFalse(ContainerLazyExtendStub::$initialized);  
      
    $container->make("IlluminateTestsContainerContainerLazyExtendStub"); 
    $this->assertTrue(ContainerLazyExtendStub::$initialized);
}

可以看出,无论在哪个位置,extend 扩展都有 lazy 初始化的特点,也就是使用 extend 函数并不会立即起作用,而是要等到 make 解析才会激活。

extend 与 instance 绑定

对于 instance 绑定来说,暂时 extend 的位置需要位于 instance 之后才会起作用,并且会立即起作用,没有 lazy 的特点:

public function testExtendInstancesArePreserved()
{
    $container = new Container;

    $obj = new StdClass;
    $obj->foo = "foo";
    $container->instance("foo", $obj);
    $container->extend("foo", function ($obj, $container) {
        $obj->bar = "baz";

        return $obj;
    });

    $this->assertEquals("foo", $container->make("foo")->foo);
    $this->assertEquals("baz", $container->make("foo")->bar);
}
extend 绑定与 rebind 回调

无论扩展对象是 instance 绑定还是 bind 绑定,extend 都会启动 rebind 回调函数:

public function testExtendReBindingInstance()
{
    $_SERVER["_test_rebind"] = false;

    $container = new Container;
    $container->rebinding("foo",function (){
        $_SERVER["_test_rebind"] = true;
    });

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

    $container->make("foo");

    $container->extend("foo", function ($obj, $container) {
        return $obj;
    });

    this->assertTrue($_SERVER["_test_rebind"]);
}

public function testExtendReBinding()
{
    $_SERVER["_test_rebind"] = false;

    $container = new Container;
    $container->rebinding("foo",function (){
        $_SERVER["_test_rebind"] = true;
    });
    $container->bind("foo",function (){
        $obj = new StdClass;

        return $obj;
    });

    $container->make("foo");

    $container->extend("foo", function ($obj, $container) {
        return $obj;
    });

    this->assertFalse($_SERVER["_test_rebind"]);
}
contextual 绑定特性 contextual 在绑定前

contextual 绑定不仅可以与 bind 绑定合作,相互不干扰,还可以与 instance 绑定相互合作。而且 instance 的位置也很灵活,可以在 contextual 绑定前,也可以在contextual 绑定后:

public function testContextualBindingWorksForExistingInstancedBindings()
{
    $container = new Container;

    $container->instance("IlluminateTestsContainerIContainerContractStub", new ContainerImplementationStub);

    $container->when("IlluminateTestsContainerContainerTestContextInjectOne")->needs("IlluminateTestsContainerIContainerContractStub")->give("IlluminateTestsContainerContainerImplementationStubTwo");

    $this->assertInstanceOf(
             "IlluminateTestsContainerContainerImplementationStubTwo",
             $container->make("IlluminateTestsContainerContainerTestContextInjectOne")->impl
     );
}
contextual 在绑定后
public function testContextualBindingWorksForNewlyInstancedBindings()
{
    $container = new Container;

    $container->when("IlluminateTestsContainerContainerTestContextInjectOne")->needs("IlluminateTestsContainerIContainerContractStub")->give("IlluminateTestsContainerContainerImplementationStubTwo");

    $container->instance("IlluminateTestsContainerIContainerContractStub", new ContainerImplementationStub);

    $this->assertInstanceOf(
            "IlluminateTestsContainerContainerImplementationStubTwo",
        $container->make("IlluminateTestsContainerContainerTestContextInjectOne")->impl
    );
}
contextual 绑定与别名

contextual 绑定也可以在别名上进行,无论赋予别名的位置是 contextual 的前面还是后面:

public function testContextualBindingDoesntOverrideNonContextualResolution()
{
    $container = new Container;

    $container->instance("stub", new ContainerImplementationStub);
    $container->alias("stub", "IlluminateTestsContainerIContainerContractStub");

    $container->when("IlluminateTestsContainerContainerTestContextInjectTwo")->needs("IlluminateTestsContainerIContainerContractStub")->give("IlluminateTestsContainerContainerImplementationStubTwo");

    $this->assertInstanceOf(
            "IlluminateTestsContainerContainerImplementationStubTwo",
            $container->make("IlluminateTestsContainerContainerTestContextInjectTwo")->impl
        );

    $this->assertInstanceOf(
            "IlluminateTestsContainerContainerImplementationStub",
            $container->make("IlluminateTestsContainerContainerTestContextInjectOne")->impl
    );
}

public function testContextualBindingWorksOnNewAliasedBindings()
{
    $container = new Container;

    $container->when("IlluminateTestsContainerContainerTestContextInjectOne")->needs("IlluminateTestsContainerIContainerContractStub")->give("IlluminateTestsContainerContainerImplementationStubTwo");

    $container->bind("stub", ContainerImplementationStub::class);
    $container->alias("stub", "IlluminateTestsContainerIContainerContractStub");

    $this->assertInstanceOf(
          "IlluminateTestsContainerContainerImplementationStubTwo",
          $container->make("IlluminateTestsContainerContainerTestContextInjectOne")->impl
    );
}
争议

目前比较有争议的是下面的情况:

public function testContextualBindingWorksOnExistingAliasedInstances()
{
    $container = new Container;

    $container->alias("IlluminateTestsContainerIContainerContractStub", "stub");
    $container->instance("stub", new ContainerImplementationStub);

    $container->when("IlluminateTestsContainerContainerTestContextInjectOne")->needs("stub")->give("IlluminateTestsContainerContainerImplementationStubTwo");

    $this->assertInstanceOf(
        "IlluminateTestsContainerContainerImplementationStubTwo",
        $container->make("IlluminateTestsContainerContainerTestContextInjectOne")->impl
    ); 
}

由于instance的特性,当别名被绑定到其他对象上时,别名 stub 已经失去了与 IlluminateTestsContainerIContainerContractStub 之间的关系,因此不能使用 stub 代替作上下文绑定。
但是另一方面:

public function testContextualBindingWorksOnBoundAlias()
{
    $container = new Container;

    $container->alias("IlluminateTestsContainerIContainerContractStub", "stub");
    $container->bind("stub", ContainerImplementationStub::class);

    $container->when("IlluminateTestsContainerContainerTestContextInjectOne")->needs("stub")->give("IlluminateTestsContainerContainerImplementationStubTwo");

    $this->assertInstanceOf(
        "IlluminateTestsContainerContainerImplementationStubTwo",
        $container->make("IlluminateTestsContainerContainerTestContextInjectOne")->impl
    ); 
}

代码只是从 instance 绑定改为 bind 绑定,由于 bind 绑定只切断了别名中的 alias 数组的联系,并没有断绝abstractAlias数组的联系,因此这段代码却可以通过,很让人难以理解。本人在给 Taylor Otwell 提出 PR 时,作者原话为“I"m not making any of these changes to the container on a patch release.”。也许,在以后(5.5或以后)版本作者会更新这里的逻辑,我们就可以看看服务容器对别名绑定的态度了,大家也最好不要这样用。

服务容器中的闭包函数参数

服务容器中很多函数都有闭包函数,这些闭包函数可以放入特定的参数,在绑定或者解析过程中,这些参数会被服务容器自动带入各种类对象或者服务容器实例。

bind 闭包参数
public function testAliasesWithArrayOfParameters()
{
    $container = new Container;    
    $container->bind("foo", function ($app, $config) {
        return $config;    
    });    

    $container->alias("foo", "baz");    
    $this->assertEquals([1, 2, 3], $container->makeWith("baz", [1, 2, 3]));
}
extend 闭包参数
public function testExtendedBindings()
{
    $container = new Container;    
    $container["foo"] = "foo’;    
    $container->extend("foo", function ($old, $container) {
        return $old."bar’;    
    });
   
    $this->assertEquals("foobar", $container->make("foo"));
    
    $container = new Container;
    
    $container->singleton("foo", function () {
        return (object) ["name" => "taylor"];    
    });    
    $container->extend("foo", function ($old, $container) {
        $old->age = 26;
        return $old;    
    });
    
    $result = $container->make("foo");
    $this->assertEquals("taylor", $result->name);    
    $this->assertEquals(26, $result->age);   
    $this->assertSame($result, $container->make("foo"));
}
bindmethod 闭包参数
public function testCallWithBoundMethod()
{
    $container = new Container;
    $container->bindMethod("IlluminateTestsContainerContainerTestCallStub@unresolvable", function ($stub,$container) {
        $container["foo"] = "foo";
        return $stub->unresolvable("foo", "bar");
    });
    $result = $container->call("IlluminateTestsContainerContainerTestCallStub@unresolvable");
    $this->assertEquals(["foo", "bar"], $result);
    $this->assertEquals("foo",$container["foo"]);
}
resolve 闭包参数
public function testResolvingCallbacksAreCalledForSpecificAbstracts()
{
     $container = new Container;
     $container->resolving("foo", function ($object,$container) {
         return $object->name = "taylor";
     });
 
     $container->bind("foo", function () {
        return new StdClass;
     });
     $instance = $container->make("foo");

     $this->assertEquals("taylor", $instance->name);
}
rebinding 闭包参数
public function testReboundListeners()
{
    $container = new Container;
    $container->bind("foo", function () {
        return "foo";
    });
  
    $container->rebinding("foo", function ($container,$object) {
         $container["bar"] = $object."bar";
    });
  
    $container->bind("foo", function () {
    });

    $this->assertEquals("bar",$container["foobar"]);
}

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/23012.html

相关文章

  • 深入剖析 Laravel 服务容器

    摘要:划下重点,服务容器是用于管理类的依赖和执行依赖注入的工具。类的实例化及其依赖的注入,完全由服务容器自动的去完成。 本文首发于 深入剖析 Laravel 服务容器,转载请注明出处。喜欢的朋友不要吝啬你们的赞同,谢谢。 之前在 深度挖掘 Laravel 生命周期 一文中,我们有去探究 Laravel 究竟是如何接收 HTTP 请求,又是如何生成响应并最终呈现给用户的工作原理。 本章将带领大...

    abson 评论0 收藏0
  • 为什么我们需要 Laravel IoC 容器

    摘要:哲学的一个重要组成部分就是容器,也可以称为服务容器。那我们要怎么做呢请看下面的例子数据库连接通过上面的代码,如果我们想把改成,根本不需要去修改类构造函数里的依赖。现在我要讲下容器里到底发生了什么。 showImg(https://segmentfault.com/img/remote/1460000018868909); IOC 容器是一个实现依赖注入的便利机制 - Taylor Ot...

    xiaokai 评论0 收藏0
  • Laravel 深入核心系列教程

    摘要:前言年底了不太忙,最近一段时间也一直在研究,就想写篇关于比较深一点的教程系列啥的,于是就找到站长给开了写教程的渠道。优点的就是为艺术家创造的框架,它也是工程化的趋势。项目维护方便也是事实。如果有遇到问题可以直接在教程下面留言。 前言 年底了不太忙,最近一段时间也一直在研究laravel,就想写篇关于laravel比较深一点的教程系列啥的,于是就找到站长给开了写教程的渠道。由于第一次写,...

    wemall 评论0 收藏0
  • PHPer、Laravel 面试可能会遇到问题及答案

    摘要:如何实现持久化持久化,将在内存中的的状态保存到硬盘中,相当于备份数据库状态。相当于备份数据库接收到的命令,所有被写入的命令都是以的协议格式来保存的。 最近社区里面有一篇文章引起了最多程序猿的关注,Laravel、PHPer 面试可能会遇到的问题,看评论区不少小伙伴们被难倒,对于一些问题同样难倒了我(其实有很多啦),趁着周末有空,又总结梳理了一遍,顺便来答一波题。由于个人技术水平有限,答...

    fanux 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<