PHP Swoft2 框架精华系列:Bean 定义的实例化

Bean 定义的实例化

Definition 的定义,我们都知道,主要在两个位置,一个是各个组件的 Autoloader 中的 beans() 方法中。另一个是在 app/bean.php 文件中。也就是所谓的核心 bean 的定义。那这些 bean 是如何实例化的呢?相关的配置信息又是如何注入到 bean 实例中的呢?

核心 bean Definition 的解析

bean 的来源

核心 bean 的实例化,在启动过程中,属于 bean 进程对象实例处理的阶段。

php 复制代码
$handler     = new BeanHandler();
// 获取核心 bean 的定义
$definitions = $this->getDefinitions();
$parsers     = AnnotationRegister::getParsers();
$annotations = AnnotationRegister::getAnnotations();
// 将核心 bean 的定义给 BeanFactory(内部给了 Container)
BeanFactory::addDefinitions($definitions);
BeanFactory::addAnnotations($annotations);
BeanFactory::addParsers($parsers);
BeanFactory::setHandler($handler);
// BeanFactory 初始化过程,就是 Container 容器对以上来源不同的 bean 的实例化过程
BeanFactory::init();

$stats = BeanFactory::getStats();

从以上代码可以看到,bean 来源主要是两个:

  • 核心 bean 的定义
    • 组件中 Autoloader::beans 方法中的返回的 bean 定义(优先级低)
    • app/bean.php文件中的 bean 定义(优先级高)
  • 注解
    • 注解的解析器
    • 注解的内容
bean 定义的解析代码跟踪入口

我们从 BeanFatory::init()开始说起。这个bean 工厂的初始化,其实是内部容器 Container 的一个初始化过程。这个过程又分为了三个部分:

  • 注解的解析
  • bean 定义的解析
  • 初始化 bean

DefinitionObjParser->parseDefinitions() 进行解析。

可以参考一下几个关键函数:

  1. new ObjectDefinition($beanName, $className) 创建 bean 定义对象
  2. DefinitionObjParser->updateObjectDefinitionByDefinition 进一步设置 bean 定义对象中的构造参数注入信息、属性注入信息、
bean 定义的主要构成

通过 DefinitionObjParser->parseDefinition 函数可以得出一个 bean 的定义的主要组成:

  • class:用来实例化 bean 的类
  • 无键名的数组:用来配置 bean 的构造函数参数。
    • 属性值和构造函数参数值,都可以通过 ${xxx}这种形式 , 注入Bean和引用配置信息
      • ${dbTest}表示引用一个名为 dbTest 的 bean。
      • ${.config.db.name}表示引用主配置 base.php中的 db['name'] 或者 db.php 中的 name 键。
      • ${.db.name}等同于上方配置(config 会被自动去掉)
  • 键名:和绑定的 class 类的成员属性名称对应
  • __option 可选的注入信息
    • scope:表示当前 bean 实例的作用范围。
      • Bean::SINGLETON 单例 bean
      • Bean::PROTOTYPE 原型 bean
      • Bean::REQUEST 请求 bean
      • Bean::SESSION 会话 bean
    • alias:表示 bean 的别名,后续再取用的时候,通过 bean 名称或者别名都可以获取 bean。
php 复制代码
db => [
    'class'  => Database::class, // 对应 bean 实例化的类
    'dsn'    => 'mysql:dbname=dbname;host=127.0.0.1', // 对应 Database::class 中的 dsn 属性
    'config' => [	// 对应 Database::class 中的 config 属性
        // fetch array
        'fetchMode' => PDO::FETCH_ASSOC, // config 中的一个项
    ],
    [
        'arg1' => '${.arg1}', // 表示引用主配置文件信息 arg1 项
        'arg2' => '${.config.arg2}' // 表示引用主配置文件 arg2 项
    ]
    '__option' => [
        'aaa' => 'xxx'
    ]
]

