news 2026/10/3 4:46:07

如何在 PHP 页面页脚中动态显示当前脚本文件的最后修改时间

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
如何在 PHP 页面页脚中动态显示当前脚本文件的最后修改时间

前言

在页脚放一行「本站最后更新:2026-09-29 10:00:00」是很多站点的常见需求,尤其是小工具站和内部后台。第一反应通常是写echo date('Y-m-d', filemtime(__FILE__));,本地跑起来也对,于是就这么上线了。问题会在后面慢慢浮现:页脚显示的时间比实际发布时间早了 8 小时;页面顶部明明改过,页脚时间却纹丝不动;换成另一台机器部署之后,时间突然变成了部署脚本执行的那一刻。

这几个现象分别对应三个独立的坑:时区、__FILE__的语义、以及文件 mtime 本身的含义。

先说清楚一件事:filemtime()返回的是文件在磁盘上的最后修改时间(mtime),它跟「这个页面的内容最后被谁改过」没有必然关系。git clone会把文件的 mtime 设成检出时间,打包发布(tar)会保留原时间,rsync -a保留源时间而rsync不带-a则用当前时间。所以这行字到底该显示什么,取决于你想表达的是「文件何时被改动」还是「站点何时发布」——前者用 mtime 合理,后者应该用一个由构建流程写入的版本常量。

本文把「取时间 → 转成正确时区 → 展示 → 当缓存版本号用」这条链路讲清楚,示例最低需要 PHP 8.1。

一、__FILE__、__DIR__与「当前脚本」的区别

这是最容易出错的一环。假设站点结构是这样:

site/ ├── index.php (入口) └── tpl/ └── footer.php (页脚模板,里面写这行时间)

在footer.php里写filemtime(__FILE__),拿到的是footer.php自己的 mtime,不是index.php的。页脚模板几个月不动一次,而入口文件天天改,于是页脚的时间永远是几个月前。

正确的做法取决于你想表达什么:

写法拿到的是谁的时间适用场景
filemtime(__FILE__)当前这个文件自己只想显示本文件何时被改
filemtime(__DIR__ . '/../index.php')硬编码的入口文件结构固定的站点
filemtime($_SERVER['SCRIPT_FILENAME'])本次请求的入口脚本多入口站点(Web 环境)
构建期写入的常量或版本文件发布时刻最准确,推荐生产使用

$_SERVER['SCRIPT_FILENAME']在 CLI 下不存在,所以要有兜底。另外它在某些 SAPI(Server API)下可能与实际路径有出入,生产上更稳的做法是在构建或发布流程里生成一个版本文件,页脚只负责读它。

二、时区:不设就默认 UTC

date()使用的是date.timezone配置或date_default_timezone_set()设置的时区。PHP 8 在未设置时区时默认使用 UTC,不会报错——于是你的页面在 UTC+8 时区显示的时间比真实时间早 8 小时,而且不容易察觉(差 8 小时看起来也像个正常时间)。

正确做法是用不可变的DateTimeImmutable,把时区显式带上:

$dt = (new DateTimeImmutable('@' . $timestamp)) ->setTimezone(new DateTimeZone('Asia/Shanghai')); echo $dt->format('Y-m-d H:i:s');

DateTimeImmutable有个额外好处:每次修改都返回新对象,不会像DateTime那样出现「某个变量在别处被改掉」的隐性 bug。

三、stat 缓存:改了文件读不到新值

PHP 会缓存stat()系列函数的结果,避免同一个请求里反复访问文件系统。这带来一个隐蔽问题:在长驻进程(常驻容器、queue worker、定时任务循环)里循环调用filemtime(),返回的可能是第一次读到的旧值。加了clearstatcache()才会重新读取:

clearstatcache(true, $path); // 第二个参数指定文件,只清这一个

这两个参数的含义是:clearstatcache(bool $clear_realpath_cache = false, string $filename = ""),同时传文件名时只清除该文件的缓存,比清空全部要高效。

四、完整可运行示例

