PHP中Curl扩展是个好东西,利用它我们可以在程序中很方便地进行HTTP请求。在这个扩展中,curl_setopt
函数和curl_setopt_array
函数被委以了配置请求参数的重任,然而在实际使用中(PHP7之前),这个两个函数暗藏着一个鲜为人知的坑。
情景再现
- 一段很简单的示例代码
<?php
/* 初始化curl句柄 */
$ch = \curl_init();
/* 定义请求参数 */
$params = [
'a' => 1,
'b' => 2.11,
'c' => 'test',
];
/* 设置请求表单数据 */
\curl_setopt($ch, \CURLOPT_POSTFIELDS, $params);
/* 打印请求参数数组 */
var_dump($params);
- 运行结果
- php5 运行结果(以
5.6.37
版本为例)
array(3) { ["a"]=> string(1) "1" ["b"]=> string(4) "2.11" ["c"]=> string(4) "test" }
- php7 运行结果(以
7.2.6
版本为例)
array(3) { ["a"]=> int(1) ["b"]=> float(2.11) ["c"]=> string(4) "test" }
- php5 运行结果(以
- 现象描述
- 在php5中,
$params
变量作为curl_setopt
函数第三个参数被使用之后,其所有成员变量都变成了string
类型,在php7中则不存在这个问题; - 不光是基础数据类型,如果
$params
的某个元素是一个类的实例,且这个类重写了__toString
方法,那么经过上述调用之后,该元素也会变成string
类型; - 根据php官方手册的介绍,
curl_setopt
方法第三个参数并非是引用传递的,理论上不应该出现上面的奇怪现象;
- 在php5中,
源码探究
既然不是引用传递,那么我怀疑是curl_setopt
这个函数中对$param
这个变量直接进行了修改操作,于是我翻看了php5.6的curl扩展源码。
在php-5.6.37/ext/curl/inerface.c
中,我发现curl_setopt
函数接收参数时使用了双指针,如下:
PHP_FUNCTION(curl_setopt)
{
zval *zid, **zvalue;
long options;
php_curl *ch;
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "rlZ", &zid, &options, &zvalue) == FAILURE) {
return;
}
...
if (_php_curl_setopt(ch, options, zvalue TSRMLS_CC) == SUCCESS) { // 这一步里面zvalue作为参数传入
RETURN_TRUE;
} else {
RETURN_FALSE;
}
}
在_php_curl_setopt
函数中,有对这个zvalue
进行了字符串转换操作:
if (Z_TYPE_PP(zvalue) == IS_ARRAY || Z_TYPE_PP(zvalue) == IS_OBJECT) {
zval **current;
HashTable *postfields;
...
for (zend_hash_internal_pointer_reset(postfields);
zend_hash_get_current_data(postfields, (void **) ¤t) == SUCCESS;
zend_hash_move_forward(postfields)
) {
}
...
SEPARATE_ZVAL(current); // 作者本想在这一步复制变量的,但是没有如愿
convert_to_string_ex(current); // 字符串转换操作,结果会直接应用在current上
...
}
- 从这个双指针变量
zvalue
中取出这个zval的HashTable
指针; - 遍历这个
HashTable
,将当前值的指针赋给current
变量; - 判断
current
对应的变量是否是引用,如果是,则开辟内存空间复制这个变量写回current
; - 字符串转换操作,这个过程中变量的值会变成字符串类型;
而本次情形当中,上述第3步总是不会去开辟新内存空间复制处理变量,导致下一步的字符串转换直接将结果赋值给HashTable
对应的变量上,改变了zval
原有的结构,所以导致上述奇怪的现象。
后记
- 其实早在2014年7月份,国外早就有大神发现这个问题并提交到了PHP官方BUG管理平台,此时发现该问题时的PHP版本为5.4;
- 而且该大神很快地提交了自己的纠正方法到PHP的源码仓库,但是这个PR并没有得以合并,可能鸟哥觉得实现不够优雅吧;
- PHP7的curl扩展中已经修复了本文章所提出的问题。