yii2的依賴注入的核心程式碼在yiidi,在這個包(資料夾)下面有3個文件,分別是Container.php(容器),Instance.php(實例),ServiceLocator(服務定位器),現在我們討論前兩個,服務定位器可以理解一個服務的註冊表,這個不影響我們討論依賴注入,它也是依賴注入的一種應用。
我們還是從程式碼開始講解yii2是怎麼使用依賴注入的。
// yii\base\application //这个是yii2的依赖注入使用入口,参数的解释请参考源码,这里不多解释 public static function createObject($type, array $params = []) { if (is_string($type)) {//type 是字符串的话,它就把type当做一个对象的“原材料”,直接把它传给容器并通过容器得到想要的对象。 return static::$container->get($type, $params); } elseif (is_array($type) && isset($type['class'])) { //type 是数组,并且有class的键,经过简单处理后,得到对象的“原材料”,然后把得到的“原材料”传给容器并通过容器得到想要的对象。 $class = $type['class']; unset($type['class']); return static::$container->get($class, $params, $type); } elseif (is_callable($type, true)) {//如果type是可调用的结构,就直接调用 return call_user_func($type, $params); } elseif (is_array($type)) {//如果type是array,并且没有'class'的键值,那么就抛出异常 throw new InvalidConfigException('Object configuration must be an array containing a "class" element.'); } else {//其他情况,均抛出另一个异常,说type不支持的配置类型 throw new InvalidConfigException("Unsupported configuration type: " . gettype($type)); } }
透過閱讀上面程式碼,Yii::createObject()是把合格的“原材料”,交給“容器($container)”,來產生目標物件的,那麼容器就是我們“依賴注入”生產物件的地方。那麼$container是什麼時候引入的呢(注意這裡用的是 static::$container, 而不是 self::$container)?還記得在首頁導入yii框架時的語句麼?
//导入yii框架 require(__DIR__ . '/../vendor/yiisoft/yii2/Yii.php');
程式碼如下
//引入基本的yii框架 require(__DIR__ . '/BaseYii.php'); //只是做了继承,这里给我们留了二次开发的余地,虽然很少能用到 class Yii extends \yii\BaseYii { } //设置自动加载 spl_autoload_register(['Yii', 'autoload'], true, true); //注册 classMap Yii::$classMap = require(__DIR__ . '/classes.php'); //注册容器 Yii::$container = new yii\di\Container();
你看的沒錯!就是最後一句話,yii2 把 yiidiContainer 的實作拿給自己使用。接下來,我們討論容器是怎麼實現的?
接著上面的static::$container->get() 的方法,在講解get方法之前,我們要先了解容器的幾個屬性,這將有助於理解get的實作
$_singletons; // 单例数组,它的键值是类的名字,如果生成的对象是单例,则把他保存到这个数组里,值为null的话,表示它还没有被实例化 $_definitions;// 定义数组,它的键值是类的名字,值是生成这个类所需的“原材料”,在set 或 setSingleton的时候写入 $_params; // 参数,它的键值是类的名字,值是生成这个类所需的额外的“原材料”,在set 或 setSingleton的时候写入 $_reflections; //反射,它的键值是类的名字,值是要生成的对象的反射句柄,在生成对象的时候写入 $_dependencies;//依赖,它的键值是类的名字,值是要生成对象前的一些必备“原材料”,在生成对象的时候,通过反射函数得到。
ok,如果你夠細心地話,理解了上面的幾個屬性,估計你就對yii2的容器有個大概的了解了,這裡還是從get開始。
public function get($class, $params = [], $config = []) { if (isset($this->_singletons[$class])) {//查看将要生成的对象是否在单例里,如果是,则直接返回 // singleton return $this->_singletons[$class]; } elseif (!isset($this->_definitions[$class])) {//如果没有要生成类的定义,则直接生成,yii2自身大部分走的是这部分,并没有事先在容器里注册什么, 那么配置文件是在哪里注册呢?还记的文章最开始的时候的"服务定位器"么?我们在服务定位器里讲看到这些。 return $this->build($class, $params, $config); } //如果已经定义了这个类,则取出这个类的定义 $definition = $this->_definitions[$class]; if (is_callable($definition, true)) {//如果定义是可调用的结构 //先整合一下参数,和$_params里是否有这个类的参数,如果有则和传入的参数以传入覆盖定义的方式整和在一起 //然后再检查整合后的参数是否符合依赖,就是说是否有必填的参数,如果有直接抛出异常,否则返回参数。检查依赖的时候,需要判断是否为实例(Instance),如果是, 则要实现实例。注意:这里出现了Instance。 $params = $this->resolveDependencies($this->mergeParams($class, $params)); //把参数专递给可调用结果,返回结果 $object = call_user_func($definition, $this, $params, $config); } elseif (is_array($definition)) {//如果定义是一个数组 //把代表要生成的class取出 $concrete = $definition['class']; //注销这个键值 unset($definition['class']); //把定义 和 配置整合成新的定义 $config = array_merge($definition, $config); //整合参数 $params = $this->mergeParams($class, $params); //如果传入的$class 和 定义里的class完全一样,则直接生成,build第一个参数确保为真实的类名,而传入的$type可能是别名 if ($concrete === $class) { $object = $this->build($class, $params, $config); } else {//如果是别名,则回调自己,生成对象,因为这时的类也有可能是别名 $object = $this->get($concrete, $params, $config); } } elseif (is_object($definition)) {//如果定义是一个对象,则代表这个类是个单例,保存到单例里,并返回这个单例,这里要自己动脑想一下, 为什么是个对象就是单例?只可意会不可言传,主要是我也组织不好语言怎么解释它。 return $this->_singletons[$class] = $definition; } else {//什么都不是则抛出异常 throw new InvalidConfigException("Unexpected object definition type: " . gettype($definition)); } //判断这个类的名字是否在单例里,如果在,则把生成的对象放到单例里 if (array_key_exists($class, $this->_singletons)) { // singleton $this->_singletons[$class] = $object; } //返回生成的对象 return $object; }
研究到這裡,我們發現get 函數只是個「入口」而已,主要的功能在build裡
//创建对象 protected function build($class, $params, $config) { //通过类名得到反射句柄,和依赖(依赖就是所需参数) //所以前面提到,传输buile的第一个参数必须为有效的“类名”否则,会直接报错 list ($reflection, $dependencies) = $this->getDependencies($class); //把依赖和参数配置,因为依赖可能有默认参数,这里覆盖默认参数 foreach ($params as $index => $param) { $dependencies[$index] = $param; } //确保依赖没问题,所有原材料是否都ok了,否则抛出异常 $dependencies = $this->resolveDependencies($dependencies, $reflection); if (empty($config)) {//如果config为空,则返回目标对象 return $reflection->newInstanceArgs($dependencies); } if (!empty($dependencies) && $reflection->implementsInterface('yii\base\Configurable')) {//如果目标对象是 Configurable的接口 // set $config as the last parameter (existing one will be overwritten) $dependencies[count($dependencies) - 1] = $config; return $reflection->newInstanceArgs($dependencies); } else {//其他的情况下 $object = $reflection->newInstanceArgs($dependencies); foreach ($config as $name => $value) { $object->$name = $value; } return $object; } }
好了,build到這裡就結束了,下面我們一起看看容器是怎麼得到反射句柄和依賴關係的
protected function getDependencies($class) { if (isset($this->_reflections[$class])) {//是否已经解析过目标对象了 return [$this->_reflections[$class], $this->_dependencies[$class]]; } $dependencies = [];//初始化依赖数组 $reflection = new ReflectionClass($class);//得到目标对象的反射,请参考php手册 $constructor = $reflection->getConstructor();//得到目标对象的构造函数 if ($constructor !== null) {//如果目标对象有构造函数,则说明他有依赖 //解析所有的参数,注意得到参数的顺序是从左到右的,确保依赖时也是按照这个顺序执行 foreach ($constructor->getParameters() as $param) { if ($param->isDefaultValueAvailable()) {//如果参数的默认值可用 $dependencies[] = $param->getDefaultValue();//把默认值放到依赖里 } else {//如果是其他的 $c = $param->getClass();//得到参数的类型,如果参数的类型不是某类,是基本类型的话,则返回null //如果,是基本类型,则生成null的实例,如果不是基本类型,则生成该类名的实例。 注意:这里用到了实例(Instance) $dependencies[] = Instance::of($c === null ? null : $c->getName()); } } } //把引用保存起来,以便下次直接使用 $this->_reflections[$class] = $reflection; //把依赖存起来,以便下次直接使用 $this->_dependencies[$class] = $dependencies; //返回结果 return [$reflection, $dependencies]; }
下面我們來看看容器是怎麼確保依賴關係的
protected function resolveDependencies($dependencies, $reflection = null) { //拿到依赖关系 foreach ($dependencies as $index => $dependency) { //如果依赖是一个实例,因为经过处理的依赖,都是Instance的对象 if ($dependency instanceof Instance) { if ($dependency->id !== null) {//这个实例有id,则通过这个id生成这个对象,并且代替原来的参数 $dependencies[$index] = $this->get($dependency->id); } elseif ($reflection !== null) {//如果反射句柄不为空,注意这个函数是protected 类型的, 所以只有本类或者本类的衍生类可访问,但是本类里只有两个地方用到了,一个是 get 的时候, 如果目标对象是可调用的结果(is_callable),那么$reflection===null,另外一个build的时候, $reflection不为空,这个时候代表目标对象有一个必须参数,但是还不是一个实例(Instance的对象), 这个时候代表缺乏必须的“原材料”抛出异常 //则拿到响应的必填参数名字,并且抛出异常 $name = $reflection->getConstructor()->getParameters()[$index]->getName(); $class = $reflection->getName(); throw new InvalidConfigException("Missing required parameter \"$name\" when instantiating \"$class\"."); } } } //确保了所有的依赖后,返回所有依赖,如果目标是is_callable($definition, true),则不会抛出异常,仅仅把Instance类型的参数实例化出来。 return $dependencies; }
看到這裡,我們就可以了解了yii2是怎麼使用容器實現「依賴注入」了,那麼有個問題,閉包的依賴怎麼保證呢?我想是因為yii2認為閉包的存在解決的是局限性的問題,不存在依賴性,或者依賴是交給開發者自行解決的。另外yii2的容器,如果參數是閉包的話,就會出現錯誤,因為對閉包的依賴,解析閉包參數的時候,會得到$dependencies[]
= Instance::of($c === null ? null : $c->getName());得到的就是一個Closure 的實例,而後面實例化這個實例的時候,就會出現問題了,所以用yii2的容器實現物件的時候,被實現的物件不能包含閉包參數,如果有閉包參數,則一定要有預設值,或者人為保證會傳入這個閉包參數,繞過自動產生的語句。
ok容器的主要函數就有這些了,其他方法,set,setSingleton,has,hasSingleton,clear一看就知道什麼意思,另外這些方法基本上沒有在框架中使用(可以在這些函數寫exit,看看你的頁面會不會空白),或者你用容器自己產生一些東西的話,可以自行查看這些函數的用法。
最後,我們來看看Instance到底扮演了什麼角色
//yii\di\Instance //很诧异吧,就是实例化一个自己,注意这个自己是 static,以后你可能需要用到这个地方 public static function of($id) { return new static($id); } [/php] 那么这个函数的构造函数呢? [php] //禁止外部实例化 protected function __construct($id) { //赋值id $this->id = $id; }
在容器中,就用到了Instance的這兩個方法,說明Instance在實例中,只是確保了依賴的可用性。此外Instance也提供了其他的函數,其中get 得到的是當前Instance所對應的id的實例化對象,另外,還有一個靜態函數ensure
//确保 $reference 是 $type类型的,如果不是则抛出异常 //在框架中多次用到,请自行查找 //另外,如果$type==null的时候,他也可以当做依赖注入的入口,使用方法请自行查看源码,到现在你应该可以自己看懂这些代码了。 public static function ensure($reference, $type = null, $container = null) { //... }
以上就是yii2 隨筆(七)依賴注入-(3) yii2的依賴注入的內容,更多相關內容請關注PHP中文網(www.php.cn)!