声明和使用 INI 设置

本章详细介绍了 PHP 如何使用其配置,以及如何通过注册和使用 INI 设置将扩展挂入 PHP 的主要配置步骤。

INI 设置提醒

在继续之前,您必须记住 INI 设置和 PHP 配置在 PHP 中的工作方式。以下是再次提取为源代码解释的步骤。PHP INI 文件解析步骤发生在 php_init_config() 中,与 INI 相关的所有内容主要发生在 Zend/zend_ini.c 中。

首先,PHP 尝试解析一个或多个 INI 文件。这些文件可能会声明一些设置,这些设置将来可能会或可能不会相关。在这个非常早期的阶段(INI 文件解析),PHP 不知道这些文件中会有什么。它只是解析内容,并保存此内容以供以后使用。

然后作为第二步,PHP 启动其扩展,调用它们的 MINIT()。如果您需要记住 PHP 生命周期,请阅读专门的章节。MINIT() 现在可以注册当前感兴趣的扩展 INI 设置。在注册设置时,引擎会检查它是否之前解析过其定义,作为 INI 文件解析步骤的一部分。如果是这种情况,则 INI 设置将注册到引擎中,并获取从 INI 文件中解析的值。如果在解析的 INI 文件中没有定义,则将使用扩展设计者为 API 提供的默认值进行注册。

设置将获取的默认值是从 INI 文件解析中探测到的。如果没有找到,则默认值是扩展开发人员给出的,而不是相反。

我们在这里讨论的默认值称为 “主值(master value)”。您可能从 phpinfo() 输出中还记得它,对吧?

image 2024 07 22 00 30 53 782

主值不能改变。如果在请求期间,用户想要更改配置,例如使用 ini_set(),并且如果允许,则更改的值将是 “本地值(local value)”,即当前请求的当前值。引擎将在请求结束时自动将本地值恢复为主值,从而重置它并忘记请求实时更改。

ini_get() 读取当前请求绑定的本地值,而 get_cfg_var() 无论发生什么情况都会读取主值。

如果您理解正确,那么对于任何不属于 INI 文件解析的值,get_cfg_var() 将返回 false,即使该值存在且由扩展声明。反之亦然:如果要求提供扩展未声明感兴趣的设置,ini_get() 将返回 false,即使此类设置是 INI 文件解析的一部分(如 php.ini)。

关于 INI 设置

在引擎中,INI 设置由 zend_ini_entry 结构表示:

struct _zend_ini_entry {
    zend_string *name;
    int (*on_modify)(zend_ini_entry *entry, zend_string *new_value, void *mh_arg1, void *mh_arg2, void *mh_arg3,
                     int stage);
    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;
};

这种结构没有什么真正强大的功能。

  • 设置的名称(name)和值(value)是最常用的字段。但请注意,该值是一个字符串(如 zend_string *),而不是其他内容。

  • 然后,就像我们在上面的介绍章节中详细介绍的那样,我们找到了 orig_valueorig_modifiedmodifiablemodified 字段,它们与设置值的修改有关。设置必须在内存中保留其原始值(作为 “主值”, master value)。

  • modifiable 表示设置是否真正可修改,并且必须从 ZEND_INI_USERZEND_INI_PERDIRZEND_INI_SYSTEMZEND_INI_ALL 中选择一些值,这些值可以一起标记,并在 PHP 手册 中详细说明。

  • 每次在请求期间修改设置时,modified 都会设置为 1,以便引擎在请求关闭时知道它必须将该 INI 设置值恢复为其主值,以便处理下一个请求。

  • on_modify() 是一个处理程序,每当当前设置的值被修改时都会被调用,例如使用对 ini_set() 的调用(但不限于此)。我们稍后会更深入地讨论 on_modify(),但可以将其视为一个 验证器函数(例如,如果设置预期表示一个整数,您可以根据整数验证将给出的值)。它还可以作为更新全局值的记忆桥梁,我们稍后也会回到这一点。

  • diplayer() 不太有用,通常不会传递任何值。displayer() 是关于如何显示您的设置。例如,您可能还记得 PHP 倾向于将布尔值 true/yes/on/1 等显示为 On。这是 displayer() 的工作:将当前值转换为 “显示” 值。

