首页 > 代码库 > laravel框架容器管理的一些要点

laravel框架容器管理的一些要点

  • 本文面向php语言的laravel框架的用户,介绍一些laravel框架里面容器管理方面的使用要点。文章很长,但是内容应该很有用,希望有需要的朋友能看到。php经验有限,不到位的地方,欢迎帮忙指正。

    1. laravel容器基本认识

    laravel框架是有一个容器框架,框架应用程序的实例就是一个超大的容器,这个实例在bootstrap/app.php内进行初始化:

    技术分享

    这个文件在每一次请求到达laravel框架都会执行,所创建的$app即是laravel框架的应用程序实例,它在整个请求生命周期都是唯一的。laravel提供了很多服务,包括认证,数据库,缓存,消息队列等等,$app作为一个容器管理工具,负责几乎所有服务组件的实例化以及实例的生命周期管理。这种方式能够很好地对代码进行解耦,使得应用程序的业务代码不必操心服务组件的对象从何而来,当需要一个服务类来完成某个功能的时候,仅需要通过容器解析出该类型的一个实例即可。从最终的使用方式来看,laravel容器对服务实例的管理主要包括以下几个方面:

    • 服务的绑定与解析
    • 服务提供者的管理
    • 别名的作用
    • 依赖注入

    弄清这几个方面的思想, 以及laravel容器的实现机制,就能熟练掌握laravel容器的管理。

    2. 如何在代码中获取到容器实例

    laravel容器实例在整个请求生命周期中都是唯一的,且管理着所有的服务组件实例。那么有哪些方式能够拿到laravel容器的实例呢?常用的有以下几种方式:

    1) 通过app这个help函数:

    + View code
     

    app这个辅助函数定义在 
    技术分享 
    文件里面,这个文件定义了很多help函数,并且会通过composer自动加载到项目中。所以,在参与http请求处理的任何代码位置都能够访问其中的函数,比如app()。

    2)通过App这个Facade

    + View code
     

    通过App这个Facade拿容器实例的方式,跟上面不同的是,不能把App先赋给一个变量,然后通过变量来调用容器的方法。这是因为App相当于只是一个类名,我们不能把一个类名复制一个变量。$app = App;不是一个合法的可执行的语句,而$app = app();却是一个合法的可执行的语句,因为它后面有app(),表示函数调用。App::basePath();也是一个合法的语句,它就是在调用类的静态方法。

    再补充2点:

    第一点: Facade是laravel框架里面比较特殊的一个特性,每个Facade都会与容器里面的一个实例对象关联,我们可以直接通过Facade类静态方法调用的形式来调用它关联的实例对象的方法。比如App这个Facade,调用App::basePath()的时候,实际相当于app()->basePath()。这个底层机制也是依赖于php语言的特性才能实现的,需要在每一个Facade里面,设定一个静态成员并关联到一个服务的实例对象,当调用Facade类的静态方法的时候,解析出调用的方法名,再去调用关联的服务实例的同名方法,最后把结果返回。我认为理解Facade能起到什么作用就够了,不一定要深究到它底层去了解实现的细节,毕竟在实际的开发中,不用Facade,也完全不影响laravel框架的使用。另外在实际编码中,要自定义一个Facade也非常容易,只要继承laravel封装的Facade基类即可:

    + View code
     

    实现Facade基类的getFacadeAccessor方法,laravel框架就知道这个Facade类该与哪个服务实例关联起来了。实际上这个getFacadeAccess方法,返回的名称就是后面要介绍的服务绑定名称。在laravel容器里面,一个服务实例,都会有一个固定的绑定名称,通过这个名称就能找到这个实例。所以为啥Facade类只要返回服务绑定名称即可。

    我们可以看看App这个Facade类的代码:

    + View code
     

    它的getFacadeAccessor返回的就是一个字符串“app”,这个app就是laravel容器自己绑定自己时用的名称。

    第二点: 从上一点最后App这个Facade的源码可以看出,App这个Facade的全类名其实是:Illuminate\Support\Facades\App,那为什么我们在代码里面能够直接通过App这个简短的名称就能访问到呢:

    + View code
     

    你看以上代码完全没有用到use或者完全限定的方式来使用Illuminate\Support\Facades\App。实际上App跟Illuminate\Support\Facades\App是完全等价的,只不过App比Illuminate\Support\Facades\App要简短很多,而且不需要use,所以用起来方便,那么它是怎么实现的?这跟laravel容器配置的别名有关系,在config/app.php中,有一节aliases专门用来配置一些类型的别名:

    + View code
     

    然后在laravel框架处理请求过程中,会通过Illuminate\Foundation\Bootstrap\RegisterFacades这个类来注册这些别名到全局环境里面:

    + View code
     

    所以我们才能直接通过别名,代替完整的类型名做同样的访问功能。如果你自己写了一些类,名称很长,并且在代码里面用的特别多,也可以考虑配置到config/app.php别名里面去,laravel会帮我们注册。

    3)另外一种方式拿到laravel容器实例就是在服务提供者里面直接使用$this->app

    服务提供者后面还会介绍,现在只是引入。因为服务提供者类都是由laravel容器实例化的,这些类都继承自Illuminate\Support\ServiceProvider,它定义了一个实例属性$app:

    技术分享

    laravel在实例化服务提供者的时候,会把laravel容器实例注入到这个$app上面。所以我们在服务提供者里面,始终能通过$this->$app访问到laravel容器实例,而不需要再使用app()函数或者App Facade了。

    3. 直观的认识laravel容器

    一直在说容器,既然它是用来存取实例对象的时候,那么它里面应该至少有一个数组充当容器存储功能的角色才行,所以我们可以通过打印的方式来直观地看下laravel容器实例的结构:

    + View code
     

    结果如下: 
    技术分享 
    从这个结构可以看出,laravel容器实例上包含了很多的数组,其中红框部分的数组,从名字也可以猜测出它们跟后面要介绍的服务,服务提供者与服务别名之间的联系。理清这几个数组的存储结构,自然就明白了laravel容器如何管理服务。

    4. 如何理解服务绑定与解析

    浅义层面理解,容器既然用来存储对象,那么就要有一个对象存入跟对象取出的过程。这个对象存入跟对象取出的过程在laravel里面称为服务的绑定与解析。

    先来看服务绑定,在laravel里面,服务绑定到容器,有多种形式:

    + View code
     

    singleton是laravel服务绑定的方法之一,详细作用后面会介绍,目前只是用它来展现服务绑定的形式。笼统的说容器的时候,我们说容器管理的是服务对象,但是laravel的容器可以管理不仅仅是对象,它能够管理的是任意类型的数据,包括基本数据类型和对象。所以在服务绑定的时候,我们也可以绑定任意的数据,正如以上代码展示的那样。在绑定的时候,我们可以直接绑定已经初始化好的数据(基本类型、数组、对象实例),还可以用匿名函数来绑定。用匿名函数的好处在于,这个服务绑定到容器以后,并不会立即产生服务最终的对象,只有在这个服务解析的时候,匿名函数才会执行,此时才会产生这个服务对应的服务实例。

    实际上,当我们使用singleton,bind方法以及数组形式,(这三个方法是后面要介绍的绑定的方法),进行服务绑定的时候,如果绑定的服务形式,不是一个匿名函数,也会在laravel内部用一个匿名函数包装起来,这样的话, 不轮绑定什么内容,都能做到前面介绍的懒初始化的功能,这对于容器的性能是有好处的。这个可以从bind的源码中看到一些细节:

    技术分享

    服务绑定时的第一个参数就是服务的绑定名称。服务绑定完成后,容器会把这个服务的绑定记录存储到实例属性bindings里面:

    技术分享

    这个bindings里面的每条记录,代表一个服务绑定。它的key值就是服务的绑定名称,value值也是一个数组,这个数组的concrete属性就是服务绑定时产生的匿名函数,也就是闭包;另外一个参数表示这个服务在多次解析的时候,是否只返回第一次解析得到的对象。这个参数在介绍服务绑定方法时会再继续介绍。

    接下来看看服务绑定的几种方法及区别:

    a. 通过bind方法

    + View code
     

    bind是laravel服务绑定的底层方法,它的签名是:

    技术分享

    第一个参数服务绑定名称,第二个参数服务绑定的结果,第三个参数就表示这个服务是否在多次解析的时候,始终返回第一次解析出的实例。它的默认值是false,意味着这样的服务在每次解析的时候都会返回一个新的实例。它的值与bindings里面服务绑定记录value数组里面的share属性是对应的。

    b. 通过singleton方法

    举例略。它跟bind的区别在于,它始终是以shared=true的形式进行服务绑定,这是因为它的源码是这样的:

    技术分享

    c. 通过数组的形式

    + View code
     

    为什么可以直接把容器实例直接当成数组来用呢,这是因为容器实现了php的ArrayAccess接口:

    + View code
     

    所以实际上以上这种数组形式的绑定实际上相当于没有第三个参数的bind方法。

    再来看服务的解析。上面的内容都是在说明把如何获取服务实例的方式绑定到容器,那么如何从容器获取到需要的服务实例呢?这个过程就是服务解析,在laravel里面通过make方法来完成服务的解析:

    + View code
     

    这个方法接收两个参数,第一个是服务的绑定名称和服务绑定名称的别名,如果是别名,那么就会根据服务绑定名称的别名配置,找到最终的服务绑定名称,然后进行解析;第二个参数是一个数组,最终会传递给服务绑定产生的闭包。

    我们可以通过make的源码理解服务解析的逻辑,这个是Illuminate\Container\Container类中的make方法源码,laravel的容器实例是Illuminate\Foundation\Application类的对象,这个类继承了Illuminate\Container\Container,这里暂时只展示Illuminate\Container\Container类中的make方法的代码,先不涉及Illuminate\Foundation\Application类的make方法,因为后者覆盖了Illuminate\Container\Container类中的make方法,加了一些服务提供者的逻辑,所以这里先不介绍它。其实前面的很多源码也都是从Illuminate\Container\Container中拿出来的,不过那些代码Application没有覆盖,不影响内容的介绍。

    + View code
     

    从这个源码可以看到:

    a. 在解析一个服务的时候,它会先尝试把别名转换成有效的服务绑定名称;

    b. 如果这个服务是一个shared为true的服务绑定,且之前已经做过解析的话,就会直接返回之前已经解析好的对象;

    c. 如果这个服务是一个shared为true的服务绑定,并且是第一次解析的话,就会把已解析的对象存入到instances这个容器属性里面去,也就是说只有shared为true的服务绑定,在解析的时候才会往instances属性里面存入记录,否则不会存入;

    d. 解析完毕,还会在容器的resolved属性里面存入一条记录,表示这个服务绑定解析过;

    e. resolved,instances数组的key值跟bindings数组的key值一样,都是服务绑定名称;

    f. 服务绑定的shared属性在整个服务绑定生命周期内都是不能更改的。

    服务的解析也有多种形式,常用的有:

    a. make方法

    b. 数组形式

    + View code
     

    这个的原理还是跟容器实现了ArrayAccess的接口有关系:

    + View code
     

    所以数组形式的访问跟不使用第二个参数的make方法形式是一样的。

    c. app($service)的形式

    + View code
     

    看了app这个help函数的源码就明白了:

    + View code
     

    原来app这个函数在第一个参数为空的时候,返回的是容器实例本身。在有参数的时候等价于调用容器实例的make方法。

    以上就是服务绑定与解析的主要内容,涉及的要点较多,希望描述的比较清楚。

    5. 服务提供者的作用与使用

    前面介绍了服务的绑定。那么服务的绑定应该在哪个位置处理呢?虽然说,能够拿到容器实例的地方,就都能进行服务的绑定;但是我们使用服务的绑定的目的,是为了在合适的位置解析出服务实例并使用,如果服务绑定的位置过于随意,那么就很难保证在解析的位置能够准确的解析出服务实例。因为服务能够解析的前提是服务绑定的代码先与服务解析的代码执行;所以,服务绑定通常会在应用程序初始化的时候进行,这样才能保证业务代码中(通常是router和controller里面)一定能解析出服务实例。这个最佳的位置就是服务提供者。

    服务提供者,在laravel里面,其实就是一个工厂类。它最大的作用就是用来进行服务绑定。当我们需要绑定一个或多个服务的时候,可以自定义一个服务提供者,然后把服务绑定的逻辑都放在该类的实现中。在larave里面,要自定一个服务提供者非常容易,只要继承Illuminate\Support\ServiceProvider这个类即可。下面通过一个简单的自定义服务提供者来说明服务提供者的一些要点:

    + View code
     

    1). 首先,自定义的服务提供者都是放在下面这个目录的: 
    技术分享 
    其实你放在哪都可以,不过得告诉laravel你的服务提供者在哪,laravel才会帮你注册。怎么告诉它,后面还有介绍。

    2)在这个举例里面,可以看到有一个register方法,这个方法是ServiceProvider里面定义的。自定义的时候,需要重写它。这个方法就是用来绑定服务的。你可以在这个服务里面,根据需要加入任意数量的服务绑定。前面要介绍过,在服务提供者里面,始终能通过$this->app拿到容器实例,所以上面的举例中,我们直接用这种方式来完成服务绑定。这个方法是怎么完成服务绑定的呢?因为当laravel找到这个服务提供者的类以后,就会初始化这个服务提供者类,得到一个服务提供者的对象,然后调用它的register方法,自然它里面的所有服务绑定代码就都会执行了。

    laravel初始化自定义服务提供者的源码是:

    + View code
     

    这个代码是在Illuminate\Foundation\Application的源码里面拿出来的,从中你能看到laravel会把所有的自定义服务提供者都注册进来。这个注册的过程其实就是前面说的实例化服务提供者的类,并调用register方法的过程。

    3). 从上一步的源码也能看到,laravel加载自定义服务提供者的时候,实际是从config/app.php这个配置文件里面的providers配置节找到所有要注册的服务提供者的。

    + View code
     

    所以你如果自己写了一个服务提供者,那么只要配置到这里面,laravel就会自动帮你注册它了。

    4)除了register方法,服务提供者里面还有一个boot方法,这个boot方法,会在所有的服务提供者都注册完成之后才会执行,所以当你想在服务绑定完成之后,通过容器解析出其它服务,做一些初始化工作的时候,那么就可以这些逻辑写在boot方法里面。因为boot方法执行的时候,所有服务提供者都已经被注册完毕了,所以在boot方法里面能够确保其它服务都能被解析出来。

    5)前面说的服务提供者的情况,在laravel应用程序初始化的时候,就会去注册服务提供者,调用register方法。但是还有一种需求,你可能需要在真正用到这个服务提供者绑定的服务的时候,才会去注册这个服务提供者,以减少不必要的注册处理,提高性能。这也是延迟处理的一种方式。那么这种服务提供者该怎么定义呢?

    其实最前面的这个举例已经告诉你了,只要定义一个$defer的实例属性,并把这个实例属性设置为true,然后添加一个provides的实例方法即可。这两个成员都是ServiceProvider基类里面定义好的,自定义的时候,只是覆盖而已。

    在基类中,$defer的默认值是false,表示这个服务提供者不需要延迟注册。provides方法,只要简单的返回这个服务提供register方法里面,注册的所有服务绑定名称即可。

    延迟注册的服务提供者的机制是:

    • 当laravel初始化服务提供者的实例后,如果发现这个服务提供者的$defer属性为true,那么就不会去调用它的register方法
    • 当laravel解析一个服务的时候,如果发现这个服务是由一个延迟服务提供的(它怎么知道这个服务是延迟服务提供的,是provides方法告诉它的),那么就会先把这个延迟服务提供者先注册,再去解析。这个可以看看Illuminate\Foundation\Application的make方法就清楚了: 
      + View code
       

    6)还记得容器实例结构上几个带有providers名称的属性数组吧:

    技术分享

    在了解以上provider的机制后,这几个数组的作用也就比较清晰了。其中serviceProviders用来存放所有已经注册完毕的服务提供者:

    技术分享

    loadedProviders跟serviceProviders的作用类似,只是存储的记录形式不同:

    技术分享

    deferredProviders用来存储所有的延迟注册的服务提供者:

    技术分享

    跟前面两个不同的是,deferredProviders存储的记录的key值并不是服务提供者的类型名称,而是服务提供者的provides返回数组里面的名称。并且如果一个服务提供者的provides里面返回了多个服务绑定名称的话,那么deferredProviders里面就会存多条记录:

    技术分享

    这样是方便根据服务绑定名称,找到对应的服务提供者,并完成注册。当服务的解析的时候,会先完成延迟类型的服务提供者的注册,注册完毕,这个服务绑定名称在deferredProviders对应的那条记录就会删除掉。不过如果一个服务提供者provides了多个服务绑定名称,解析其中一个服务的时候,只移除该名称对应的deferredProviders记录,而不是所有。

    7)服务提供者还有一个小问题值的注意,由于php是一门基本语言,在处理请求的时候,都会从入口文件把所有php都执行一遍。为了性能考虑,laravel会在第一次初始化的时候,把所有的服务提供者都缓存到bootstrap/cache/services.php文件里面,所以有时候当你改了一个服务提供者的代码以后,再刷新不一定能看到期望的效果,这有可能就是因为缓存所致。这时把services.php删掉就能看到你要的效果了。

    6. 服务绑定名称的别名

    前面介绍的别名是在config/app.php的aliases配置节里面定义的,那个别名的作用仅仅是简化类名的时候,laravel帮你把长的类型名注册成为简短的名称,然后在全局环境了里面都能使用。laravel还存在另外一个别名,就是服务绑定名称的别名。通过服务绑定的别名,在解析服务的时候,跟不使用别的效果一致。别名的作用也是为了同时支持全类型的服务绑定名称以及简短的服务绑定名称考虑的。

    1)如何指定和使用服务绑定名称的别名

    假如有一个服务做如下绑定:

    + View code
     

    那么可以通过容器方法alias方法指定别名:

    + View code
     

    这个方法的第一个参数是服务绑定名称,第二个参数是别名。这个方法调用后,就会在容器实例属性aliases数组里面存入一条记录:

    技术分享 
    技术分享

    你看刚才举例中的别名就已经添加到这个数组里面。这个数组里面每条记录的key值都是别名。但是value有可能是服务绑定名称,也有可能是另外一个别名。这是因为别名是可以递归的。

    2)别名支持递归

    也就是说,可以对别名再指定别名:

    + View code
     

    技术分享

    3)别名如何应用于服务解析

    在解析服务的时候,会先确定这个服务名称是否为一个别名(只要看看在aliases数组里是否存在记录即可),如果不是别名,直接用这个服务名称进行解析。如果这个服务名称是一个别名,那么就会通过调用的方式,找到最终的服务名称:

    技术分享

    如下所有的服务解析都是等价的:

    + View code
     

    4)另外一种指定别名的方式

    可以在服务绑定的时候,进行别名的指定。只要按照如下的方式进行绑定即可:

    + View code
     

    也就是把服务绑定名称换成数组形式而已。数组记录的key值就是服务名称,value值就是别名。

    7. 依赖注入的机制

    + View code
     

    在这个举例中,定义了一个Service类,这个类有一个实例成员$app,它需要一个实现了\Illuminate\Contracts\Foundation\Application 接口的实例对象,也就是容器实例。然后通过直接使用类型名称的方式把这个类快速地绑定到了容器。app()->singleton(Service::class),等价于app()->singleton(Service::class,Service:class)。这种通过类名形式的绑定,laravel在解析的时候会调用这个类型的构造函数来实例化服务。并且在调用构造函数的时候,会通过反射获得这个构造函数的参数类型,然后从容器已有的绑定中,解析出对应参数类型的服务实例,传入构造函数完成实例化。这个过程就是所谓的依赖注入。

    在以上代码中,完全没有手写的new Service(app())代码,就能正确地解析到service实例,这就是依赖注入的好处:

    技术分享

    当一个类需要某个服务类型的实例时,不需要自己去创造这个服务的实例,只要告诉容器,它需要的实例类型即可,然后容器会根据这个类型, 解析出满足该类型的服务。如何根据参数类型解析出该参数类型的服务实例呢?其实就是根据参数类型的类型名称进行解析得到的,所以依赖注入能够成功的前提是根据参数类型的名称,能够成功地解析到一个服务对象。以上之所以能够通过Illuminate\Contracts\Foundation\Application 这个名称解析到服务,那是因为在容器实例aliases数组里面有一条Illuminate\Contracts\Foundation\Application 的别名记录:

    技术分享

    也就是说Illuminate\Contracts\Foundation\Application 实际上是app这个服务绑定名称的一个别名,所以laravel在解析Illuminate\Contracts\Foundation\Application的时候,就能得到对应的服务实例了。

    这些别名属于laravel容器核心的别名,在laravel初始化的时候会被注册:

    + View code
     

    依赖注入更多地用在接口编程当中,就像上面的举例类似。再看一个自定义的例子:

    + View code
     

    按接口进行编程,像Service这种业务类,只需要声明自己需要一个Inter类型的实例即可。接口的好处在于解耦,将来要更换一种Inter的实现,不需要改Service的代码,只需要在实例化Service的时候,传入另外一个Inter的实例即可。有了依赖注入以后,也不用改Service实例化的代码,只要把Inter这个服务类型,重新做一个绑定,绑定到另外一个实现即可。

    + View code
     

    8. 其它

    还有两个小点,也值的介绍一下。

    1) 容器实例的instance方法

    这个方法其实也是完成绑定的作用,但是它跟前面介绍的三种绑定方法不同,它是把一个已经存在的实例,绑定到容器:

    + View code
     

    这是它的源码:

    + View code
     

    从这个代码可以看到,instance方法,会直接把外部实例化好的对象,直接存储到容器的instances里面。如果这个服务绑定名称存在bindings记录,那么还会做一下重新绑定的操作。也就是说,通过intance方法绑定,是直接绑定服务实例,而原来的bind方法其实只是绑定了一个闭包函数,服务实例要到解析的时候才会创建。

    2) 容器实例的share方法

    容器实例的singleton方法,绑定的服务在解析的时候,始终返回第一次解析的对象。还有一个方式也能做到这个效果,那就是使用share方法包装服务绑定的匿名函数:

    + View code
     

    当我们使用app(‘cas‘)解析的时候,始终拿到的都是第一次解析创建的那个CasManager对象。这个跟share方法的实现有关系:

    技术分享

    从源码看出,share方法把服务绑定的闭包再包装了一下,返回一个新的闭包,并且在这个闭包里面,加了一个静态$object变量,它会存储原始闭包第一次解析调用后的结果,并在后续解析中直接返回,从而保证这个服务的实例只有一个。

laravel框架容器管理的一些要点