扩展配置

前文介绍了配置文件的解析,接下来介绍配置项是如何生效的。在实际的项目开发中,我们可能会用到各种扩展,如 date、mbstring、mysqli 等,这些扩展其实也有着属于自己的配置项,例如,php.ini 中的 date section 部分就是 date 模块的配置:

[Date]
;date.timezone = Asia/Shanghai
;date.default_latitude = 31.7667
;date.default_longitude = 35.2333
;date.sunrise_zenith = 90.583333
;date.sunset_zenith = 90.583333

在配置文件的解析过程中,会把配置文件中的 PHP 扩展和 Zend 扩展解析出来放在名为 extension_lists 的结构中以供后续加载。

typedef struct _php_extension_lists {
    zend_llist engine;    // 保存PHP extension
    zend_llist functions; // 保存Zend extension
} php_extension_lists;

在配置文件解析完成以后,开始加载 PHP 核心默认配置项(display_errors、post_max_size 等)和 Zend 默认项(error_reporting、zend.enable_gc 等)。其解析的结果保存在名为 registered_zend_ini_directives 的全局 hashtable 中,EG(ini_directives) 也指向该 hashtable。核心配置的解析和扩展配置的解析非常类似,我们将在后面展开介绍。在核心的默认配置解析完成后,开始加载静态编译的扩展和保存到 extension_lists 中的动态扩展。接着依次启动各个扩展:

/* Register PHP core ini entries */
REGISTER_INI_ENTRIES();

/* Register Zend ini entries */
zend_register_standard_ini_entries();

/* startup extensions statically compiled in /
php_register_internal_extensions_func();

/* start additional PHP extensions */
php_register_extensions_bc(additional_modules, num_additional_modules);

php_ini_register_extensions();

核心配置以及其他扩展的默认配置项都是硬编码在对应的源代码中。PHP 扩展通过 PHP_INI_BEGIN 和 PHP_INI_END 宏来定义当前模块的配置(Zend 扩展通过 ZEND_INI_BEGIN 和 ZEND_INI_END 来定义)。我们以 date 扩展来说明扩展配置是怎么解析的。

date 扩展配置的定义在 ext/date/php_date.c中,如下所示:

PHP_INI_BEGIN()
    STD_PHP_INI_ENTRY("date.timezone",  "",  PHP_INI_ALL,  OnUpdate_date_timezone,
        default_timezone, zend_date_globals, date_globals)
    PHP_INI_ENTRY("date.default_latitude",  DATE_DEFAULT_LATITUDE,  PHP_INI_ALL,
        NULL)
    PHP_INI_ENTRY("date.default_longitude",  DATE_DEFAULT_LONGITUDE,  PHP_INI_ALL,
        NULL)
    PHP_INI_ENTRY("date.sunset_zenith", DATE_SUNSET_ZENITH, PHP_INI_ALL, NULL)
    PHP_INI_ENTRY("date.sunrise_zenith", DATE_SUNRISE_ZENITH, PHP_INI_ALL, NULL)
PHP_INI_END()

可以看出 date 扩展一共注册了 5 个配置项。上边的宏展开后其实只是把包围在 PHP_INI_BEGIN 和 PHP_INI_END 之间的配置项组成一个名为 ini_entries 的 zend_ini_entry_def 结构的数组。配置项有以下几种注册方式:

  • ZEND_INI_ENTRY (name, default_value, modifiable, on_modify);

  • ZEND_INI_ENTRY_EX (name, default_value, modifiable, on_modify,NULL);

  • STD_ZEND_INI_ENTRY (name, default_value, modifiable, on_modify,property_name, struct_type, struct_ptr);

  • STD_ZEND_INI_BOOLEAN (name, default_value, modifiable, on_modify,property_name, struct_type, struct_ptr);

  • STD_ZEND_INI_ENTRY_EX (name, default_value, modifiable, on_modify,property_name, struct_type, struct_ptr, displayer);

可能读者会感到疑惑:上文的定义明明是 PHP_INI 开头,这里怎么变成了 ZEND_INI 呢?其实只是一种名称替换:

#define PHP_INI_ENTRY       ZEND_INI_ENTRY

所有的配置宏展开后,最终如下所示:

{ name,  on_modify,  arg1,  arg2,  arg3,  default_value,  displayer,  modifiable, sizeof(name)-1, sizeof(default_value)-1 }

每一个默认配置项实质上是 _zend_ini_entry_def 结构:

typedef struct _zend_ini_entry_def {
    const char *name; // 配置项的名称
    ZEND_INI_MH((*on_modify)); // 有这个配置后的回调函数
    void *mh_arg1; // 配置项在结构体中的偏移量
    void *mh_arg2; // 要映射到结构体的指针
    void *mh_arg3;
    const char *value; // 配置项默认值
    void (*displayer)(zend_ini_entry *ini_entry, int type);
    int modifiable; // 可修改级别
    uint name_length; // 配置项的名称长度
    uint value_length; // 配置项的值长度
} zend_ini_entry_def;