您还需要处理这个结构 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;

zend_ini_entry_defzend_ini_entry 非常相似,当程序员(你)必须在引擎中注册一个 INI 设置时,就会用到 zend_ini_entry_def。引擎读取 zend_ini_entry_def,并根据你提供的定义模型在内部创建一个 zend_ini_entry 供自己使用。很简单。

注册和使用 INI 条目

注册

INI 设置在请求过程中始终有效。它们可能会在请求期间在运行时更改其值,但在请求关闭时它们会恢复为原始值。因此,在扩展的 MINIT() 钩子中,注册 INI 设置在一次完成后即可完成。

您必须做的是声明 zend_ini_entry_def 的向量,专用宏将为您提供帮助。然后,您将向量注册到引擎,声明就完成了。让我们通过上一章关于随机数选择和猜测的示例来看一下,现在再次仅显示相关部分:

PHP_INI_BEGIN()
PHP_INI_ENTRY("pib.rnd_max", "100", PHP_INI_ALL, NULL)
PHP_INI_END()

PHP_MINIT_FUNCTION(pib)
{
    REGISTER_INI_ENTRIES();

    return SUCCESS;
}

PHP_MINFO_FUNCTION(pib)
{
    DISPLAY_INI_ENTRIES();
}

PHP_MSHUTDOWN_FUNCTION(pib)
{
    UNREGISTER_INI_ENTRIES();

    return SUCCESS;
}

这是最简单的 INI 声明,我们不会保持原样,但步骤很简单:使用 PHP_INI_BEGINPHP_INI_END 宏声明一个 zend_ini_entry_def[] 向量。在中间,您再次使用宏添加自己的 zend_ini_entry_def 条目。我们使用最简单的一个:PHP_INI_ENTRY(),它只需要四个参数:要注册的条目的名称、如果它不是 INI 文件扫描的一部分则给出的默认值(有关详细信息,请参阅上一章)、修改级别、PHP_INI_ALL 表示 “无处不在”。我们还没有使用验证器,并传递了 NULL

MINIT hook 中,我们使用 REGISTER_INI_ENTRIES 宏来完成描述工作,而其对应部分 UNREGISTER_INI_ENTRIES 则用于在模块关闭时释放分配的资源。

现在,新的 “pib.rnd_max” INI 设置被声明为 PHP_INI_ALL,这意味着用户可以使用 ini_set() 修改其值(并使用 ini_get() 将其读回)。

我们没有忘记使用 DISPLAY_INI_ENTRIES() 将这些 INI 设置显示为扩展信息的一部分。在 MINFO() 钩子的声明中忘记这一点将导致我们的 INI 设置在信息页面(phpinfo())中对用户隐藏。如果需要,请查看 扩展信息章节

使用

作为扩展开发人员,我们现在可能需要您自己读取 INI 设置值。在扩展中执行此操作的最简单方法是使用宏,这些宏将在保留所有 INI 设置的主数组中查找值,找到它并将其返回为您要求的类型。我们根据想要返回的 C 类型提供了几个宏。

INI_INT(val)INI_FLT(val)INI_STR(val)INI_BOOL(val) 所有四个宏都将从 INI 设置数组中查找提供的值并将其返回(如果找到)并转换为您要求的类型。

请记住,在 zend_ini_entry 中,值是 zend_string 类型。在我们的示例中,我们注册了一个类型为 “long” 的 INI 设置,即 pib.rnd_max,其默认值为 100。但是,该值作为 zend_string 注册到 INI 设置数组中,因此每次我们想要读回它的值时,都需要将其转换为 “long”。INI_INT() 就是做这样的工作的。

示例:

php_printf("The value is : %lu", INI_INT("pib.rnd_max"));

如果找不到该值,则返回 0,因为我们要求的是 long。如果是浮点数转换等,也会返回 0.0。

如果用户修改了设置,而我们想要显示 “主” 原始值(在我们的例子中是 100),那么我们将使用 INI_ORIG_INT() 而不是 INI_INT()。当然,其他类型也存在此类偏差宏。

验证器和全局内存桥

到目前为止一切顺利,注册和读回 INI 设置值并不难。但我们在前面几行中使用它们的方式远非最佳。

