分类 网页与编程 中的文章

PHP 8.1 正式版已经发布

The PHP development team announces the immediate availability of PHP 8.1.0. This release marks the latest minor release of the PHP language.

PHP 8.1 comes with numerous improvements and new features such as:

Take a look at the PHP 8.1 Announcement Addendum for more information.

For source downloads of PHP 8.1.0 please visit our downloads page, Windows source and binaries can be found on windows.php.net/download/. The list of changes is recorded in the ChangeLog.

The migration guide is available in the PHP Manual. Please consult it for the detailed list of new features and backward incompatible changes.

Many thanks to all the contributors and supporters!

Pader 2021-11-26 0

在长驻 PHP 程序中慎用 is_file 和 is_dir 函数

最近有一个场景一直让我郁闷,在一个基于 Wind 框架开发的长驻程序中,会经常往某个临时目录写入文件,但程序日志中每隔一段时间就会报找不到文件的错误,原因是那个目录不存在,后来想起来该临时目录每隔一段时间就会被一个脚本清理,而清理的方式是直接删除这个目录。这个临时目录在之前的程序中(传统的 php-fpm 模式的程序),每次写入会判断该临时目录是否存在,不存在则创建,于是在这个长驻的程序中也加入了这样的逻辑。代码很简单,如下:

$dir = dirname($realPath);
if (!is_dir($dir)) {
    mkdir($dir);
}

心想这下总该没问题了吧,但是程序运行一段时间之后,那个错误依然存在,这就很奇怪了,反复测试确认程序是能在该位置创建目录的。

经过一会琢磨,想起了 clearstatcache() 这个函数,此函数简单的说是可以清除 php 运行中关于文件系统的一些状态缓存,文档中描述是 stat(), lstat(), file_exists(), is_writable(), is_readable(), is_executable(), is_file(), is_dir(), is_link(), filectime(), fileatime(), filemtime (), fileinode(), filegroup(), fileowner(), filesize(), filetype() 和 fileperms() 这些函数都会受这个缓存的影响。

不过经过我的测试,is_file 和 is_dir 确实会受文件状态缓存的影响,而 file_exists 则不受影响,并且有时多次调用 is_dir 对多个目录进行判断,会有部分较早调用的 is_dir 不受缓存影响,这里有点琢磨不透,猜测这里的缓存机制应该并不是简单的全部缓存,而是具有某些类似于 LRU 的机制。

在长驻进程中(可以使用 php -a)进行测试,用 is_dir 对某个目录判断是否存在,当这个目录存在时,在另一处删除这个目录,只要之前的 php 进程并未关闭,那么该进程的 is_dir 会始终认为这个目录是存在的,除非主动调用了 clearstatcache() 清除状态缓存,或者该目录是由进程中的 unlink() 函数删除的,也会自动清除该目录的状态缓存。

所以保险起见,在常驻进程的 PHP 程序中应使用 file_exists() 来判断文件或目录是否存在,不过虽然 file_exists() 在实际测试中并不受缓存影响,但文档中描述它也是受影响的,真正的保险起见,在判断文件是否存在前应还是先调用一次 clearstatcache() 最佳。

Pader 2021-5-22 0

为 Laravel Http 客户端添加详细的请求日志

有时候难免要对 Http 的请求和响应包体进行记录以方便查找问题或做什么。

Laravel 的 Http 客户端是基于 Guzzle 进行封装的,在上层进行了简化,并没有直接给我们留相关的日志配置,想要对请求的 http 进行详细的记录,则需要借助于 Guzzle 的 Handler/中间件,和 withOptions 方法。

首先们要使用 composer 安装一个第三方的 Guzzle 日志中间件。

composer require rtheunissen/guzzle-log-middleware

该中间件比较简单,仅是在请求发生时将请求和响应对象传递给我们指定的闭包,然后我们在闭包中直接调用 Log 的方法进行记录即可。

简单的演示如下:

$stack = new HandlerStack();
$stack->setHandler(new CurlHandler()); //使用 HandlerStack 后必须指定一个 Handler

//日志中间件
$logger = new Logger(function ($level, $message, array $context) {
    Log::log($level, $message);
});

$stack->push($logger);

