当前位置: 首页 > 后端技术 > PHP

如何理解Laravel和ThinkPHP5中的服务容器和注入?

时间:2023-03-29 23:12:43 PHP

来自文档很多人一开始看到官方文档,不管是Laravel还是ThinkPHP,看完之后都是一头雾水,不太了解。我什至直接跳过,不看,反正这么高端的东西我反正用不上,短时间内有这个想法很正常,尤其是用惯了ThinkPHP3的用户,介绍的概念比较前沿,想了半天不了解,就看自己的职业规划了。接下来,就让我们一起来看看,认真跟拍产品吧。从Laravel入手,从Laravel的文档可以看出有bind、singleton和instance这三个常用的方法,接下来我会一一解答。实际应用假设我们有这样一个场景。我们的用户在注册的时候,需要给用户的手机发送一个短信验证码,然后当用户收到验证码,提交注册表的时候,需要验证验证码是否正确。这个需求看起来很容易实现吧?当我们拿到短信平台的开发文档时,只需要写两个方法即可。send和check分别用于发送验证码和验证验证码。接下来,在不使用容器的情况下编写伪代码。MeiSms.phpset('sms:'.$phone,$code,5*60);返回真;}publicfunctioncheck($phone,$code){$cacheManager=cache();返回$cacheManager->get('sms:'.$phone)===$code;}}简单,不是吗?然后在controller中新建一个MeiSms实例,直接调用send和check分别发送和校验验证码。但是如果运行突然报之前提供的短信平台不靠谱,导致短信发送不稳定,用户经常收不到。这时候,我们就需要改变界面了。常见的做法是另外写一个对象,然后说不定这段代码是别人写的给你接收的。你觉得send或者check的方法名不够规范,然后你改了,然后顺便把原来的注册端改了,然后代码突然上线开始运行了。没过多久,运营商觉得这个平台的短信太贵了,又找了一个又便宜又稳定的,然后你又重复了上面的操作。这一次,你觉得这个方法已经很完美了,不需要再改了。.只需要写好方法体,然后在调用的地方改一些新的类名即可。当然,这只是一个小例子。在开发过程中,我们可能会遇到比这更复杂的变化,或者,运营要你切换回之前的版本?嗯。在这里,如果有了解简单工厂模式的朋友,可能会觉得我可以用简单工厂模式来搞定这件事。functionfactory($name){$modules=['sms'=>newMeiSms(),];if(!isset($modules[$name])){thrownew\Exception('对象不存在。');}return$modules[$name];}直接在需要的地方调用factory('sms')这样就可以得到一个发送短信的对象。需求变化的时候,我直接修改工厂就可以了,不是吗?简单多了。然而,在这里你会发现一些问题。工厂生产出来的对象是没有类型提示的,我们也没有办法限制这个类必须在工厂中实现哪些方法(当然你可以把工厂做得更复杂,再加上接口验证),但是最后你会发现最初在这里要做的事情是越来越远,越来越复杂,不是吗?此外,工厂也不是那么容易使用。服务容器这里就不得不回过头来看看我们的服务容器。首先我们看controller的文档中对依赖注入部分的描述。这是许多人第一次了解依赖注入的地方。注入到Laravel服务容器中的构造函数解析所有控制器。因此,您可以在控制器的构造函数中对可能需要的依赖项进行类型提示。依赖声明被自动解析并注入控制器实例方法注入处理构造函数注入,您还可以在控制器方法中对依赖项进行类型提示。方法注入最常见的用例是将Illuminate\Http\Request的实例注入到控制器方法中。我们每次创建控制器方法时,都会主动填写第一个参数,即Request$request。你有没有注意到那个请求?参数呢?是不是很神奇?为什么我什么都不做,我可以用,而且不局限于Laravel内置的对象,我们也可以自己写对象,用IDE开发的时候也可以方便的使用类型提示,这些工作,就是什么服务容器为我们做了。当容器解析到这个方法时,当这个方法存在时,它会使用反射来解析这个方法中需要的参数和参数类型。ReflectionFunctionAbstract::getParameters,然后在容器中查找我们是否有这个类型的bind,如果没有,继续使用容器创建这个类(因为这个类的构造方法可能依赖其他类),直到依赖类instance改造完成,将实例存放到容器中。至此,你是不是觉得服务容器没用了?只是为了帮助我们递归实例化类。那你太年轻了,既然上面说了简单工厂解决的问题需要用服务容器来解决,我还会骗你吗?哈哈,下面就要开始讲第一个bindbind方法了首先,我们看一下Laravel中bind方法的实现。/***向容器注册一个绑定。**@paramstring$abstract*@param\Closure|string|null$concrete*@parambool$shared*@returnvoid*/publicfunctionbind($abstract,$concrete=null,$shared=false){//如果没有给出具体类型,我们将简单地将具体类型设置为//抽象类型。之后,具体类型将被注册为共享//而不必在两个参数中声明它们的类。$this->dropStaleInstances($abstract);如果(is_null($concrete)){$concrete=$abstract;}//如果工厂不是闭包,这意味着它只是一个类名,//绑定到这个容器中的抽象类型,我们将把它包装起来//在它自己的闭包中给我们更多的方便延伸。if(!$concreteinstanceofClosure){$concrete=$this->getClosure($abstract,$concrete);$this->bindings[$abstract]=compact('concrete','shared');//如果抽象类型已经在此容器中解析,我们将触发//反弹侦听器,以便任何已经解析的对象//都可以通过侦听器回调更新其对象副本。如果($this->resolved($abstract)){$this->rebound($abstract);}}/***删除所有陈旧的实例和别名。**@paramstring$abstract*@returnvoid*/protectedfunctiondropStaleInstances($abstract){unset($this->instances[$abstract],$this->aliases[$abstract]);}}/***获取在构建类型时要使用的闭包。**@paramstring$abstract*@paramstring$concrete*@return\Closure*/protectedfunctiongetClosure($abstract,$concrete){返回fu函数($container,$parameters=[])使用($abstract,$concrete){如果($abstract==$concrete){return$container->build($concrete);}返回$container->make($concrete,$parameters);};在一开始,$this->dropStaleInstances($abstract);追源码可以看到,它直接删除了第一个参数对应的已有实例和别名,然后继续下一步,当$concrete不是Closure(匿名方法)时,会做一些包装,加工成匿名方法,最后存储属性绑定,key是$abstract,value是一个数组,其中concrete是封装的方法,然后调用容器的make。.运行到这个位置时if($this->resolved($abstract)){$this->rebound($abstract);}会先判断this是否已经resolved,更新容器中已经存在的副本。这就是绑定的作用。简单地描述一下,它为类、类实例和匿名方法提供了一个别名,并将其绑定到容器中。当我们刚才使用resolve传入别名的时候,我们就可以解析得到我们之前绑定的实例。快点试试?我们先打开bootstrap/app.php,可以看到一开始就创建了一个Application的实例,我们尝试绑定$app再返回。bootstrap/app.php:54$app->bind('你好',\App\Tools\MeiSms::class);return$app;routes/web.php:23Route::any('你好',function(){$resolve=resolve('你好');var_dump(get_class($resolve));});打开浏览器看看,还不错,但是这里,我们只是做了一个类似简单工厂的东西,然后我们改造看看我们的短信类。首先,我们就接口达成一致。短信验证必须有两种方法:发送短信和验证短信验证码,分别是send($phone)和check($phone,$code)。Sms.phpset('sms:'.$phone,$code,5*60);返回真;}publicfunctioncheck(string$phone,string$code):bool{$cacheManager=cache();返回$cacheManager->get('sms:'.$phone)===$code;}}现在,我们去bootstrap/app.php重新注册,这次和之前有点不一样。$app->bind(\App\Contracts\Interfaces\Sms::class,\App\Tools\MeiSms::class);返回$应用程序;可以看到我们第一个参数是Sms的接口,就是要绑定的MeiSms类,然后我们修改路由。web.phpRoute::any('你好',函数(\App\Contracts\Interfaces\Sms$sms){var_dump(get_class($sms));});结果你刚才的截图是不是骗我的?以上显然仅限于\App\Contracts\Interfaces\Sms。怎么打印出来\App\Tools\MeiSms,代码不报错呢?不要惊讶,首先\App\Tools\MeiSms已经实现了\App\Contracts\Interfaces\Sms接口,所以这在接口限定类型中是合法的。并且因为这个方法是容器调用的,所以容器会调用内部的make方法进行一系列的依赖注入处理。当方法需要\App\Contracts\Interfaces\Sms类型的参数时,容器会在绑定中查找类名字符串,因为我们之前已经注册过了,所以相当于为类实现了一个别名,最后\App\Tools\MeiSms会给我们执行结果。好处那么回到正题,我们遇到问题还没有考虑,貌似已经解决了,那么对比前面的方法有什么好处,一一列举。更好的规范因为我们在路由中限制了接口,所以我们不用担心调用send或者检查它是否不存在。不用再担心因为send或者check方法的返回值参数不知道怎么判断结果了(因为我们已经限定了只能返回bool值)不用改原??来的业务代码,bug少是的,就是这样正确的。我们不再需要更改现有的业务代码。我们只需要在实现接口后将新添加的类绑定到容器中,其他的都没有改变。更好的测试填补了空白当然,说到这里,你可能会觉得我漏掉了什么。单例的方法其实从源码中很容易看出来。单例还是调用了bind方法,只是shared参数不同,说明绑定了一个单例对象。/***在容器中注册一个共享绑定。**@paramstring$abstract*@param\Closure|string|null$concrete*@returnvoid*/publicfunctionsingleton($abstract,$concrete=null){$this->bind($abstract,$concrete,true);}实例方法。这与bind几乎相同,只是bind可以绑定匿名方法或直接类名(将在内部处理)。而instance,就像它的名字一样,是用来给容器绑定一个实例的。从小的意义上说,服务容器和工厂模式有很多相似之处,但是服务容器会让你接触到PHP的另一个知识块,反射,这个强大的API。其实我也没搞清楚为什么要以Laravel为例来写这篇文章。因为看了一下ThinkPHP的实现,比较好读。其实一开始我是想说说这两个框架的,但是感觉差不多,所以选择了Laravel。虽然比较复杂,甚至很多点都没有考虑到,但我还是写了这篇文章,不是吗?也希望这篇文章对大家理解服务容器有所帮助。当然,我更推荐你去看Laravel或者ThinkPHP的源码,因为这样可以加深你对它的理解。framework/Container.phpat6.0·top-think/frameworkframework/Container.phpat5.8·laravel/frameworkSupplementNovember9,2019推荐延伸阅读LaravelContainer(容器)深入理解(下)|Laravel中国社区参考依赖注入-维基百科,免费百科全书控制反转(IoC)和依赖注入(DI)-简书服务容器|《Laravel 5.8 中文文档》|Laravel中国社区PHP:Reflection-手册