使用 “高级” INI 设置 API 可以同时解决两个问题:

  • 每次我们想要读取我们的值时,都需要查找主 INI 设置表,以及转换为正确的类型(通常)。这些操作会花费一些 CPU 周期。

  • 我们没有提供任何验证器,因此用户可以更改我们的设置并将任何他想要的内容作为值。

解决方案是使用 on_modify() 验证器和内存桥来更新全局变量。

通过使用高级 INI 设置管理 API,我们可以告诉引擎正常注册我们的设置,但我们也可以指示它在每次更改 INI 设置值时更新我们喜欢的全局变量。因此,每当我们想要读回我们的值时,我们只需要读取我们的全局变量。在我们需要经常读取 INI 设置值的情况下,这将提高性能,因为不再需要哈希表查找和转换操作。

您需要熟悉全局变量才能继续阅读本章。全局空间管理将 单独成章

要将内存桥声明为全局变量,我们需要创建一个请求全局变量,并更改声明 INI 设置的方式。如下所示:

ZEND_BEGIN_MODULE_GLOBALS(pib)
    zend_ulong max_rnd;
ZEND_END_MODULE_GLOBALS(pib)

ZEND_DECLARE_MODULE_GLOBALS(pib)

PHP_INI_BEGIN()
    STD_PHP_INI_ENTRY("pib.rnd_max", "100", PHP_INI_ALL, OnUpdateLongGEZero, max_rnd, zend_pib_globals, pib_globals)
PHP_INI_END()

PHP_MINIT_FUNCTION(pib)
{
    REGISTER_INI_ENTRIES();

    return SUCCESS;
}

我们声明一个名为 max_rnd 的全局变量,类型为 zend_ulong。然后,我们这次使用 STD_PHP_INI_ENTRY() 注册我们的 “pib.rnd_max” INI 值。这允许我们将更多参数传递给宏。前三个是已知的,我们在本章之前详细介绍了它们。

最后四个参数代表全局桥。我们告诉我们想要更新 max_rnd,在 zend_pib_globals 结构中,由符号 pib_globals 表示。如果不舒服,请阅读全局管理章节。快速提醒一下,ZEND_BEGIN_MODULE_GLOBALS() 声明 zend_pib_globals 结构,而 ZEND_DECLARE_MODULE_GLOBALS() 声明这种类型的 pib_globals 符号。

我们声明一个名为 max_rnd 的全局变量,其类型为 zend_ulong。然后,我们使用 STD_PHP_INI_ENTRY() 注册我们的 “pib.rnd_max” INI 值。这样我们就可以向宏传递更多参数。前三个参数是已知的,我们在本章中已经详细介绍过。

最后四个参数代表 globals 桥接。我们要更新由 pib_globals 符号表示的 zend_pib_globals 结构中的 max_rnd。如果不习惯,请阅读全局管理章节。作为一个快速提醒,ZEND_BEGIN_MODULE_GLOBALS() 声明了 zend_pib_globals 结构,而 ZEND_DECLARE_MODULE_GLOBALS() 声明了这样一个类型的 pib_globals 符号。

在内部, offsetof 将用于计算 max_rnd 成员的字节切片到 zend_pib_globals 结构中,以便能够在 ‘pib.rnd_max’ 发生变化时更新该部分内存。

这里使用的 on_modify() 验证器 onUpdateLongGEZero() 是 PHP 中存在的默认验证器,用于验证值是否大于或等于零。全局变量需要此验证器才能更新,因为此工作已在验证器中完成。

现在,要读回我们的 INI 设置值,我们只需读取 max_rnd 全局变量的值:

php_printf("The value is : %lu", PIB_G(max_rnd));

大功告成。

现在让我们来看看验证器(on_modify() 处理程序)。验证器有两个目标:

  • 验证传递的值

  • 如果验证成功,则更新全局变量

只有在 INI 设置被设置或修改(写入)时,才会调用验证器。

如果您希望全局变量使用 INI 设置值进行更新,则需要验证器。这种机制不是由引擎神奇地执行的,而必须明确地在验证器中执行。

我们看一下 onUpdateLongGEZero() 源代码:

#define ZEND_INI_MH(name) int name(zend_ini_entry *entry, zend_string *new_value,
                                    void *mh_arg1, void *mh_arg2, void *mh_arg3, int stage)

