详解如何实现Laravel的服务容器的方法示例
1.容器的本质
- 服务容器本身就是一个数组,键名就是服务名,值就是服务。
- 服务可以是一个原始值,也可以是一个对象,可以说是任意数据。
- 服务名可以是自定义名,也可以是对象的类名,也可以是接口名。
//服务容器 $container=[ //原始值 'text'=>'这是一个字符串', //自定义服务名 'customName'=>newStdClass(), //使用类名作为服务名 'StdClass'=>newStdClass(), //使用接口名作为服务名 'Namespace\\StdClassInterface'=>newStdClass(), ]; //-----------↓↓↓↓示例代码↓↓↓↓-----------// //绑定服务到容器 $container['standard']=newStdClass(); //获取服务 $standard=$container['standard']; var_dump($standard);
2.封装成类
为了方便维护,我们把上面的数组封装到类里面。
$instances还是上面的容器数组。我们增加两个方法,instance用来绑定服务,get用来从容器中获取服务。
classBaseContainer
{
//已绑定的服务
protected$instances=[];
//绑定服务
publicfunctioninstance($name,$instance)
{
$this->instances[$name]=$instance;
}
//获取服务
publicfunctionget($name)
{
returnisset($this->instances[$name])?$this->instances[$name]:null;
}
}
//-----------↓↓↓↓示例代码↓↓↓↓-----------//
$container=newBaseContainer();
//绑定服务
$container->instance('StdClass',newStdClass());
//获取服务
$stdClass=$container->get('StdClass');
var_dump($stdClass);
3.按需实例化
现在我们在绑定一个对象服务的时候,就必须要先把类实例化,如果绑定的服务没有被用到,那么类就会白白实例化,造成性能浪费。
为了解决这个问题,我们增加一个bind函数,它支持绑定一个回调函数,在回调函数中实例化类。这样一来,我们只有在使用服务时,才回调这个函数,这样就实现了按需实例化。
这时候,我们获取服务时,就不只是从数组中拿到服务并返回了,还需要判断如果是回调函数,就要执行回调函数。所以我们把get方法的名字改成make。意思就是生产一个服务,这个服务可以是已绑定的服务,也可以是已绑定的回调函数,也可以是一个类名,如果是类名,我们就直接实例化该类并返回。
然后,我们增加一个新数组$bindings,用来存储绑定的回调函数。然后我们把bind方法改一下,判断下$instance如果是一个回调函数,就放到$bindings数组,否则就用make方法实例化类。
classDeferContainerextendBaseContainer
{
//已绑定的回调函数
protected$bindings=[];
//绑定服务
publicfunctionbind($name,$instance)
{
if($instanceinstanceofClosure){
//如果$instance是一个回调函数,就绑定到bindings。
$this->bindings[$name]=$instance;
}else{
//调用make方法,创建实例
$this->instances[$name]=$this->make($name);
}
}
//获取服务
publicfunctionmake($name)
{
if(isset($this->instances[$name])){
return$this->instances[$name];
}
if(isset($this->bindings[$name])){
//执行回调函数并返回
$instance=call_user_func($this->bindings[$name]);
}else{
//还没有绑定到容器中,直接new.
$instance=new$name();
}
return$instance;
}
}
//-----------↓↓↓↓示例代码↓↓↓↓-----------//
$container=newDeferContainer();
//绑定服务
$container->bind('StdClass',function(){
echo"我被执行了\n";
returnnewStdClass();
});
//获取服务
$stdClass=$container->make('StdClass');
var_dump($stdClass);
StdClass这个服务绑定的是一个回调函数,在回调函数中才会真正的实例化类。如果没有用到这个服务,那回调函数就不会被执行,类也不会被实例化。
4.单例
从上面的代码中可以看出,每次调用make方法时,都会执行一次回调函数,并返回一个新的类实例。但是在某些情况下,我们希望这个实例是一个单例,无论make多少次,只实例化一次。
这时候,我们给bind方法增加第三个参数$shared,用来标记是否是单例,默认不是单例。然后把回调函数和这个标记都存到$bindings数组里。
为了方便绑定单例服务,再增加一个新的方法singleton,它直接调用bind,并且$shared参数强制为true。
对于make方法,我们也要做修改。在执行$bindings里的回调函数以后,做一个判断,如果之前绑定时标记的shared是true,就把回调函数返回的结果存储到$instances里。由于我们是先从$instances里找服务,所以这样下次再make的时候就会直接返回,而不会再次执行回调函数。这样就实现了单例的绑定。
classSingletonContainerextendsDeferContainer
{
//绑定服务
publicfunctionbind($name,$instance,$shared=false)
{
if($instanceinstanceofClosure){
//如果$instance是一个回调函数,就绑定到bindings。
$this->bindings[$name]=[
'callback'=>$instance,
//标记是否单例
'shared'=>$shared
];
}else{
//调用make方法,创建实例
$this->instances[$name]=$this->make($name);
}
}
//绑定一个单例
publicfunctionsingleton($name,$instance)
{
$this->bind($name,$instance,true);
}
//获取服务
publicfunctionmake($name)
{
if(isset($this->instances[$name])){
return$this->instances[$name];
}
if(isset($this->bindings[$name])){
//执行回调函数并返回
$instance=call_user_func($this->bindings[$name]['callback']);
if($this->bindings[$name]['shared']){
//标记为单例时,存储到服务中
$this->instances[$name]=$instance;
}
}else{
//还没有绑定到容器中,直接new.
$instance=new$name();
}
return$instance;
}
}
//-----------↓↓↓↓示例代码↓↓↓↓-----------//
$container=newSingletonContainer();
//绑定服务
$container->singleton('anonymous',function(){
returnnewclass
{
publicfunction__construct()
{
echo"我被实例化了\n";
}
};
});
//无论make多少次,只会实例化一次
$container->make('anonymous');
$container->make('anonymous');
//获取服务
$anonymous=$container->make('anonymous');
var_dump($anonymous)
上面的代码用singleton绑定了一个名为anonymous的服务,回调函数里返回了一个匿名类的实例。这个匿名类在被实例化时会输出一段文字。无论我们make多少次anonymous,这个回调函数只会被执行一次,匿名类也只会被实例化一次。
5.自动注入
自动注入是Ioc容器的核心,没有自动注入就无法做到控制反转。
自动注入就是指,在实例化一个类时,用反射类来获取__construct所需要的参数,然后根据参数的类型,从容器中找到已绑定的服务。我们只要有了__construct方法所需的所有参数,就能自动实例化该类,实现自动注入。
现在,我们增加一个build方法,它只接收一个参数,就是类名。build方法会用反射类来获取__construct方法所需要的参数,然后返回实例化结果。
另外一点就是,我们之前在调用make方法时,如果传的是一个未绑定的类,我们直接new了这个类。现在我们把未绑定的类交给build方法来构建,因为它支持自动注入。
classInjectionContainerextendsSingletonContainer
{
//获取服务
publicfunctionmake($name)
{
if(isset($this->instances[$name])){
return$this->instances[$name];
}
if(isset($this->bindings[$name])){
//执行回调函数并返回
$instance=call_user_func($this->bindings[$name]['callback']);
if($this->bindings[$name]['shared']){
//标记为单例时,存储到服务中
$this->instances[$name]=$instance;
}
}else{
//使用build方法构建此类
$instance=$this->build($name);
}
return$instance;
}
//构建一个类,并自动注入服务
publicfunctionbuild($class)
{
$reflector=newReflectionClass($class);
$constructor=$reflector->getConstructor();
if(is_null($constructor)){
//没有构造函数,直接new
returnnew$class();
}
$dependencies=[];
//获取构造函数所需的参数
foreach($constructor->getParameters()as$dependency){
if(is_null($dependency->getClass())){
//参数类型不是类时,无法从容器中获取依赖
if($dependency->isDefaultValueAvailable()){
//查找参数的默认值,如果有就使用默认值
$dependencies[]=$dependency->getDefaultValue();
}else{
//无法提供类所依赖的参数
thrownewException('找不到依赖参数:'.$dependency->getName());
}
}else{
//参数类型是类时,就用make方法构建该类
$dependencies[]=$this->make($dependency->getClass()->name);
}
}
return$reflector->newInstanceArgs($dependencies);
}
}
//-----------↓↓↓↓示例代码↓↓↓↓-----------//
classRedis
{
}
classCache
{
protected$redis;
//构造函数中依赖Redis服务
publicfunction__construct(Redis$redis)
{
$this->redis=$redis;
}
}
$container=newInjectionContainer();
//绑定Redis服务
$container->singleton(Redis::class,function(){
returnnewRedis();
});
//构建Cache类
$cache=$container->make(Cache::class);
var_dump($cache);
6.自定义依赖参数
现在有个问题,如果类依赖的参数不是类或接口,只是一个普通变量,这时候就无法从容器中获取依赖参数了,也就无法实例化类了。
那么接下来我们就支持一个新功能,在调用make方法时,支持传第二个参数$parameters,这是一个数组,无法从容器中获取的依赖,就从这个数组中找。
当然,make方法是用不到这个参数的,因为它不负责实例化类,它直接传给build方法。在build方法寻找依赖的参数时,就先从$parameters中找。这样就实现了自定义依赖参数。
需要注意的一点是,build方法是按照参数的名字来找依赖的,所以parameters中的键名也必须跟__construct中参数名一致。
classParametersContainerextendsInjectionContainer
{
//获取服务
publicfunctionmake($name,array$parameters=[])
{
if(isset($this->instances[$name])){
return$this->instances[$name];
}
if(isset($this->bindings[$name])){
//执行回调函数并返回
$instance=call_user_func($this->bindings[$name]['callback']);
if($this->bindings[$name]['shared']){
//标记为单例时,存储到服务中
$this->instances[$name]=$instance;
}
}else{
//使用build方法构建此类
$instance=$this->build($name,$parameters);
}
return$instance;
}
//构建一个类,并自动注入服务
publicfunctionbuild($class,array$parameters=[])
{
$reflector=newReflectionClass($class);
$constructor=$reflector->getConstructor();
if(is_null($constructor)){
//没有构造函数,直接new
returnnew$class();
}
$dependencies=[];
//获取构造函数所需的参数
foreach($constructor->getParameters()as$dependency){
if(isset($parameters[$dependency->getName()])){
//先从自定义参数中查找
$dependencies[]=$parameters[$dependency->getName()];
continue;
}
if(is_null($dependency->getClass())){
//参数类型不是类或接口时,无法从容器中获取依赖
if($dependency->isDefaultValueAvailable()){
//查找默认值,如果有就使用默认值
$dependencies[]=$dependency->getDefaultValue();
}else{
//无法提供类所依赖的参数
thrownewException('找不到依赖参数:'.$dependency->getName());
}
}else{
//参数类型是类时,就用make方法构建该类
$dependencies[]=$this->make($dependency->getClass()->name);
}
}
return$reflector->newInstanceArgs($dependencies);
}
}
//-----------↓↓↓↓示例代码↓↓↓↓-----------//
classRedis
{
}
classCache
{
protected$redis;
protected$name;
protected$default;
//构造函数中依赖Redis服务和name参数,name的类型不是类,无法从容器中查找
publicfunction__construct(Redis$redis,$name,$default='默认值')
{
$this->redis=$redis;
$this->name=$name;
$this->default=$default;
}
}
$container=newParametersContainer();
//绑定Redis服务
$container->singleton(Redis::class,function(){
returnnewRedis();
});
//构建Cache类
$cache=$container->make(Cache::class,['name'=>'test']);
var_dump($cache);
提示:实际上,Laravel容器的build方法并没有第二个参数$parameters,它是用类属性来维护自定义参数。原理都是一样的,只是实现方式不一样。这里为了方便理解,不引入过多概念。
7.服务别名
别名可以理解成小名、外号。服务别名就是给已绑定的服务设置一些外号,使我们通过外号也能找到该服务。
这个就比较简单了,我们增加一个新的数组$aliases,用来存储别名。再增加一个方法alias,用来让外部注册别名。
唯一需要我们修改的地方,就是在make时,要先从$aliases中找到真实的服务名。
classAliasContainerextendsParametersContainer
{
//服务别名
protected$aliases=[];
//给服务绑定一个别名
publicfunctionalias($alias,$name)
{
$this->aliases[$alias]=$name;
}
//获取服务
publicfunctionmake($name,array$parameters=[])
{
//先用别名查找真实服务名
$name=isset($this->aliases[$name])?$this->aliases[$name]:$name;
returnparent::make($name,$parameters);
}
}
//-----------↓↓↓↓示例代码↓↓↓↓-----------//
$container=newAliasContainer();
//绑定服务
$container->instance('text','这是一个字符串');
//给服务注册别名
$container->alias('string','text');
$container->alias('content','text');
var_dump($container->make('string'));
var_dump($container->make('content'));
8.扩展绑定
有时候我们需要给已绑定的服务做一个包装,这时候就用到扩展绑定了。我们先看一个实际的用法,理解它的作用后,才看它是如何实现的。
//绑定日志服务
$container->singleton('log',newLog());
//对已绑定的服务再次包装
$container->extend('log',function(Log$log){
//返回了一个新服务
returnnewRedisLog($log);
});
现在我们看它是如何实现的。增加一个$extenders数组,用来存放扩展器。再增加一个extend方法,用来注册扩展器。
然后在make方法返回$instance之前,按顺序依次调用之前注册的扩展器。
classExtendContainerextendsAliasContainer
{
//存放扩展器的数组
protected$extenders=[];
//给服务绑定扩展器
publicfunctionextend($name,$extender)
{
if(isset($this->instances[$name])){
//已经实例化的服务,直接调用扩展器
$this->instances[$name]=$extender($this->instances[$name]);
}else{
$this->extenders[$name][]=$extender;
}
}
//获取服务
publicfunctionmake($name,array$parameters=[])
{
$instance=parent::make($name,$parameters);
if(isset($this->extenders[$name])){
//调用扩展器
foreach($this->extenders[$name]as$extender){
$instance=$extender($instance);
}
}
return$instance;
}
}
//-----------↓↓↓↓示例代码↓↓↓↓-----------//
classRedis
{
public$name;
publicfunction__construct($name='default')
{
$this->name=$name;
}
publicfunctionsetName($name)
{
$this->name=$name;
}
}
$container=newExtendContainer();
//绑定Redis服务
$container->singleton(Redis::class,function(){
returnnewRedis();
});
//给Redis服务绑定一个扩展器
$container->extend(Redis::class,function(Redis$redis){
$redis->setName('扩展器');
return$redis;
});
$redis=$container->make(Redis::class);
var_dump($redis->name);
9.上下文绑定
有时侯我们可能有两个类使用同一个接口,但希望在每个类中注入不同的实现,例如两个控制器,分别为它们注入不同的Log服务。
classApiController
{
publicfunction__construct(Log$log)
{
}
}
classWebController
{
publicfunction__construct(Log$log)
{
}
}
最终我们要用以下方式实现:
//当ApiController依赖Log时,给它一个RedisLog
$container->addContextualBinding('ApiController','Log',newRedisLog());
//当WebController依赖Log时,给它一个FileLog
$container->addContextualBinding('WebController','Log',newFileLog());
为了更直观更方便更语义化的使用,我们把这个过程改成链式操作:
$container->when('ApiController')
->needs('Log')
->give(newRedisLog());
我们增加一个$context数组,用来存储上下文。同时增加一个addContextualBinding方法,用来注册上下文绑定。以ApiController为例,$context的真实模样是:
$context['ApiController']['Log']=newRedisLog();
然后build方法实例化类时,先从上下文中查找依赖参数,就实现了上下文绑定。
接下来,看看链式操作是如何实现的。
首先定义一个类Context,这个类有两个方法,needs和give。
然后在容器中,增加一个when方法,它返回一个Context对象。在Context对象的give方法中,我们已经具备了注册上下文所需要的所有参数,所以就可以在give方法中调用addContextualBinding来注册上下文了。
classContextContainerextendsExtendContainer
{
//依赖上下文
protected$context=[];
//构建一个类,并自动注入服务
publicfunctionbuild($class,array$parameters=[])
{
$reflector=newReflectionClass($class);
$constructor=$reflector->getConstructor();
if(is_null($constructor)){
//没有构造函数,直接new
returnnew$class();
}
$dependencies=[];
//获取构造函数所需的参数
foreach($constructor->getParameters()as$dependency){
if(isset($this->context[$class])&&isset($this->context[$class][$dependency->getName()])){
//先从上下文中查找
$dependencies[]=$this->context[$class][$dependency->getName()];
continue;
}
if(isset($parameters[$dependency->getName()])){
//从自定义参数中查找
$dependencies[]=$parameters[$dependency->getName()];
continue;
}
if(is_null($dependency->getClass())){
//参数类型不是类或接口时,无法从容器中获取依赖
if($dependency->isDefaultValueAvailable()){
//查找默认值,如果有就使用默认值
$dependencies[]=$dependency->getDefaultValue();
}else{
//无法提供类所依赖的参数
thrownewException('找不到依赖参数:'.$dependency->getName());
}
}else{
//参数类型是一个类时,就用make方法构建该类
$dependencies[]=$this->make($dependency->getClass()->name);
}
}
return$reflector->newInstanceArgs($dependencies);
}
//绑定上下文
publicfunctionaddContextualBinding($when,$needs,$give)
{
$this->context[$when][$needs]=$give;
}
//支持链式方式绑定上下文
publicfunctionwhen($when)
{
returnnewContext($when,$this);
}
}
classContext
{
protected$when;
protected$needs;
protected$container;
publicfunction__construct($when,ContextContainer$container)
{
$this->when=$when;
$this->container=$container;
}
publicfunctionneeds($needs)
{
$this->needs=$needs;
return$this;
}
publicfunctiongive($give)
{
//调用容器绑定依赖上下文
$this->container->addContextualBinding($this->when,$this->needs,$give);
}
}
//-----------↓↓↓↓示例代码↓↓↓↓-----------//
classDog
{
public$name;
publicfunction__construct($name)
{
$this->name=$name;
}
}
classCat
{
public$name;
publicfunction__construct($name)
{
$this->name=$name;
}
}
$container=newContextContainer();
//给Dog类设置上下文绑定
$container->when(Dog::class)
->needs('name')
->give('小狗');
//给Cat类设置上下文绑定
$container->when(Cat::class)
->needs('name')
->give('小猫');
$dog=$container->make(Dog::class);
$cat=$container->make(Cat::class);
var_dump('Dog:'.$dog->name);
var_dump('Cat:'.$cat->name);
10.完整代码
classContainer
{
//已绑定的服务
protected$instances=[];
//已绑定的回调函数
protected$bindings=[];
//服务别名
protected$aliases=[];
//存放扩展器的数组
protected$extenders=[];
//依赖上下文
protected$context=[];
//绑定服务实例
publicfunctioninstance($name,$instance)
{
$this->instances[$name]=$instance;
}
//绑定服务
publicfunctionbind($name,$instance,$shared=false)
{
if($instanceinstanceofClosure){
//如果$instance是一个回调函数,就绑定到bindings。
$this->bindings[$name]=[
'callback'=>$instance,
//标记是否单例
'shared'=>$shared
];
}else{
//调用make方法,创建实例
$this->instances[$name]=$this->make($name);
}
}
//绑定一个单例
publicfunctionsingleton($name,$instance)
{
$this->bind($name,$instance,true);
}
//给服务绑定一个别名
publicfunctionalias($alias,$name)
{
$this->aliases[$alias]=$name;
}
//给服务绑定扩展器
publicfunctionextend($name,$extender)
{
if(isset($this->instances[$name])){
//已经实例化的服务,直接调用扩展器
$this->instances[$name]=$extender($this->instances[$name]);
}else{
$this->extenders[$name][]=$extender;
}
}
//获取服务
publicfunctionmake($name,array$parameters=[])
{
//先用别名查找真实服务名
$name=isset($this->aliases[$name])?$this->aliases[$name]:$name;
if(isset($this->instances[$name])){
return$this->instances[$name];
}
if(isset($this->bindings[$name])){
//执行回调函数并返回
$instance=call_user_func($this->bindings[$name]['callback']);
if($this->bindings[$name]['shared']){
//标记为单例时,存储到服务中
$this->instances[$name]=$instance;
}
}else{
//使用build方法构建此类
$instance=$this->build($name,$parameters);
}
if(isset($this->extenders[$name])){
//调用扩展器
foreach($this->extenders[$name]as$extender){
$instance=$extender($instance);
}
}
return$instance;
}
//构建一个类,并自动注入服务
publicfunctionbuild($class,array$parameters=[])
{
$reflector=newReflectionClass($class);
$constructor=$reflector->getConstructor();
if(is_null($constructor)){
//没有构造函数,直接new
returnnew$class();
}
$dependencies=[];
//获取构造函数所需的参数
foreach($constructor->getParameters()as$dependency){
if(isset($this->context[$class])&&isset($this->context[$class][$dependency->getName()])){
//先从上下文中查找
$dependencies[]=$this->context[$class][$dependency->getName()];
continue;
}
if(isset($parameters[$dependency->getName()])){
//从自定义参数中查找
$dependencies[]=$parameters[$dependency->getName()];
continue;
}
if(is_null($dependency->getClass())){
//参数类型不是类或接口时,无法从容器中获取依赖
if($dependency->isDefaultValueAvailable()){
//查找默认值,如果有就使用默认值
$dependencies[]=$dependency->getDefaultValue();
}else{
//无法提供类所依赖的参数
thrownewException('找不到依赖参数:'.$dependency->getName());
}
}else{
//参数类型是一个类时,就用make方法构建该类
$dependencies[]=$this->make($dependency->getClass()->name);
}
}
return$reflector->newInstanceArgs($dependencies);
}
//绑定上下文
publicfunctionaddContextualBinding($when,$needs,$give)
{
$this->context[$when][$needs]=$give;
}
//支持链式方式绑定上下文
publicfunctionwhen($when)
{
returnnewContext($when,$this);
}
}
classContext
{
protected$when;
protected$needs;
protected$container;
publicfunction__construct($when,Container$container)
{
$this->when=$when;
$this->container=$container;
}
publicfunctionneeds($needs)
{
$this->needs=$needs;
return$this;
}
publicfunctiongive($give)
{
//调用容器绑定依赖上下文
$this->container->addContextualBinding($this->when,$this->needs,$give);
}
}
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持毛票票。
声明:本文内容来源于网络,版权归原作者所有,内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:czq8825#qq.com(发邮件时,请将#更换为@)进行举报,并提供相关证据,一经查实,本站将立刻删除涉嫌侵权内容。