Homestead 是 Vagrant 的虚拟机环境,这种情况下 Xdebug 相对于本机是使用的远程调试方式。
在网上搜索的多数教程都比较老,有的是基于较老的 xdebug2 版本,有的丢失了一些关键步骤导致调试走不通,所以这里做了一下整理,把 PHPStorm 和 VSCode 两种编辑器的配置都上了,以便参阅。
Homestead 是 Vagrant 的虚拟机环境,这种情况下 Xdebug 相对于本机是使用的远程调试方式。
在网上搜索的多数教程都比较老,有的是基于较老的 xdebug2 版本,有的丢失了一些关键步骤导致调试走不通,所以这里做了一下整理,把 PHPStorm 和 VSCode 两种编辑器的配置都上了,以便参阅。
最近有一个场景一直让我郁闷,在一个基于 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() 最佳。
有时候难免要对 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>
怎么样,详细否?
PHP 在最近收到了一个可以实现协程的扩展 RFC:Fibers(https://wiki.php.net/rfc/fibers)。
Fibers 从本质上来讲是一个加强的,有栈的生成器,通过 Fibers 可以对整个调用栈代码无侵入式的暂停和恢复执行,再配合用户层面实现 EventLoop 和异步 IO,可以做到非常通俗易懂的,很常规的代码中实现协程,说人话就是最终做到不需要用 yield,不需要第三方扩展,即可实现纯 PHP 的协程,写法上和常规的代码没有区别。
但是这样的一个 RFC,却引起了诸多的争辩,尤其是国内知名的协程扩展 Swoole 的相关人员几乎全投了反对票。当然支持的人也是非常多。
Wind Framework 是我一开始基于纯 PHP 协程实现开发出的一个实验性项目,目的是为了测试纯 PHP 协程应用于工作中的可行性。但经过测试发现应对绝大部分 IO 密集型的场景是完全可行的,于是便基于此不断开发出来的框架。