ZEND_API ZEND_INI_MH(OnUpdateLongGEZero)
{
    zend_long *p, tmp;
#ifndef ZTS
    char *base = (char *) mh_arg2;
#else
    char *base;

    base = (char *) ts_resource(*((int *) mh_arg2));
#endif

    tmp = zend_atol(ZSTR_VAL(new_value), (int)ZSTR_LEN(new_value));
    if (tmp < 0) {
        return FAILURE;
    }

    p = (zend_long *) (base+(size_t) mh_arg1);
    *p = tmp;

    return SUCCESS;
}

如您所见,没有什么复杂的。您的验证器被赋予了 new_value,并且必须根据它进行验证。请记住,new_value 的类型为 zend_string *onUpdateLongGEZero() 将值作为 long 并检查它是否为正整数。如果一切顺利,验证器必须返回 SUCCESS,否则返回 FAILURE

然后是更新全局的部分。mh_arg 变量用于将任何类型的信息传送到您的验证器。

‘mh’ 代表修改处理程序。验证器回调也称为 修改处理程序回调

mh_arg2 是一个指向内存区域的指针,该内存区域代表全局结构内存的开头,在我们的例子中,是 pib_globals 分配内存的开头。请注意,当我们谈论请求全局变量内存时,无论您是否使用 ZTS 模式,后者的访问方式都不同。有关 ZTS 的更多信息, 请参见此处

mh_arg1 传递了全局成员的计算偏移量(对于我们来说是 max_rnd),您必须自己对内存进行切片以获取指向它的指针。这就是为什么我们将 mh_arg2 存储为通用 char * 指针并将 mh_arg1 转换为 size_t 的原因。

然后,您只需通过写入指针来使用经过验证的值更新内容。mh_arg3 实际上未使用。

PHP 的默认验证器是 OnUpdateLongGEZero()OnUpdateLong()OnUpdateBool()OnUpdateReal()OnUpdateString()OnUpdateStringUnempty()。它们的名称和源代码都是自描述的(你可以阅读它)。

基于这样的模型,我们可以开发自己的验证器,用于验证 01000 之间的正整数,例如:

ZEND_INI_MH(onUpdateMaxRnd)
{
    zend_long tmp;

    zend_long *p;
#ifndef ZTS
    char *base = (char *) mh_arg2;
#else
    char *base;

    base = (char *) ts_resource(*((int *) mh_arg2));
#endif

    p = (zend_long *) (base+(size_t) mh_arg1);

    tmp = zend_atol(ZSTR_VAL(new_value), (int)ZSTR_LEN(new_value));

    if (tmp < 0 || tmp > 1000) {
        return FAILURE;
    }

    *p = tmp;

    return SUCCESS;
}

PHP_INI_BEGIN()
    STD_PHP_INI_ENTRY("pib.rnd_max", "100", PHP_INI_ALL, onUpdateMaxRnd, max_rnd, zend_pib_globals, pib_globals)
PHP_INI_END()

一旦检查了范围(验证器会执行此操作),就可以安全地将长整型写入无符号长整型。

现在,如果用户想要修改设置并传递一个未经验证的错误值,ini_set() 只会向用户空间返回 false,并且不会修改该值:

ini_set('pib.rnd_max', 2048); /* returns false as 2048 is > 1000 */

相反,ini_set() 返回旧值并修改当前值。新提供的值将成为当前 “本地(local)” 值,而默认的前一个值则保留为 “主值,master value”。phpinfo()ini_get_all() 详细说明了这些值。示例:

ini_set('pib.rnd_max', 500);

var_dump(ini_get_all('pib'));

/*
array(1) {
  ["pib.rnd_max"]=>
  array(3) {
    ["global_value"]=>
    string(3) "100"
    ["local_value"]=>
    string(3) "500"
    ["access"]=>
    int(7)
  }
*/

请注意,每次值更改时都会调用验证器回调,并且会更改多次。例如,对于我们的小示例,我们设计的验证器被调用三次:

  • 一次在 REGISTER_INI_ENTRY() 中,一次在 MINIT() 中。我们在此处将默认值设置为我们的设置,因此这是使用我们的验证器完成的。请记住,默认值可能来自 INI 文件解析。

  • 每次 ini_set() 用户空间调用一次。

  • 一次在 RSHUTDOWN() 中,如果值在当前请求期间已更改,则引擎将尝试将本地值恢复为其主值。用户空间 ini_restore() 执行相同的工作。