以 date 扩展的第一个配置项为例,实例化后的结构体为

typedef struct _zend_ini_entry_def {
    "date.timezone",
    OnUpdate_date_timezone,
    (void *) XtOffsetOf(zend_date_globals, default_timezone),
    (void *) &date_globals,
    NULL,
    "",
    NULL,
    PHP_INI_ALL,
    sizeof("date.timezone")-1,
    sizeof("")-1
} zend_ini_entry_def;

配置文件中的 “date.timezone” 配置映射到 zend_date_globals 类型的变量 date_globals 中的 default_timezone。其默认值为空,可修改范围为 PHP_INI_ALL。当有自定义配置时,调用 OnUpdate_date_timezone 函数来处理。其他配置项类似。

不是所有的配置项都一定映射到某个结构中,有些配置项只会保存在 EG(ini_directives) 中。

PHP 扩展的启动是依次执行各个扩展中的 PHP_MINIT_FUNCTION,在每个扩展启动期间会执行 REGISTER_INI_ENTRIES。展开后如下:

/*
* Registration / unregistration
*/
ZEND_API int zend_register_ini_entries(const zend_ini_entry_def *ini_entry, int
    module_number) /* {{{ */
{
    // code
}

PHP_INI_BEGIN 和 PHP_INI_END 定义的配置项数组会保存到 ini_entries, ini_entries-zend_register_ini_entries 的参数 ini_entry 即为 ini_entries 的元素;ini_entrieszend_register_ini_entries 的第二个参数 module_number 为 module_registry 数组中的编号,module_registry 为所有已经加载的扩展数组。zend_register_ini_entries 会依次把扩展中的各个配置项添加到全局配置 registered_zend_ini_directives 中。添加后新的配置项变为如下结构:

struct _zend_ini_entry {
    zend_string *name;
    ZEND_INI_MH((*on_modify));
    void *mh_arg1;
    void *mh_arg2;
    void *mh_arg3;
    zend_string *value;
    zend_string *orig_value; // 初始配置
    void (*displayer)(zend_ini_entry *ini_entry, int type);
    int modifiable;

    int orig_modifiable;  // 初始配置级别
    int modified;         // 是否被修改
    int module_number;   // 配置所在的模块注册的编号
};

配置项添加到 registered_zend_ini_directives 的同时,根据配置项的 key 到 configuration_hash 中查找,如果能找到,说明有自定义的配置项,没有则继续使用默认值。最后调用该配置项的 on_modify 回调函数,使新的配置项生效。例如,date 扩展的 timezone 配置项,其 on_modify 回调函数如下:

static PHP_INI_MH(OnUpdate_date_timezone){}

这个函数会把自定义的时区配置复制到当前模块,同时做时区配置的合法性检查。不同的配置可以定义自己的回调函数来做个性化处理。在这里读者可能有疑问,为什么要把配置项复制到当前模块,直接从 registered_zend_ini_directives 中获取不行么?PHP 的扩展有一个当前模块级别的 global 结构体,可以保存当前扩展的一些全局信息(注意:不只是保存配置),例如:

ZEND_BEGIN_MODULE_GLOBALS(date)
    char                     *default_timezone;
    char                     *timezone;
    HashTable                *tzcache;
    timelib_error_container *last_errors;
    int                      timezone_valid;
ZEND_END_MODULE_GLOBALS(date)

这个宏展开即为

typedef struct _zend_date_globals {
    char                     *default_timezone;
    char                     *timezone;
    HashTable                *tzcache;
    timelib_error_container *last_errors;
    int                      timezone_valid;
} zend_date_globals;

模块在启动完毕后,相关的配置及模块内的全局信息会保存在这个结构中,方便模块内部处理。各个模块还可以通过宏 ZEND_MODULE_GLOBALS_ACCESSOR 来定义配置的快捷访问方式,例如:

#define DATEG(v) ZEND_MODULE_GLOBALS_ACCESSOR(date, v)

通过这样的方式,在之后的执行阶段,date扩展不需访问 registered_zend_ini_directives。扩展内通过 DATEG(v) 来访问自己的 date_globals,便可以拿到用户自定义的配置。图8-2为 date 模块配置解析示意图。

image 2024 06 10 12 11 16 491
Figure 1. 图8-2 date模块配置解析示意图

到这里,date 模块的配置解析过程就介绍完了,其他模块的解析过程类似,在这里不再一一展开,有兴趣的读者可以追踪一下其他模块的解析过程。