以上只是一个示例,实际 Database 类并没有__construct函数,找了半天没有找到合适的示例,只能造个假的了,但是规则是一样的。

注意以上定义中,键名应该都用字符串类型的数据,否则可能会导致实例化后对应的参数注入失败。

配置信息的注入

bean 定义中的键值的部分(不管是属性,还是构造函数),如果是以美元符号开头的字符串${xxx},表示数据是一个引用值。

${.xxx}花括号中为点开始的数据,表示引用的配置。不是以点开始的表示引用的是一个 bean 实例。

如果不是以美元符号和花括号表示的引用值,则表示这个是一个固定值,会直接注入到对应的属性或者参数中。

Bean 的实例化

DefinitionObjParser 结构说明

bean 定义的解析之后,所有的 bean 的定义数据会存储在 DefinitionObjParser 中。

  • classNames:存储同一个类对应的不同的 bean(多个不同的bean ,可以是同一个类的实例化对象)。
  • objectDefinitions:存储每个 beanName 对应的全部的定义信息。
  • definitions:存储用户的 bean 的定义(未经解析过的)。
  • alias:关联数组,存储每个别名对应的原始的 beanName。

以上数据结构示例如下:

php 复制代码
// 假如以下配置为 bean.php 定义 beanName1 和 beanName2(Autoloader::beans() 无配置)
return [
    'beanName1' => [
        'class' => Test::class,
        '__option' => [
            'scope' => Bean::SINGLETON
        ]
    ],
    'beanName2' => [
        'class' => Test::class,
        '__option' => [
            'scope' => Bean::SINGLETON
        ]
    ]
];
// classNames 结构如下:
class DefinitionObjParser {
    // merge 后的定义数组(bean.php 和 Autoloader::beans() merge 的结果)
    protected $definitions = [
        'beanName1' => [
            'class' => Test::class,
            '__option' => [
                'scope' => Bean::SINGLETON
            ]
    	],
        'beanName2' => [
            'class' => Test::class,
            '__option' => [
                'scope' => Bean::SINGLETON
            ]
        ]
    ];
    // 记录每个定义中类关联的所有实例名称
	protected $classNames[
        Test::class => ['beanName1', 'beanName2'],
    ];
    // 记录 ObjectDefinition 对象集合
    protected $objectDefinitions = [
        // 不同的实例名称关联一个 ObjectDefinition 实例
        'beanName1' => new ObjectDefinition('beanName1', Test::class),
        'beanName2' => new ObjectDefinition('beanName2', Test::class),
    ];
}
Bean 实例化过程细节解析
definition 的合并

将 bean.php 和 Autoloader::beans() 获取到的 bean 配置进行合并。同一个 bean 实例名称的,合并为一个实例配置。

bean.php 配置的优先级要高于 Autoloader::beans() 中配置。

php 复制代码
// 实际的合并过程可以理解为如下:
$definitions = array_merge(Autoloader::beans(), require bean.php);
将用户定义的 definitions 转换成 ObjectDefinition

DefinitionObjParser->parseDefinitions()解析 bean 定义的两个重要函数,

  • createObjectDefinition

    当发现一个新的 bean 定义的时候会生成一个 ObjectDefinition 对象

  • resetObjectDefinition

    当同一个 bean 名字已经存在一个 ObjectDefinition 对象时候,要更新这个对象的结构

转成成 ObjectDefinition 后,这些集合数据存储在 DefinitionObjParser->objectDefinitions

注意:DefinitionObjParser->parseDefinitions() 后,其中的 classNames 结构被复制到了 Container 中。他们两个结构是一致的。

将 ObjectDefinition 转换成 bean 实例,存储到容器中

遍历上一步中DefinitionObjParser->objectDefinitions,将每个 bean 按照 scope 不同,分别放到不同的对象池中。

Container->newBean(beanName) 对每个 bean 进行实例化。

