Homestead(Vagrant) + Xdebug3 + PHPStorm + VSCode 进行调试

Homestead 是 Vagrant 的虚拟机环境,这种情况下 Xdebug 相对于本机是使用的远程调试方式。

在网上搜索的多数教程都比较老,有的是基于较老的 xdebug2 版本,有的丢失了一些关键步骤导致调试走不通,所以这里做了一下整理,把 PHPStorm 和 VSCode 两种编辑器的配置都上了,以便参阅。

在长驻 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 密集型的场景是完全可行的,于是便基于此不断开发出来的框架。