0

0

yii2 随笔(七)依赖注入——(3)yii2的依赖注入

黄舟

黄舟

发布时间:2017-01-17 10:36:51

|

1219人浏览过

|

来源于php中文网

原创


yii2的依赖注入的核心代码在 yii\di,在这个包(文件夹)下面有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 把 yii\di\Container 的实现拿给自己使用。接下来,我们讨论一下容器是怎么实现的?

接着上面的 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)!

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

通义千问
通义千问

阿里巴巴推出的全能AI助手

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
微信网页版文件传输助手教程合集
微信网页版文件传输助手教程合集

本专题整合了微信网页版文件传输助手教程、入口等等内容,阅读专题下面的文章了解更多详细内容。

15

2026.02.04

微信文件过期恢复教程
微信文件过期恢复教程

本专题整合了微信文件过期恢复方法、技巧教程,阅读专题下面的文章了解更多详细内容。

9

2026.02.04

抖音网页版入口与视频观看指南 抖音官网视频在线访问
抖音网页版入口与视频观看指南 抖音官网视频在线访问

本专题汇总了抖音网页版的入口链接、官方登录页面以及视频观看入口,帮助用户快速访问抖音网页版,提供免登录访问方式和直接进入视频播放页面的方法,确保顺利浏览和观看抖音视频。

90

2026.02.04

学习通网页版入口与在线学习指南 学习通官网登录与使用方法
学习通网页版入口与在线学习指南 学习通官网登录与使用方法

本专题详细汇总了学习通网页版入口与登录方法,提供学习通官方网页端入口、学生登录平台、网页版使用指南等内容,帮助用户快速稳定地登录学习通官网,顺利进入学习平台,提升学习效率和体验。

16

2026.02.04

Python Web 框架 Django 深度开发
Python Web 框架 Django 深度开发

本专题系统讲解 Python Django 框架的核心功能与进阶开发技巧,包括 Django 项目结构、数据库模型与迁移、视图与模板渲染、表单与认证管理、RESTful API 开发、Django 中间件与缓存优化、部署与性能调优。通过实战案例,帮助学习者掌握 使用 Django 快速构建功能全面的 Web 应用与全栈开发能力。

13

2026.02.04

Java 流式处理与 Apache Kafka 实战
Java 流式处理与 Apache Kafka 实战

本专题专注讲解 Java 在流式数据处理与消息队列系统中的应用,系统讲解 Apache Kafka 的基础概念、生产者与消费者模型、Kafka Streams 与 KSQL 流式处理框架、实时数据分析与监控,结合实际业务场景,帮助开发者构建 高吞吐量、低延迟的实时数据流管道,实现高效的数据流转与处理。

6

2026.02.04

Golang 容器化与 Docker 实战
Golang 容器化与 Docker 实战

本专题深入讲解 Golang 应用的容器化与 Docker 部署,涵盖 Docker 基础概念、容器构建与镜像管理、Go 应用的 Dockerfile 编写、跨平台容器部署与优化、Docker Compose 和 Kubernetes 部署工具。通过实际案例,帮助学习者掌握 如何将 Golang 应用容器化并实现高效部署与管理,提升系统的可扩展性与运维效率。

7

2026.02.04

全国统一发票查询平台入口合集
全国统一发票查询平台入口合集

本专题整合了全国统一发票查询入口地址合集,阅读专题下面的文章了解更多详细入口。

66

2026.02.03

短剧入口地址汇总
短剧入口地址汇总

本专题整合了短剧app推荐平台,阅读专题下面的文章了解更多详细入口。

161

2026.02.03

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Yii2中文手册
Yii2中文手册

共0课时 | 0人学习

thinkphp基础介绍和yii2基础介绍
thinkphp基础介绍和yii2基础介绍

共10课时 | 2.3万人学习

Yii2框架基础视频教程
Yii2框架基础视频教程

共22课时 | 2.2万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号