文件信息
概述
PHP 提供了丰富的函数用于获取文件和目录的元信息,包括文件大小、类型、权限、修改时间等。stat() 是最全面的文件信息函数,而 filesize()、filetype() 等则是特定用途的便捷函数。注意 PHP 会缓存文件信息,修改后需调用 clearstatcache() 清除缓存。
适用场景
- 文件管理器显示文件属性
- 判断文件类型进行不同处理
- 检查文件权限决定操作
- 文件监控和变更检测
基础概念
核心信息函数
| 函数 | 功能 | 返回值 |
|---|---|---|
stat() | 获取完整文件状态信息 | array|false |
filesize() | 获取文件大小(字节) | int|false |
filetype() | 获取文件类型 | string|false |
is_file() | 判断是否为普通文件 | bool |
is_dir() | 判断是否为目录 | bool |
is_link() | 判断是否为符号链接 | bool |
is_readable() | 判断是否可读 | bool |
is_writable() | 判断是否可写 | bool |
is_executable() | 判断是否可执行 | bool |
file_exists() | 判断文件/目录是否存在 | bool |
filemtime() | 获取修改时间 | int|false |
fileatime() | 获取访问时间 | int|false |
filectime() | 获取 inode 修改时间 | int|false |
fileowner() | 获取文件所有者 | int|false |
fileperms() | 获取文件权限 | int|false |
stat() 返回信息
php
<?php
$stat = stat('/tmp/example.txt');
/*
[
[0] => dev // 设备号
[1] => ino // inode 号
[2] => mode // 权限模式
[3] => nlink // 硬链接数
[4] => uid // 所有者 UID
[5] => gid // 所有者 GID
[6] => rdev // 设备类型(特殊文件)
[7] => size // 文件大小
[8] => atime // 最后访问时间
[9] => mtime // 最后修改时间
[10] => ctime // 最后 inode 变更时间
[11] => blksize // 块大小
[12] => blocks // 占用块数
]
*/PHP 缓存
PHP 会缓存 stat()、file_exists()、is_file() 等函数的结果。在同一请求中,如果文件被修改,必须调用 clearstatcache() 来刷新缓存。
语法与代码示例
文件大小与类型
php
<?php
$filePath = '/tmp/example.txt';
// 文件大小
if (file_exists($filePath)) {
$size = filesize($filePath);
echo "文件大小: {$size} 字节\n";
}
// 文件类型
$type = filetype('/tmp/example.txt'); // 'file'
$type = filetype('/tmp/mydir'); // 'dir'
$type = filetype('/tmp/mylink'); // 'link'
// 可选值:file, dir, link, char, block, fifo, socket, unknown
// 便捷判断
if (is_file($filePath)) {
echo "是普通文件\n";
}
if (is_dir('/tmp/mydir')) {
echo "是目录\n";
}文件时间戳
php
<?php
$filePath = '/tmp/example.txt';
// 最后修改时间(内容变更)
$mtime = filemtime($filePath);
echo "修改时间: " . date('Y-m-d H:i:s', $mtime) . PHP_EOL;
// 最后访问时间(读取文件)
$atime = fileatime($filePath);
echo "访问时间: " . date('Y-m-d H:i:s', $atime) . PHP_EOL;
// inode 修改时间(权限变更、重命名)
$ctime = filectime($filePath);
echo "变更时间: " . date('Y-m-d H:i:s', $ctime) . PHP_EOL;
// touch 更新文件时间
touch($filePath); // 更新 mtime 和 atime 为当前时间
touch($filePath, time() - 3600); // 设置 mtime 为 1 小时前
touch($filePath, time() - 3600, time() - 1800); // mtime 和 atime 分别设置文件权限信息
php
<?php
$filePath = '/tmp/example.txt';
// 获取原始权限值
$perms = fileperms($filePath);
echo "原始权限: " . decoct($perms) . PHP_EOL;
// 提取权限部分(后 12 位)
$perms = fileperms($filePath);
$info = '';
// 类型
$info .= (($perms & 0xC000) == 0xC000) ? 's' : '-'; // socket
$info .= (($perms & 0xA000) == 0xA000) ? 'l' : '-'; // symbolic link
$info .= (($perms & 0x8000) == 0x8000) ? '-' : '-'; // regular
$info .= (($perms & 0x6000) == 0x6000) ? 'b' : '-'; // block
$info .= (($perms & 0x4000) == 0x4000) ? 'd' : '-'; // directory
$info .= (($perms & 0x2000) == 0x2000) ? 'c' : '-'; // character
$info .= (($perms & 0x1000) == 0x1000) ? 'p' : '-'; // FIFO pipe
// 所有者
$info .= ($perms & 0x0100) ? 'r' : '-';
$info .= ($perms & 0x0080) ? 'w' : '-';
$info .= ($perms & 0x0040) ? (($perms & 0x0800) ? 's' : 'x') : (($perms & 0x0800) ? 'S' : '-');
// 组
$info .= ($perms & 0x0020) ? 'r' : '-';
$info .= ($perms & 0x0010) ? 'w' : '-';
$info .= ($perms & 0x0008) ? (($perms & 0x0400) ? 's' : 'x') : (($perms & 0x0400) ? 'S' : '-');
// 其他
$info .= ($perms & 0x0004) ? 'r' : '-';
$info .= ($perms & 0x0002) ? 'w' : '-';
$info .= ($perms & 0x0001) ? (($perms & 0x0200) ? 't' : 'x') : (($perms & 0x0200) ? 'T' : '-');
echo "权限字符串: {$info}\n"; // 如:-rw-r--r--clearstatcache 清除缓存
php
<?php
$filePath = '/tmp/cache.txt';
// 第一次获取大小
echo filesize($filePath) . PHP_EOL; // 0
// 修改文件
file_put_contents($filePath, 'new content');
// 不清除缓存,仍然是旧值
echo filesize($filePath) . PHP_EOL; // 0(缓存)
// 清除缓存后再获取
clearstatcache(true, $filePath);
echo filesize($filePath) . PHP_EOL; // 11(正确值)
// 清除所有文件缓存
clearstatcache();
// clearstatcache 参数
// 第一个参数:是否清除真实路径缓存(缓存符号链接目标的缓存)
// 第二个参数:指定文件名(仅清除该文件的缓存)
clearstatcache(true, '/tmp/cache.txt');实战示例
文件信息格式化工具
php
<?php
declare(strict_types=1);
class FileInfoFormatter
{
public static function format(string $path): array
{
if (!file_exists($path)) {
throw new RuntimeException("文件不存在: {$path}");
}
$stat = stat($path);
return [
'path' => realpath($path),
'type' => filetype($path),
'size' => $stat['size'],
'sizeHuman' => self::formatSize($stat['size']),
'owner' => posix_getpwuid($stat['uid'])['name'] ?? $stat['uid'],
'group' => posix_getgrgid($stat['gid'])['name'] ?? $stat['gid'],
'perms' => self::formatPerms($stat['mode']),
'mtime' => date('Y-m-d H:i:s', $stat['mtime']),
'atime' => date('Y-m-d H:i:s', $stat['atime']),
'ctime' => date('Y-m-d H:i:s', $stat['ctime']),
'isReadable' => is_readable($path),
'isWritable' => is_writable($path),
'isExecutable' => is_executable($path),
];
}
private static function formatSize(int $bytes): string
{
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
$index = (int)floor(log(max($bytes, 1), 1024));
$index = min($index, count($units) - 1);
return round($bytes / pow(1024, $index), 2) . ' ' . $units[$index];
}
private static function formatPerms(int $mode): string
{
$perms = substr(sprintf('%o', $mode), -4);
return $perms;
}
public static function formatTable(array $filesInfo): string
{
$separator = str_repeat('-', 80);
$lines = [$separator];
foreach ($filesInfo as $info) {
$lines[] = sprintf(
"%s %10s %6s %s %s",
$info['perms'],
$info['owner'],
$info['sizeHuman'],
$info['mtime'],
$info['path']
);
}
$lines[] = $separator;
return implode(PHP_EOL, $lines);
}
}
// 使用示例
$info = FileInfoFormatter::format('/tmp/example.txt');
print_r($info);文件变更监控
php
<?php
declare(strict_types=1);
class FileWatcher
{
private array $fileStates = [];
private array $callbacks = [];
public function watch(string $path, callable $callback): void
{
if (!file_exists($path)) {
throw new RuntimeException("文件不存在: {$path}");
}
$stat = stat($path);
$this->fileStates[$path] = [
'mtime' => $stat['mtime'],
'size' => $stat['size'],
];
$this->callbacks[$path] = $callback;
}
public function check(): void
{
foreach ($this->fileStates as $path => $oldState) {
clearstatcache(true, $path);
if (!file_exists($path)) {
$this->callbacks[$path]('deleted', $path, $oldState);
unset($this->fileStates[$path]);
continue;
}
$stat = stat($path);
if ($stat['mtime'] !== $oldState['mtime']) {
$newState = ['mtime' => $stat['mtime'], 'size' => $stat['size']];
$this->callbacks[$path]('modified', $path, $newState);
$this->fileStates[$path] = $newState;
}
}
}
}
// 使用示例
$watcher = new FileWatcher();
$watcher->watch('/tmp/config.json', function (string $event, string $path, array $state) {
echo "[{$event}] {$path} - size: {$state['size']}\n";
});
// 循环检测
while (true) {
$watcher->check();
sleep(2);
}注意事项
大文件 size 问题
php
<?php
// 32 位系统上 filesize 最大返回 2GB - 1
// 64 位系统无此限制
$size = filesize('/tmp/large_file.bin');
// 跨平台安全获取文件大小
function safeFilesize(string $path): int
{
$size = filesize($path);
if ($size === false) {
return -1;
}
// 处理负值(32 位系统大文件)
if ($size < 0) {
$f = fopen($path, 'rb');
if (!$f) return -1;
fseek($f, 0, SEEK_END);
$size = ftell($f);
fclose($f);
}
return $size;
}is_file vs file_exists
php
<?php
// file_exists 对文件和目录都返回 true
file_exists('/tmp/file.txt'); // true(文件)
file_exists('/tmp/mydir'); // true(目录)
file_exists('/tmp/nonexist'); // false
// is_file 只对普通文件返回 true
is_file('/tmp/file.txt'); // true
is_file('/tmp/mydir'); // false
is_file('/tmp/mylink'); // false(符号链接指向文件也返回 false)
// file_exists 检查符号链接是否存在(不解析目标)
file_exists('/tmp/mylink'); // true(链接本身存在)符号链接注意事项
stat() 会跟随符号链接返回目标文件的信息。如果需要获取链接本身的信息,使用 lstat()。
clearstatcache 性能
php
<?php
// 默认不清除真实路径缓存(适用于不删除文件的场景)
clearstatcache();
// 删除文件后清除缓存需要设置第一个参数为 true
unlink('/tmp/deleted.txt');
clearstatcache(true); // 清除真实路径缓存最佳实践
1. 缓存文件信息减少 stat 调用
php
<?php
// 在循环外获取文件信息
$mtime = filemtime('/tmp/config.json');
$size = filesize('/tmp/config.json');
if ($mtime && $size) {
// 使用缓存的信息处理
}
// 避免在循环内重复调用 stat
// 不好
for ($i = 0; $i < 1000; $i++) {
$size = filesize('/tmp/data.txt'); // 每次都调用
}
// 好
$size = filesize('/tmp/data.txt');
for ($i = 0; $i < 1000; $i++) {
// 使用 $size
}2. 异常友好的文件检查
php
<?php
function requireFile(string $path): string
{
if (!file_exists($path)) {
throw new InvalidArgumentException("文件不存在: {$path}");
}
if (!is_file($path)) {
throw new InvalidArgumentException("路径不是文件: {$path}");
}
if (!is_readable($path)) {
throw new RuntimeException("文件不可读: {$path}");
}
return realpath($path);
}3. 使用 SplFileInfo 封装
php
<?php
// SplFileInfo 提供面向对象的文件信息访问
$file = new SplFileInfo('/tmp/example.txt');
echo $file->getFilename() . PHP_EOL; // example.txt
echo $file->getExtension() . PHP_EOL; // txt
echo $file->getSize() . PHP_EOL; // 字节数
echo $file->getMTime() . PHP_EOL; // 时间戳
echo $file->isReadable() ? 'yes' : 'no';
echo $file->getPerms() . PHP_EOL; // 八进制权限
echo $file->getPathname() . PHP_EOL; // 完整路径