<?php declare(strict_types=1); /** * lastmod.php —— 读取脚本文件的最后修改时间并格式化 * 最低 PHP 8.1 * CLI 运行:php lastmod.php [文件路径] 默认取本文件 * Web 运行:放到站点里访问即可 */ final class FileTime { /** * @param string $timezone 目标时区,生产环境建议与站点展示时区一致 */ public static function format(int $timestamp, string $timezone = 'Asia/Shanghai'): string { return (new DateTimeImmutable('@' . $timestamp)) ->setTimezone(new DateTimeZone($timezone)) ->format('Y-m-d H:i:s'); } /** 人类可读的相对时间,例如「3 天前」 */ public static function human(int $timestamp, ?int $now = null): string { $now = $now ?? time(); $diff = $now - $timestamp; return match (true) { $diff < 0 => '(时间在未来,请检查文件时间)', $diff < 60 => '刚刚', $diff < 3600 => intdiv($diff, 60) . ' 分钟前', $diff < 86400 => intdiv($diff, 3600) . ' 小时前', $diff < 2592000 => intdiv($diff, 86400) . ' 天前', default => '很久以前', }; } /** 读取 mtime,失败时抛出异常而不是返回 0 */ public static function read(string $path): int { clearstatcache(true, $path); // 长驻进程里必须清,否则读到的是缓存值 $mtime = @filemtime($path); if ($mtime === false) { // 不能直接返回 0 或 false:传给 date() 会静默变成 1970-01-01 throw new RuntimeException('无法读取文件修改时间:' . $path); } return $mtime; } /** 用于 HTTP 头 Last-Modified,必须是 GMT 格式的固定写法 */ public static function httpDate(int $timestamp): string { return gmdate('D, d M Y H:i:s \G\M\T', $timestamp); } } // ---------------- 演示 ---------------- // Web 环境取本次请求的入口脚本;CLI 环境允许用参数指定,默认取本文件 $target = $argv[1] ?? ($_SERVER['SCRIPT_FILENAME'] ?? __FILE__); try { $mtime = FileTime::read($target); } catch (RuntimeException $e) { http_response_code(500); exit($e->getMessage() . PHP_EOL); } $relativePath = basename($target); printf("目标文件:%s%s", $relativePath, PHP_EOL); printf("最后修改:%s(%s)%s", FileTime::format($mtime), FileTime::human($mtime), PHP_EOL); printf("Unix 时间戳:%d%s", $mtime, PHP_EOL); printf("Last-Modified 头:%s%s", FileTime::httpDate($mtime), PHP_EOL); printf("静态资源版本号:?v=%d%s", $mtime, PHP_EOL);
php lastmod.php
目标文件:lastmod.php 最后修改:2026-09-29 10:00:00(刚刚) Unix 时间戳:1790647200 Last-Modified 头:Tue, 29 Sep 2026 02:00:00 GMT 静态资源版本号:?v=1790647200

注意中间两行的差异:同一个时刻,本地时区显示10:00:00,而 HTTP 头必须是 GMT 的02:00:00 GMT。HTTP 协议要求Last-Modified用 GMT 格式,用date()会带上本地时区偏移,缓存协商就会失效。

页脚里实际使用的形态大致是这样:

function footer_last_modified(string $entryFile): string { try { return '本站最后更新:' . FileTime::format(FileTime::read($entryFile)); } catch (RuntimeException) { return ''; // 页脚信息不该因为读不到文件而让整页崩掉 } } echo footer_last_modified($_SERVER['SCRIPT_FILENAME'] ?? __FILE__);

常见坑点


  1. ❌ 在footer.php里用filemtime(__FILE__),以为拿到的是整站页面的时间。


✅ 改用$_SERVER['SCRIPT_FILENAME']取本次请求的入口脚本,或者读构建期生成的版本文件。


  1. ❌ 用date()格式化filemtime()却没设置时区。


✅ 用DateTimeImmutable显式带上DateTimeZone,别依赖服务器的默认时区。


  1. ❌ 直接把filemtime()的返回值传下去,不判断false。


✅ 先判断=== false再使用;false被强转成 0 会显示 1970-01-01,把「文件不存在」这个错误伪装成一个正常时间。


  1. ❌ 在长驻进程里循环读取 mtime,改了文件却读不到新值。


✅ 调用前clearstatcache(true, $path),只清目标文件的 stat 缓存。


  1. ❌ 把 mtime 当作「内容最后修改时间」展示给用户。


✅ 明确它的语义:git clone得到检出时间,rsync -a保留源时间,tar 解包保留打包时间;要展示发布时刻就用构建期写入的常量。


  1. ❌ 用 mtime 当静态资源的缓存版本号,却又用touch()手动改时间。


✅ 版本号要与资源内容强相关,改时间就是「骗缓存」;要么用内容摘要,要么保证发布流程一定会更新 mtime。


  1. ❌ 用date()生成Last-Modified响应头。


✅ HTTP 头必须用 GMT,写成gmdate('D, d M Y H:i:s \G\M\T', $mtime)。


  1. ❌ 每次请求都对几十个文件调用filemtime()拼版本号。


✅ 每个文件一次stat系统调用,文件多起来开销可观;生产上把版本号在构建期算好写进一个常量文件,运行时只读一次。

总结

需求推荐做法关键点
显示文件何时被改filemtime(入口脚本路径)用$_SERVER['SCRIPT_FILENAME'],不要用__FILE__
显示站点何时发布构建期写入的版本常量或版本文件与文件系统时间解耦,部署方式无关
正确显示时间DateTimeImmutable+DateTimeZone不设时区就是 UTC
HTTP 缓存协商gmdate(...)必须是 GMT 格式
长驻进程中读取先clearstatcache(true, $path)否则读到的可能是旧值
文件读取失败抛异常或返回空字符串不要让页脚因为一个时间信息崩掉整页


「显示脚本最后修改时间」看似一行代码,真正决定它是否可靠的其实是三个语义问题:取的是哪个文件的 mtime、这个 mtime 在你们的部署流程里意味着什么、以及时间要按哪个时区展示。把这三件事想清楚,页脚那行字才能一直是对的。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/3 4:45:36

Day 7复盘:用规则和数据战胜意志力,打造可复用的习惯养成法

“Day 7”这三个字&#xff0c;我写在手账的日历上已经整整一周。它代表我给自己定下的一个30天自我实验&#xff1a;每天完成一次深度阅读、一段不少于800字的写作、一次超过20分钟的身体训练&#xff0c;然后在睡前用固定格式做复盘。今天是第七天&#xff0c;正好卡在一个最…

作者头像 李华
网站建设 2026/10/3 4:44:53

招工招聘小程序开发实战:功能模块、跨端适配与性能优化

1. 项目定位与功能模块设计1.1 招工招聘小程序的真实需求场景这几年我接触过不少做人力资源服务的团队&#xff0c;也帮几个客户落地过招聘类的小程序项目&#xff0c;说实话&#xff0c;这个赛道比大部分人想象的要复杂得多。传统招聘平台的问题在于信息过载&#xff0c;BOSS直…

作者头像 李华
网站建设 2026/10/3 4:44:25

如何在 PHP 页面中通过按钮点击将变量安全传递到另一页面

前言页面 A 上有个按钮&#xff0c;点击后要把一个变量&#xff08;订单号、用户 ID、筛选条件&#xff09;带到页面 B。很多人的第一版代码是把值拼进 URL&#xff0c;B 页面直接用 $_GET 读出来就用。功能能跑&#xff0c;问题在后面才冒出来&#xff1a;用户在地址栏把 id1 …

作者头像 李华
网站建设 2026/10/3 4:44:11

OpenShell实战:统一终端配置,解决多端同步与命令复用

从开始用命令行那天起&#xff0c;我就一直在跟"换机器"这件事较劲。本地辛苦攒下来的别名、函数、补全规则&#xff0c;全散落在.zshrc、.bashrc、PowerShell 的$PROFILE里&#xff0c;换台电脑就得手动重来一遍&#xff0c;还总漏掉几个冷门配置。后来我把目光投向…

作者头像 李华
网站建设 2026/10/3 4:42:32

QwenPaw 从零上手:安装配置、参数调优与避坑指南

1. 从零上手 QwenPaw&#xff1a;这个工具到底解决什么问题第一次听到 QwenPaw 这个名字&#xff0c;很多人会下意识把它和某个模型权重、某个推理框架或者某个命令行工具联系起来。我最初接触它的时候也是这个反应&#xff0c;翻了一圈资料才理清楚&#xff1a;QwenPaw 本质上…

作者头像 李华
网站建设 2026/10/3 4:41:26

Windows + WSL2 部署 vLLM:大模型本地推理的完整实践指南

1. vLLM本地部署的整体设计与思路拆解先说结论&#xff1a;如果你在Windows上跑大模型推理&#xff0c;又不甘心只做UI聊天玩具&#xff0c;想正儿八经把vLLM跑起来、接API、走Docker分发&#xff0c;那么“Windows WSL2 vLLM HuggingFace/ModelScope”这条链路是目前最兼顾…

作者头像 李华