$res = Http::withOptions([
    'handler' => $stack
])->post($url, $data);

此时请求发生时我们就会在日志里看到一条这样的简单日志:

[2021-04-28 17:00:30] local.INFO: homestead GuzzleHttp/7 - [28/Apr/2021:17:00:30 +0800] "POST /your/request/url HTTP/1.1" 200 136

不过显示这个日志太简单了,我们需要更详细的信息,比如请求和响应的头信息及主体内容,通过该中间件的主页得知可使用一个闭包来进行 message 的格式化。

于是我们对 Logger 进行一番修改,从 Request 和 Response 中取出相应的信息,并且拼装成 Http 的包体结构,结果如下:

$logger = new Logger(function ($level, $message, array $context) {
    Log::log($level, $message);
}, function ($request, $response, $reason) {
    /**
     * @var Request $request
     * @var Response $response
     */
    $requestBody = $request->getBody();
    $requestBody->rewind();
    
    //请求头
    $requestHeaders = [];
    
    foreach ($request->getHeaders() as $k => $vs) {
        foreach ($vs as $v) {
            $requestHeaders[] = "$k: $v";
        }
    }
    
    //响应头
    $responseHeaders = [];
    
    foreach ($response->getHeaders() as $k => $vs) {
        foreach ($vs as $v) {
            $responseHeaders[] = "$k: $v";
        }
    }
    
    $uri = $request->getUri();
    $path = $uri->getPath();
    
    if ($query = $uri->getQuery()) {
        $path .= '?'.$query;
    }
    
    return sprintf(
        "Request %s\n%s %s HTTP/%s\r\n%s\r\n\r\n%s\r\n--------------------\r\nHTTP/%s %s %s\r\n%s\r\n\r\n%s",
        $uri,
        $request->getMethod(),
        $path,
        $request->getProtocolVersion(),
        join("\r\n", $requestHeaders),
        $requestBody->getContents(),
        $response->getProtocolVersion(),
        $response->getStatusCode(),
        $response->getReasonPhrase(),
        join("\r\n", $responseHeaders),
        $response->getBody()->getContents()
    );
});

发送请求后,日志内容如下:

[2021-04-28 17:06:11] local.NOTICE: Request https://www.baidu.com/
POST / HTTP/1.1
User-Agent: GuzzleHttp/7
Content-Type: application/json
Host: www.baidu.com

{"hello":"I am a fake POST."}
--------------------
HTTP/1.1 302 Found
Bdpagetype: 3
Connection: keep-alive
Content-Length: 154
Content-Type: text/html
Date: Wed, 28 Apr 2021 09:06:11 GMT
Location: https://www.baidu.com/search/error.html
Server: BWS/1.1
Set-Cookie: BDSVRTM=0; path=/
Traceid: 161960077103724047468432068516915727024
X-Ua-Compatible: IE=Edge,chrome=1

<html>
<head><title>302 Found</title></head>
<body bgcolor="white">
<center><h1>302 Found</h1></center>
<hr><center>nginx</center>
</body>
</html>

怎么样,详细否?

Pader 2021-4-28 0

关于 PHP 最近的 RFC:Fibers

PHP 在最近收到了一个可以实现协程的扩展 RFC:Fibers(https://wiki.php.net/rfc/fibers)。

Fibers 从本质上来讲是一个加强的,有栈的生成器,通过 Fibers 可以对整个调用栈代码无侵入式的暂停和恢复执行,再配合用户层面实现 EventLoop 和异步 IO,可以做到非常通俗易懂的,很常规的代码中实现协程,说人话就是最终做到不需要用 yield,不需要第三方扩展,即可实现纯 PHP 的协程,写法上和常规的代码没有区别。

但是这样的一个 RFC,却引起了诸多的争辩,尤其是国内知名的协程扩展 Swoole 的相关人员几乎全投了反对票。当然支持的人也是非常多。

纯 PHP 协程框架 Wind Framework 0.1.0 发布啦

        Wind Framework 是我一开始基于纯 PHP 协程实现开发出的一个实验性项目,目的是为了测试纯 PHP 协程应用于工作中的可行性。但经过测试发现应对绝大部分 IO 密集型的场景是完全可行的,于是便基于此不断开发出来的框架。