[ PHP 内核与扩展开发系列] INI 配置文件:声明和访问 php.ini 设置

在前面的章节中,我们已经学会了 MINITMSHUTDOWN,以及 RINITRSHUTDOWN 等函数的使用,这里我们将介绍并学习 php.ini 设置的使用。

INI 条目被定义在一个完整的独立块,位于 MINIT 方法所在的同一个源文件,并且用下面的一对宏来定义,并在这对宏之间放入一个或者多个条目:PHP_INI_BEGIN()PHP_INI_END()

这些宏方法和上一章节所提到的 ZEND_BEGIN_MODULE_GLOBALS()ZEND_END_MODULE_GLOBALS() 有着相同的用法。这些结构是用静态数据的实例来声明,而不仅仅是提供一个结构的定义:

static zend_ini_entry ini_entries[] = {
    {0,0,NULL,0,NULL,NULL,NULL,NULL,NULL,0,NULL,0,0,NULL} 
};

正如你所看到的,上面定义了一个 zend_ini_entry 向量值并以一个空记录来终止。你或许已经多次在 function_entry 结构的定义之中看到过这种填充静态向量的方法。

简单 INI 设置

现在你可以使用 INI 结构来声明条目,这个机制是用来注册和销毁一些在机器上的设置,你可以声明一些对你的扩展有用的、有实际意义的设置了。

假设你的扩展的方法就像你在第五章看到的那个例子一样,只是输出一个简单的问候,你可能想让你要输出的问候语句是可定制的:

PHP_FUNCTION(sample4_hello_world) 
{
    php_printf("Hello World!\n"); 
}

最简单的方法就是定义一个 INI 的设置,让它的默认值为 Hello World!,像下面这样:

#include "php_ini.h" 
PHP_INI_BEGIN()
    PHP_INI_ENTRY("sample4.greeting", "Hello World", PHP_INI_ALL, NULL)
PHP_INI_END()

你可能已经猜到,PHP_INI_ENTRY 这个宏里面设置的前面两个参数,分别代表着 INI 设置的名称和它的默认值。第三个参数决定设置是否允许被修改,以及它能被修改的作用域。最后一个参数是一个回调函数,当 INI 的值被修改时触发此回调函数。你将会在下面修改事件的地方详细了解这个参数。

PHP 总共有 4 个指令配置作用域(PHP 中的每个指令都有自己的作用域,指令只能在其作用域中修改,不是任何地方都能修改配置指令的):

参数 含义
PHP_INI_PERDIR 指令可以在 php.inihttpd.conf.htaccess 文件中修改
PHP_INI_SYSTEM 指令可以在 php.inihttpd.conf 文件中修改
PHP_INI_USER 指令可以在用户脚本中修改
PHP_INI_ALL 指令可以在任何地方修改

现在你已经声明了你的 INI 的设置,现在准备将它用在你的问候函数之中:

PHP_FUNCTION(sample4_hello_world) 
{
    const char * greeting = INI_STR("sample4.greeting");
    php_printf("%s\n", greeting); 
}

有一点很重要:char * 类型的值被认为是属于 Zend 引擎的,是不能修改的。正因为如此,你将本地变量设置进 INI 的时候,它将在你的方法中被声明为 const。并不是所有的 INI 值都是基于字符串的,也有其他一些用于整数、浮点数或布尔值的宏,例如:

long lval = INI_INT("sample4.intval"); 
double dval = INI_FLT("sample4.fltval"); 
zend_bool bval = INI_BOOL("sample4.boolval");

通常你想知道当前 INI 设置的值,恭喜你,Zend 内核刚好就存在一组这样的宏为你提供查询每种类型的 INI 的默认值:

const char * strval = INI_ORIG_STR("sample4.stringval"); 
long lval = INI_ORIG_INT("sample4.intval");
double dval = INI_ORIG_FLT("sample4.fltval"); 
zend_bool bval = INI_ORIG_BOOL("sample4.boolval");