还请记住,值访问器由 ini_set() 检查。如果我们设计了一个 PHP_INI_SYSTEM 设置,那么用户将无法使用 ini_set() 修改它,因为 ini_set() 使用 PHP_INI_USER 作为访问器。然后就会检测到不匹配的情况,在这种情况下,引擎就不会调用验证器。

如果需要在运行时更改扩展的 INI 设置值,内部调用 zend_alter_ini_entry(),这也是用户态 ini_set() 所使用的。

使用显示器

关于 INI 设置,您需要了解的最后一件事是 displayer() 回调。它在实践中用得较少,每当用户空间要求 “打印” 您的 INI 设置值时,即通过使用 phpinfo()php --ri,它都会被触发。

如果您不提供显示器,则将使用默认显示器。查看它:

> php -dextension=pib.so -dpib.rnd_max=120 --ri pib

Directive => Local Value => Master Value
pib.rnd_max => 120 => 120

默认显示器采用 INI 设置值(提醒一下,其类型为 zend_string *),并简单地显示它。如果未找到任何值或值为空字符串,则显示字符串 “no value”。

要实现这样的过程,我们必须声明将被调用的 displayer() 回调。让我们尝试将我们的 “pib.rnd_max” 值表示为百分比条,带有 “#” 和 “.” 字符。仅举一个例子:

#define ZEND_INI_DISP(name) void name(zend_ini_entry *ini_entry, int type)

ZEND_INI_DISP(MaxRnd)
{
    char disp[100] = {0};
    zend_ulong tmp = 0;

    if (type == ZEND_INI_DISPLAY_ORIG && ini_entry->modified && ini_entry->orig_value) {
        tmp = ZEND_STRTOUL(ZSTR_VAL(ini_entry->orig_value), NULL, 10);
    } else if (ini_entry->value) {
        tmp = ZEND_STRTOUL(ZSTR_VAL(ini_entry->value), NULL, 10);
    }

    tmp /= 10;

    memset(disp, '#', tmp);
    memset(disp + tmp, '.', 100 - tmp);

    php_write(disp, 100);
}

PHP_INI_BEGIN()
    STD_PHP_INI_ENTRY_EX("pib.rnd_max", "100", PHP_INI_ALL, onUpdateMaxRnd, max_rnd, zend_pib_globals,
                          pib_globals, MaxRnd)
PHP_INI_END()

这次我们使用 _EX() 宏对应部分来声明我们的 INI 设置。此宏接受显示器函数作为最后一个参数。然后使用 STD_PHP_INI_ENTRY_EX()

然后使用 ZEND_INI_DISP() 来声明我们的显示器函数。它接收已附加到的 INI 设置作为参数,以及 PHP 希望您显示的值:ZEND_INI_DISPLAY_ORIG 表示主值,ZEND_INI_DISPLAY_ACTIVE 表示当前请求绑定的本地值。

然后,我们玩弄这个值,将其表示为 “#” 和 “.” 字符,类似于这样:

ini_set('pib.rnd_max', 500);
phpinfo(INFO_MODULES);

如果我们用:

> php -dextension=pib.so /tmp/file.php

然后显示:

pib

Directive => Local Value => Master Value
pib.rnd_max => ##################################################..................................................
            => ##########..........................................................................................

如果我们用:

> php -dextension=pib.so -dpib.rnd_max=10 /tmp/file.php

然后显示:

pib

Directive => Local Value => Master Value
pib.rnd_max => ##################################################..................................................
            => ....................................................................................................

由于 PHP 将同时显示本地值和主值,我们的显示器回调将在此处被调用两次。本地值实际上代表的是 “500”,而主值显示的是默认的硬编码值 “100”(如果我们不更改它),如果我们使用 php-cli 中的 -d 来更改它,它实际上就被使用了。

如果想使用 PHP 中现有的显示程序,可以使用 zend_ini_boolean_displayer_cb()zend_ini_color_displayer_cb()display_link_numbers()