探索 PHP 源码(三)——从一个简单的时间函数入门

想必phperdate()函数不会陌生,date('Y-m-d H:i:s')是常见的用法。date扩展代码行数不少,而且一大块宏让人摸不着头脑。先根据自己的思路写一个吧,给PHP添加一个打印当前时间格式化形式的函数。函数原型位于ext/standard/basic_functions.stub.phpfunction pmydate(string $value): void {}函数接收一个格式化字符串,仅支持YmdHis几种格式,没有返回值,直接输出结果。

// ext/standard/basic_functions.stub.php
function pmydate(string $value): void {}

扩展代码:

// ext/standard/basic_functions.c
PHP_FUNCTION(pmydate)
{
    zval *zv_ptr;

    php_printf("passed %d parameters to the function: pmydate\n", ZEND_NUM_ARGS());
    if (zend_parse_parameters(ZEND_NUM_ARGS(), "z", &zv_ptr) == FAILURE) {
        return;
    }

    if (Z_TYPE_P(zv_ptr) != IS_STRING) {
        php_printf("Expect one string argument\n");
        return;
    }

    time_t now = time(0);
    struct tm *lt = localtime(&now);
    char c;
    int val;
    char type;
    char prev = '\0';
    for (int i = 0; i < (*zv_ptr).value.str->len; i++) {
        c = (*zv_ptr).value.str->val[i];
        type = 's';
        switch (c) {
        case 'Y':
            val = lt->tm_year + 1900;
            type = 'i';
            break;
        case 'm':
            val = lt->tm_mon + 1;
            break;
        case 'd':
            val = lt->tm_mday;
            break;
        case 'H':
            val = lt->tm_hour;
            break;
        case 'i':
            val = lt->tm_min;
            break;
        case 's':
            val = lt->tm_sec;
            break;
        case '\\':
            val = c;
            type = '\0';
            break;
        default:
            val = c;
            type = 'c';
        }

        if (prev != '\\') {
            switch (type) {
            case 's':
                php_printf("%02d", val);
                break;
            case 'i':
                php_printf("%d", val);
                break;
            case 'c':
                php_printf("%c", val);
                break;
            default:;
            }
        } else {
            php_printf("%c", c);
        }

        prev = c;
    }
    php_printf("\n");
}

用法如下:

$ ./output/bin/php -r 'pmydate("Y-m-d H:i:s \Y-\m-\d \H:\i:\s");'
passed 1 parameters to the function: pmydate
2025-01-08 00:00:00 Y-m-d H:i:s
  1. 定义函数: 使用PHP_FUNCTION宏封装函数名;
  2. 解释参数:zv_ptr是一个指针变量,指向zval类型,zvalPHP变量类型;php_printf("passed %d parameters to the function: pmydate\n", ZEND_NUM_ARGS())仅用于查看函数参数数量;zend_parse_parameters(ZEND_NUM_ARGS(), "z", &zv_ptr)解释函数参数到zv_ptr指针,如果解释参数失败,直接返回;
  3. 判断参数类型:Z_TYPE_P(zv_ptr)获取解释后的zv_ptr指向的参数类型,如果不是IS_STRING类型,返回;
  4. 获取时间:使用C语言的时间函数获取当前时间,保存到指向struct tm类型的指针中;
  5. 声明临时变量:c保存当前指向的字节,val保存格式串对应字节表示的时间,type表示当前字节应该在输出时以什么类型来处理,prev保存上一个字节;
  6. 遍历字符串参数:由于要接收一个格式化字符串,因此遍历的数据位于(*zv_ptr).value.str中,(*zv_ptr)就是格式化字符串;(*zv_ptr).valuezend_value2.类型,实际上就是一个union,保存具体的参数;字符串参数当然就要通过zend_valuestr字段来获取了;
  7. 逐个获取字符串参数中的字节:(*zv_ptr).value.str是指针,指向zend_string类型,zend_string同时也是一个struct,字符串的字节实际保存在zend_stringval字段中,因此(*zv_ptr).value.str->val[i]指向单个字节;
  8. 根据字符串参数的字节获取对应的时间,其中\字符特殊处理一下,前一个字节为\时,当前字符不作解释;
  9. 其中大片的switch分支不作详细解释,应该很好理解;
  10. 为什么不使用printf而要使用php_printf?因为PHP有多种SAPI,如果只是使用printf只会默认在控制台输出,封装的php_printf则会将数据保存到缓冲中,根据不同的SAPI写入不同的输出流;

Search

    Table of Contents