注:在这个例子中,sample4 只是 INI 设置的一个前缀,用来帮助保证它的设置不会和其他扩展的设置相冲突。这种加前缀的方法不仅仅是用在闭源的扩展上,更被广泛用在任何商业或者开源的、公开发布的扩展上面。

访问级别

每一个给出的 INI 总是有默认值的。在许多情况下,INI 都有一个比较合理的默认值。然而,在某些比较特殊的环境或者某个脚本要做一些比较特别的事情的情况下,这些 INI 值通常会被修改。这些设置可以在任何三个不同的点被修改,看下表:

访问级别 含义
SYSTEM 设置被放在 php.ini 中,或者在 Apache 的 http.conf 配置文件中,它在Apache 启动时候生效,被认为是设置的全局变量
PERDIR 一些设置被放在 Apache 的 http.conf 的块中,或者 .htaccess 文件中
USER 一旦脚本开始执行,唯一改变 INI 设置的方法就是利用用户方法:ini_set()

某些设置,例如 safe_mode,如果它们能在任何地方被任意修改的话,那它的存在就没意义了。例如,一个恶意脚本的作者只需简单禁用 safe_mode,然后就可以任意读取和修改其他的被禁止的文件。

同样,一些非安全相关的设置,如:register_globalsmagic_quotes_gpc 不能在一个脚本中被改变,因为他们所承担的任务已经过时了。

这些设置是通过 PHP_INI_ENTRY() 的第三个参数来设置的。在你的设置声明之中,你已经使用了 PHP_INI_ALL, 他们是按位或者 PHP_INI_SYSTEM | PHP_INI_PERDIR | PHP_INI_USER 的组合来定义的,如 register_globalsmagic_quotes_gpc 的设置。反过来,使用PHP_INI_SYSTEM | PHP_INI_PERDIR 来声明,对于这些设置,在任何调用 ini_set() 的地方排除 PHP_INI_USER 的话将以失败告终。

现在你可能会猜测,safe_modeopen_basedir 之类的设置只能用 PHP_INI_SYSTEM 来声明。这种设置可以确保只有系统管理员才能修改这些值,因为只有他们有权限修改 php.ini 或者http.conf 中的值。

修改事件

无论 INI 设置在什么时候被修改,无论是通过 ini_set() 方法来修改还是在一个 PERDIR 指令执行期间来修改,Zend 引擎都会通过一个 OnModify 的回调来检查它。修改的人可能会通过使用ZEND_INI_MH 宏来定义,然后通过 OnModify 方法的参数来附加到 INI 设置里面:

ZEND_INI_MH(php_sample4_modify_greeting) 
{
    if (new_value_length == 0) { 
            return FAILURE;
    }
    return SUCCESS; 
}

PHP_INI_BEGIN()
    PHP_INI_ENTRY("sample4.greeting", "Hello World",
        PHP_INI_ALL, php_sample4_modify_greeting) 
PHP_INI_END()

new_value_length 的长度为 0 的时候返回 FAILURE,这样修改者就可以禁止将祝福语句设置为一个空字符串。像下面这样使用 ZEND_INI_MH() 可以生成整个原型:

int php_sample4_modify_greeting(zend_ini_entry * entry, 
    char * new_value, uint new_value_length,
    void * mh_arg1, void * mh_arg2, void * mh_arg3,
    int stage TSRMLS_DC);

回调参数

参数 含义
entry 指向 Zend 引擎实际存储的 INI 设置,这种结构提供一些信息,包括现在的值、原始值、拥有模块等详细信息
new_value 设置的值。如果处理方法返回 SUCCESS,这个值将被填充进 entry->value,如果 entry->orig_value 至今没有设置的话,当前的值将被旋转到这个位置,并且也会设置 entry->modified 这个标志。字符串的长度将被填充到 new_value_length
mh_arg1,2,3 这组指针(三个一组)提供了访问数据指针最初给出的 INI 设置的声明。在实践中,这些值都是通过内部引擎进程来调用的,所以你不需要担心它们
stage ZEND_INI_STAGE_s 这种形式里面有五个值,这五个由 s 代表的值为 STARTUPSHUTDOWNACTIVATEDEACTIVATE 或者 RUNTIME,这些常量分别对应 MINITMSHUTDOWNRINITRSHUTDOWN 还有处于活跃状态正在执行的脚本。

