laravel请求处理流程
详解如何实现Laravel的服务容器的方法示例1. 容器的本质
- 服务容器本身就是一个数组,键名就是服务名,值就是服务。
- 服务可以是一个原始值,也可以是一个对象,可以说是任意数据。
- 服务名可以是自定义名,也可以是对象的类名,也可以是接口名。
|
// 服务容器 $container = [ // 原始值 'text' => '这是一个字符串' , // 自定义服务名 'customName' => new StdClass(), // 使用类名作为服务名 'StdClass' => new StdClass(), // 使用接口名作为服务名 'Namespace\\StdClassInterface' => new StdClass(), ]; // ----------- ↓↓↓↓示例代码↓↓↓↓ ----------- // // 绑定服务到容器 $container [ 'standard' ] = new StdClass(); // 获取服务 $standard = $container [ 'standard' ]; var_dump( $standard ); |
2. 封装成类
为了方便维护,我们把上面的数组封装到类里面。
$instances还是上面的容器数组。我们增加两个方法,instance用来绑定服务,get用来从容器中获取服务。
|
class BaseContainer { // 已绑定的服务 protected $instances = []; // 绑定服务 public function instance( $name , $instance ) { $this ->instances[ $name ] = $instance ; } // 获取服务 public function get( $name ) { return isset( $this ->instances[ $name ]) ? $this ->instances[ $name ] : null; } } // ----------- ↓↓↓↓示例代码↓↓↓↓ ----------- // $container = new BaseContainer(); // 绑定服务 $container ->instance( 'StdClass' , new StdClass()); // 获取服务 $stdClass = $container ->get( 'StdClass' ); var_dump( $stdClass ); |
3. 按需实例化
现在我们在绑定一个对象服务的时候,就必须要先把类实例化,如果绑定的服务没有被用到,那么类就会白白实例化,造成性能浪费。
为了解决这个问题,我们增加一个bind函数,它支持绑定一个回调函数,在回调函数中实例化类。这样一来,我们只有在使用服务时,才回调这个函数,这样就实现了按需实例化。
这时候,我们获取服务时,就不只是从数组中拿到服务并返回了,还需要判断如果是回调函数,就要执行回调函数。所以我们把get方法的名字改成make。意思就是生产一个服务,这个服务可以是已绑定的服务,也可以是已绑定的回调函数,也可以是一个类名,如果是类名,我们就直接实例化该类并返回。
然后,我们增加一个新数组$bindings,用来存储绑定的回调函数。然后我们把bind方法改一下,判断下$instance如果是一个回调函数,就放到$bindings数组,否则就用make方法实例化类。
|
class DeferContainer extend BaseContainer { // 已绑定的回调函数 protected $bindings = []; // 绑定服务 public function bind( $name , $instance ) { if ( $instance instanceof Closure) { // 如果$instance是一个回调函数,就绑定到bindings。 $this ->bindings[ $name ] = $instance ; } else { // 调用make方法,创建实例 $this ->instances[ $name ] = $this ->make( $name ); } } // 获取服务 public function make( $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 = new DeferContainer(); // 绑定服务 $container ->bind( 'StdClass' , function () { echo "我被执行了\n" ; return new StdClass(); }); // 获取服务 $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的时候就会直接返回,而不会再次执行回调函数。这样就实现了单例的绑定。
|
class SingletonContainer extends DeferContainer { // 绑定服务 public function bind( $name , $instance , $shared = false) { if ( $instance instanceof Closure) { // 如果$instance是一个回调函数,就绑定到bindings。 $this ->bindings[ $name ] = [ 'callback' => $instance , // 标记是否单例 'shared' => $shared ]; } else { // 调用make方法,创建实例 $this ->instances[ $name ] = $this ->make( $name ); } } // 绑定一个单例 public function singleton( $name , $instance ) { $this ->bind( $name , $instance , true); } // 获取服务 public function make( $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 = new SingletonContainer(); // 绑定服务 $container ->singleton( 'anonymous' , function () { return new class { public function __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方法来构建,因为它支持自动注入。
|
class InjectionContainer extends SingletonContainer { // 获取服务 public function make( $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 ; } // 构建一个类,并自动注入服务 public function build( $class ) { $reflector = new ReflectionClass( $class ); $constructor = $reflector ->getConstructor(); if ( is_null ( $constructor )) { // 没有构造函数,直接new return new $class (); } $dependencies = []; // 获取构造函数所需的参数 foreach ( $constructor ->getParameters() as $dependency ) { if ( is_null ( $dependency ->getClass())) { // 参数类型不是类时,无法从容器中获取依赖 if ( $dependency ->isDefaultValueAvailable()) { // 查找参数的默认值,如果有就使用默认值 $dependencies [] = $dependency ->getDefaultValue(); } else { // 无法提供类所依赖的参数 throw new Exception( '找不到依赖参数:' . $dependency ->getName()); } } else { // 参数类型是类时,就用make方法构建该类 $dependencies [] = $this ->make( $dependency ->getClass()->name); } } return $reflector ->newInstanceArgs( $dependencies ); } } // ----------- ↓↓↓↓示例代码↓↓↓↓ ----------- // class Redis { } class Cache { protected $redis ; // 构造函数中依赖Redis服务 public function __construct(Redis $redis ) { $this ->redis = $redis ; } } $container = new InjectionContainer(); // 绑定Redis服务 $container ->singleton(Redis:: class , function () { return new Redis(); }); // 构建Cache类 $cache = $container ->make(Cache:: class ); var_dump( $cache ); |
6. 自定义依赖参数
现在有个问题,如果类依赖的参数不是类或接口,只是一个普通变量,这时候就无法从容器中获取依赖参数了,也就无法实例化类了。
那么接下来我们就支持一个新功能,在调用make方法时,支持传第二个参数$parameters,这是一个数组,无法从容器中获取的依赖,就从这个数组中找。
当然,make方法是用不到这个参数的,因为它不负责实例化类,它直接传给build方法。在build方法寻找依赖的参数时,就先从$parameters中找。这样就实现了自定义依赖参数。
需要注意的一点是,build方法是按照参数的名字来找依赖的,所以parameters中的键名也必须跟__construct中参数名一致。
|
class ParametersContainer extends InjectionContainer { // 获取服务 public function make( $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 ; } // 构建一个类,并自动注入服务 public function build( $class , array $parameters = []) { $reflector = new ReflectionClass( $class ); $constructor = $reflector ->getConstructor(); if ( is_null ( $constructor )) { // 没有构造函数,直接new return new $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 { // 无法提供类所依赖的参数 throw new Exception( '找不到依赖参数:' . $dependency ->getName()); } } else { // 参数类型是类时,就用make方法构建该类 $dependencies [] = $this ->make( $dependency ->getClass()->name); } } return $reflector ->newInstanceArgs( $dependencies ); } } // ----------- ↓↓↓↓示例代码↓↓↓↓ ----------- // class Redis { } class Cache { protected $redis ; protected $name ; protected $default ; // 构造函数中依赖Redis服务和name参数,name的类型不是类,无法从容器中查找 public function __construct(Redis $redis , $name , $default = '默认值' ) { $this ->redis = $redis ; $this ->name = $name ; $this -> default = $default ; } } $container = new ParametersContainer(); // 绑定Redis服务 $container ->singleton(Redis:: class , function () { return new Redis(); }); // 构建Cache类 $cache = $container ->make(Cache:: class , [ 'name' => 'test' ]); var_dump( $cache ); |
提示:实际上,Laravel容器的build方法并没有第二个参数$parameters,它是用类属性来维护自定义参数。原理都是一样的,只是实现方式不一样。这里为了方便理解,不引入过多概念。
7. 服务别名
别名可以理解成小名、外号。服务别名就是给已绑定的服务设置一些外号,使我们通过外号也能找到该服务。
这个就比较简单了,我们增加一个新的数组$aliases,用来存储别名。再增加一个方法alias,用来让外部注册别名。
唯一需要我们修改的地方,就是在make时,要先从$aliases中找到真实的服务名。
|
class AliasContainer extends ParametersContainer { // 服务别名 protected $aliases = []; // 给服务绑定一个别名 public function alias( $alias , $name ) { $this ->aliases[ $alias ] = $name ; } // 获取服务 public function make( $name , array $parameters = []) { // 先用别名查找真实服务名 $name = isset( $this ->aliases[ $name ]) ? $this ->aliases[ $name ] : $name ; return parent::make( $name , $parameters ); } } // ----------- ↓↓↓↓示例代码↓↓↓↓ ----------- // $container = new AliasContainer(); // 绑定服务 $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' , new Log()); // 对已绑定的服务再次包装 $container ->extend( 'log' , function (Log $log ){ // 返回了一个新服务 return new RedisLog( $log ); }); |
现在我们看它是如何实现的。增加一个$extenders数组,用来存放扩展器。再增加一个extend方法,用来注册扩展器。
然后在make方法返回$instance之前,按顺序依次调用之前注册的扩展器。
|
class ExtendContainer extends AliasContainer { // 存放扩展器的数组 protected $extenders = []; // 给服务绑定扩展器 public function extend( $name , $extender ) { if (isset( $this ->instances[ $name ])) { // 已经实例化的服务,直接调用扩展器 $this ->instances[ $name ] = $extender ( $this ->instances[ $name ]); } else { $this ->extenders[ $name ][] = $extender ; } } // 获取服务 public function make( $name , array $parameters = []) { $instance = parent::make( $name , $parameters ); if (isset( $this ->extenders[ $name ])) { // 调用扩展器 foreach ( $this ->extenders[ $name ] as $extender ) { $instance = $extender ( $instance ); } } return $instance ; } } // ----------- ↓↓↓↓示例代码↓↓↓↓ ----------- // class Redis { public $name ; public function __construct( $name = 'default' ) { $this ->name = $name ; } public function setName( $name ) { $this ->name = $name ; } } $container = new ExtendContainer(); // 绑定Redis服务 $container ->singleton(Redis:: class , function () { return new Redis(); }); // 给Redis服务绑定一个扩展器 $container ->extend(Redis:: class , function (Redis $redis ) { $redis ->setName( '扩展器' ); return $redis ; }); $redis = $container ->make(Redis:: class ); var_dump( $redis ->name); |
9. 上下文绑定
有时侯我们可能有两个类使用同一个接口,但希望在每个类中注入不同的实现,例如两个控制器,分别为它们注入不同的Log服务。
|
class ApiController { public function __construct(Log $log ) { } } class WebController { public function __construct(Log $log ) { } } |
最终我们要用以下方式实现:
|
// 当ApiController依赖Log时,给它一个RedisLog $container ->addContextualBinding( 'ApiController' , 'Log' , new RedisLog()); // 当WebController依赖Log时,给它一个FileLog $container ->addContextualBinding( 'WebController' , 'Log' , new FileLog()); |
为了更直观更方便更语义化的使用,我们把这个过程改成链式操作:
|
$container ->when( 'ApiController' ) ->needs( 'Log' ) ->give( new RedisLog()); |
我们增加一个$context数组,用来存储上下文。同时增加一个addContextualBinding方法,用来注册上下文绑定。以ApiController为例,$context的真实模样是:
|
$context [ 'ApiController' ][ 'Log' ] = new RedisLog(); |
然后build方法实例化类时,先从上下文中查找依赖参数,就实现了上下文绑定。
接下来,看看链式操作是如何实现的。
首先定义一个类Context,这个类有两个方法,needs和give。
然后在容器中,增加一个when方法,它返回一个Context对象。在Context对象的give方法中,我们已经具备了注册上下文所需要的所有参数,所以就可以在give方法中调用addContextualBinding来注册上下文了。
|
class ContextContainer extends ExtendContainer { // 依赖上下文 protected $context = []; // 构建一个类,并自动注入服务 public function build( $class , array $parameters = []) { $reflector = new ReflectionClass( $class ); $constructor = $reflector ->getConstructor(); if ( is_null ( $constructor )) { // 没有构造函数,直接new return new $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 { // 无法提供类所依赖的参数 throw new Exception( '找不到依赖参数:' . $dependency ->getName()); } } else { // 参数类型是一个类时,就用make方法构建该类 $dependencies [] = $this ->make( $dependency ->getClass()->name); } } return $reflector ->newInstanceArgs( $dependencies ); } // 绑定上下文 public function addContextualBinding( $when , $needs , $give ) { $this ->context[ $when ][ $needs ] = $give ; } // 支持链式方式绑定上下文 public function when( $when ) { return new Context( $when , $this ); } } class Context { protected $when ; protected $needs ; protected $container ; public function __construct( $when , ContextContainer $container ) { $this ->when = $when ; $this ->container = $container ; } public function needs( $needs ) { $this ->needs = $needs ; return $this ; } public function give( $give ) { // 调用容器绑定依赖上下文 $this ->container->addContextualBinding( $this ->when, $this ->needs, $give ); } } // ----------- ↓↓↓↓示例代码↓↓↓↓ ----------- // class Dog { public $name ; public function __construct( $name ) { $this ->name = $name ; } } class Cat { public $name ; public function __construct( $name ) { $this ->name = $name ; } } $container = new ContextContainer(); // 给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. 完整代码
|
class Container { // 已绑定的服务 protected $instances = []; // 已绑定的回调函数 protected $bindings = []; // 服务别名 protected $aliases = []; // 存放扩展器的数组 protected $extenders = []; // 依赖上下文 protected $context = []; // 绑定服务实例 public function instance( $name , $instance ) { $this ->instances[ $name ] = $instance ; } // 绑定服务 public function bind( $name , $instance , $shared = false) { if ( $instance instanceof Closure) { // 如果$instance是一个回调函数,就绑定到bindings。 $this ->bindings[ $name ] = [ 'callback' => $instance , // 标记是否单例 'shared' => $shared ]; } else { // 调用make方法,创建实例 $this ->instances[ $name ] = $this ->make( $name ); } } // 绑定一个单例 public function singleton( $name , $instance ) { $this ->bind( $name , $instance , true); } // 给服务绑定一个别名 public function alias( $alias , $name ) { $this ->aliases[ $alias ] = $name ; } // 给服务绑定扩展器 public function extend( $name , $extender ) { if (isset( $this ->instances[ $name ])) { // 已经实例化的服务,直接调用扩展器 $this ->instances[ $name ] = $extender ( $this ->instances[ $name ]); } else { $this ->extenders[ $name ][] = $extender ; } } // 获取服务 public function make( $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 ; } // 构建一个类,并自动注入服务 public function build( $class , array $parameters = []) { $reflector = new ReflectionClass( $class ); $constructor = $reflector ->getConstructor(); if ( is_null ( $constructor )) { // 没有构造函数,直接new return new $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 { // 无法提供类所依赖的参数 throw new Exception( '找不到依赖参数:' . $dependency ->getName()); } } else { // 参数类型是一个类时,就用make方法构建该类 $dependencies [] = $this ->make( $dependency ->getClass()->name); } } return $reflector ->newInstanceArgs( $dependencies ); } // 绑定上下文 public function addContextualBinding( $when , $needs , $give ) { $this ->context[ $when ][ $needs ] = $give ; } // 支持链式方式绑定上下文 public function when( $when ) { return new Context( $when , $this ); } } class Context { protected $when ; protected $needs ; protected $container ; public function __construct( $when , Container $container ) { $this ->when = $when ; $this ->container = $container ; } public function needs( $needs ) { $this ->needs = $needs ; return $this ; } public function give( $give ) { // 调用容器绑定依赖上下文 $this ->container->addContextualBinding( $this ->when, $this ->needs, $give ); } } |
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持开心学习网。
原文链接:https://segmentfault.com/a/1190000018864154