Skip to content

文件信息

概述

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;  // 完整路径

参考链接