zend_ini_entry

struct _zend_ini_entry { 
    int module_number; 
    int modifiable; 
    char *name;
    uint name_length; 
    ZEND_INI_MH((*on_modify)); 
    void *mh_arg1;
    void *mh_arg2;
    void *mh_arg3;
    char *value;
    uint value_length;
    char *orig_value;
    uint orig_value_length; 
    int modified;
    void ZEND_INI_DISP(*displayer); 
};

显示 INI 设置

在之前的章节,你已经见过 MINFO 方法,还有一些其他关于扩展的基础知识。因为输出 INI 的信息对于一个扩展来说是很正常的,Zend 引擎有一个统一的宏来输出这些内容,它可以被放置在PHP_MINFO_FUNCTION() 块中:

PHP_MINFO_FUNCTION(sample4) 
{
    DISPLAY_INI_ENTRIES(); 
}

这个宏需要 INI 设置的值,而这个值早已经在前面定义在了 PHP_INI_BEGINPHP_INI_END 宏之间。INI 设置被迭代显示在一个三列的表单中,这三列分别是 INI 设置的名字,它的原始(全局)设置,还有通过 PERDIR 或者 ini_set() 修改过的当前的设置。

默认情况下,所有的设置的条目都是根据原有的字符串来简单的输出的。一些类似布尔值和语法高亮显示的颜色的值,在显示的时候会经过特殊的格式化处理。这种特殊格式化的方法是通过每个 INI 设置自己的显示处理程序来处理的,这个处理程序是通过动态指向一个回调函数来实现的,跟我们前面看到的 OnModify 类似。

显示处理程序指定使用一个扩展的 PHP_INI_ENTRY() 宏的版本,PHP_INI_ENTRY() 接受一个额外的参数。如果将它设置为 NULL,默认的显示处理程序将按照字符串的值原样使用。

PHP_INI_ENTRY_EX("sample4.greeting", "Hello World", PHP_INI_ALL, 
    php_sample4_modify_greeting, php_sample4_display_greeting)

明显的,这个回调需要在 INI 设置使用之前提前定义好。与 OnModify 的回调类似,这个用一个包装宏来完成,并且只用少量的代码就能实现:

#include "SAPI.h" /* needed for sapi_module */ 
PHP_INI_DISP(php_sample4_display_greeting)
{
    const char *value = ini_entry->value;
    /* Select the current or original value as appropriate */ 
    if (type == ZEND_INI_DISPLAY_ORIG &&
        ini_entry->modified)  {
          value = ini_entry->orig_value; 
    }                 
    /* Make the greeting bold (when HTML output is enabled) */ 
    if (sapi_module.phpinfo_as_text) {
        php_printf("%s", value); 
    } else {
            php_printf("<b>%s</b>", value); 
    }
}

绑定到扩展的全局设置

所有的 INI 条目都在 Zend Engine 中被分配了存储空间,用来在脚本之中追踪它的改变,并且在请求之外维持全局设置。在这个存储空间之中,所有的 INI 设置都是以字符串形式保存的。你应该会想到,这些值可以使用 INI_INT()INI_FLT()INI_BOOL() 这类宏很容易地转换为标量值。

这个查询和转换过程非常低效的原因有两个,它必须通过名字位于一个 Hash 表之中,所以每次都要重新获取。这种查找方式对于用户空间脚本是非常好的,因为一个脚本只有在运行时才会被编译,但是对于编译型的语言,做这个工作是毫无意义的。

对于标量来说,这个会导致更加低效,因为底层的字符串值在每一次被请求时都必须再转换一次。使用你已经知道的,可以声明一个线程安全的全局存储介质,并在每次改变的时候使用新值的地址来更新它。然后,任何代码都可以通过在你的线程安全的结构之中查找指针来访问 INI 设置,这个可以利用编译时优化。