Container->setNewBean(string $beanName, string $scope, $object, string $id = '') 此函数按照生存期(scope)不同放到容器的不同的对象池中,如下:

  • singletonPool
  • prototypePool
  • requestPool
  • sessionPool

配置 Bean 定义常见问题

通过构造函数实例化的 bean

如果配置的是通过构造函数进行初始化的 bean,这个时候尤其小心,因为 bean 的最终定义是通过

array_merge('组件Autolaod中定义', 'bean.php 文件中定义') 进行合并的,且构造函数参数默认是 Definition 数组中的索引号为 0 的数组。多次配置后,经过merge 会生成多组构造函数参数。

如果在 Autolaoder.php beans 方法中配置过构造函数,但是 在 bean.php 中(或者其他任何能够配置bean定义的地方)再次配置了构造函数。这个时候,array_merge 会让第二次的配置,索引自动变为1,那么在实际这个定义进行实例化的时候,就会多出一组构造函数,无法与任何属性,对应上。进而抛出一个异常:

php 复制代码
InvalidArgumentException(code:0) Property key from definition must be string

所以,同一个bean实例,构造函数实例化的bean 不管在任何地方配置,构造数数组,只能配置一次

如果想要配置其他实例,更改 bean 名称与之前的 bean 名称不同即可,这样实例化后是两个 bean 示例,也不会影响。

单例 bean 和 prototype bean 配置差异

通过以上实例化 bean 的细节可知,不同的 beanName 生成的实例是不一样的,即使 class 是同一个。这个跟配置定义时候的生存期 scope 也无关。

通过不同 beanName 获取的实例是不同的。但是通过类全路径名称获取到的是,当前类关联的所有类实例中的最后一个。如下示例:

php 复制代码
// 通过 bean(Test::class) 获取到的是 bean2 对应的实例。
class Container {
    private $classNames = [
        Test::class => ['bean1', 'bean2']
    ]
        
    private $singletonPool = [
        'bean1' => new Object,
        'bean2' => new Object,
    ];

    private $prototypePool = [];
}

PHP Swoft2 开源框架系列教程专栏推荐

Swoft2 框架精华教程:Validator 校验器详解

Swoft 框架精华教程:Devtool 详解

Swoft2 框架精华教程:Controller 组件解析,使用说明

Swoft2 框架精华教程:Config 配置解析,使用说明

Swoft2 框架精华教程:CLog 使用篇

Swoft2 框架精华教程:数据库 Migration

Swoft2 框架精华教程:数据库操作

Swoft2 框架精华教程: Swoft 组件开发单元测试

Swoft2 框架精华教程:面向切面编程(Aspect)

Swoft2 框架精华教程:Annotation 注解机制详解

Swoft 框架精华教程:Bean 定义的实例化

相关推荐
小兔子酱#10 小时前
【Docker 07】Network - 网络
网络·docker·php
onejason13 小时前
如何利用 PHP 爬虫按关键字搜索 Amazon 商品
前端·后端·php
最美不过下雨天啊13 小时前
tp框架导出excel的时候报错:unexcepted identifier “Closure“,excepting variable
php·excel·thinkphp6
Q_Q196328847518 小时前
python大学校园旧物捐赠系统
开发语言·spring boot·python·django·flask·node.js·php
木子金光军2 天前
BeikeShop - 一个开源、用户友好的跨境电子商务平台
开源·php·laravel
喵叔哟2 天前
第11章:Neo4j实际应用案例
服务器·php·neo4j
万岳科技程序员小金2 天前
2025餐饮供应链趋势:一套系统源码如何打通食堂采购全流程?
开源·php·源码·食堂采购系统源码·供应链管理平台
杰_happy2 天前
PHP Swoft2 框架精华系列:Annotation 注解机制详解
php·swoft
kali-Myon2 天前
攻防世界[level7]-Web_php_wrong_nginx_config
前端·nginx·安全·php·web·ctf·攻防世界