php_sample4.h 文件中,在 MODULE_GLOBALS 结构中添加 const char * greeting;,然后更新位于 sample4.c 中的两个方法:

ZEND_INI_MH(php_sample4_modify_greeting) 
{
    /* Disallow empty greetings */ 
    if (new_value_length == 0) {
            return FAILURE; 
    }
    SAMPLE4_G(greeting) = new_value;
    return SUCCESS; 
}

PHP_FUNCTION(sample4_hello_world) 
{
    php_printf("%s\n", SAMPLE4_G(greeting)); 
}

因为这是一个优化 INI 值存取的非常一般的方法,另一对宏是通过 Zend 引擎来导出的,将 INI 设置绑定到全局变量:

STD_PHP_INI_ENTRY_EX("sample4.greeting", "Hello World", 
    PHP_INI_ALL, OnUpdateStringUnempty, greeting, 
    zend_sample4_globals, sample4_globals, 
    php_sample4_display_greeting)

这个条目和刚刚你不需要的回调执行相同的工作。相反,他使用一个通用目的的修饰回调 OnUpdateStringUnempty,并且连同信息在存储空间的存储位置。为了允许空白的问候语,你可以简单的指定 OnUpdateString 修饰语,这样就比 OnUpdateStringUnempty 方法简单。

类似的方法,比如 INI 设置可能会被绑定到类似 longdoublezend_bool 之类的标量上。在你的 php_sample4.h 中添加三个条目到 MODULE_GLOBALS 结构中:

long mylong; 
double mydouble; 
zend_bool mybool;

现在使用 STD_PHP_INI_ENTRY() 宏在 PHP_INI_BEGIN()/PHP_INI_END() 块中创建 INI 条目,不同于它的 _EX 版本的仅仅是缺少一个播放者方法以及绑定他们到你的新值:

STD_PHP_INI_ENTRY("sample4.longval", "123", 
    PHP_INI_ALL, OnUpdateLong, mylong, 
    zend_sample4_globals, sample4_globals)
STD_PHP_INI_ENTRY("sample4.doubleval", "123.456", 
    PHP_INI_ALL, OnUpdateDouble, mydouble, 
    zend_sample4_globals, sample4_globals)
STD_PHP_INI_ENTRY("sample4.boolval", "1", 
    PHP_INI_ALL, OnUpdateBool, mybool, 
    zend_sample4_globals, sample4_globals)

注意在这一点上,如果 DISPLAY_INI_ENTRIES() 被调用,"sample4.boolval" 这个布尔类型的 INI 设置会像其他的设置一样以字符串被显示出来;然而,优先以字符串输出的是"on"或者"off"。为了确认这些显示的值是有意义的,你可以在后面的两个方法之中任选一个,其中一个是切换到STD_PHP_INI_ENTRY_EX() 宏创建一个显示的方法,另一个是你可以任意使用可以帮到你的宏:

STD_PHP_INI_BOOLEAN("sample4.boolval", "1", 
    PHP_INI_ALL, OnUpdateBool, mybool, 
    zend_sample4_globals *, sample4_globals)

这种特定类型的宏跟在 INI 家族中的布尔是不一样的,它仅仅提供一个显示处理者来将开启的设置为"on",关闭的设置为"off"

在本章节,你探索了 PHP 中最古老的特性,也可以说是 PHP 健壮可移植性的最大的问题所在。对于每个有用的 INI 设置,它对编程的障碍就是随时都会出现它,这让编程越来越复杂。INI 设置是一把双刃剑,所以在使用之前一定要深思熟虑,不然造成的后果将是长久的;胡乱使用将使你的系统出现很多不可预测的问题。

在接下来的三章中,我们将专研数据流 API,从使用开始,进一步提升到数据流的实现层、包装、环境以及过滤器。

学院君 has written 703 articles

资深PHP工程师,Laravel学院院长

发表评论

标记为*的字段是必填项(邮箱地址不会被公开)

你可以使用这些HTML 标签和属性: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>