本文首发于个人博客 【PHP 文件系统完全指南】(
http://blog.phpzendo.com/?p=421
),转载请注明出处。
今天我们将开启一个新的探索旅程,深入到 PHP 文件系统中,系统的学习和掌握 PHP 文件系统的基本使用。
相信大家在日常研发过程中,难免需要和各种文件纠缠不清。比如,打开
.env
文件并从中读取配置信息、把项目中的错误信息写入到日志文件中或者获取图片的创建时间等等。在处理这些功能时,我们都需要使用到 PHP 文件系统接口。
下面是本文所涉主题的提纲:
-
一 什么是文件系统
-
二 深入 PHP 文件系统
-
三 面向对象的目录遍历
-
四 PHP 文件系统思维导图
本文较长,耗时约 20 分钟,请做好战斗准备!
一 什么是文件系统
开始之前,我们首先需要厘清我们所研究的问题领域,理解什么是文件系统,还有我们所研究的对象。
在计算机中,文件系统(file system or filesystem)用于管理数据如何存储和如何被获取的。 - 维基百科
简单来说,就是我们应该如何管理我们的目录(文件夹)和文件。通常,我们将具有相似属性的文件,存储到同一个目录中以便后续查找,这个常见的操作就会涉及到目录和文件。
对于软件工程师来讲,一个非常典型的使用场景,就是在开发
MVC
项目时,将控制器、视图和模型等模块的文件,存储到不同的目录结构中方便管理。
无论如何,我们依据不同特性划分文件和目录都是为了解决文件存储和查找的问题。
有了这些认知后,应该自然而然的想到我们当前研究的 PHP 文件系统(或者说文件系统)其所研究的对象,简单概括起来就是:
-
目录(文件夹)
-
文件
也就是说,本文我们所讲解的 PHP 文件系统函数处理,基本都是围绕目录和文件展开的。
二 深入 PHP 文件系统
在 PHP 文件系统中内置提供了超过 80 个可用的 文件系统函数。由于数量繁多功能强大,自然本文无法将对所有的系统函数逐一讲解。一来,时间过于仓促;再者,短时间内我们也没有那么多的精力将它们全部掌握。
尽管如此,大家也不必气馁,本文会将有限的时间和精力,来研究以下几个在文件处理时的常见话题:
-
文件的元数据应该如何获取
-
文件的 MIME 类型如何获取
-
文件和目录的操作处理
-
文件和目录的权限管理
另外,补充说明一点,PHP 标准函数库不仅为我们提供了面向过程的文件系统处理函数。同时,还封装了常用目录及文件操作的面向对象接口和迭代器接口方便大家使用:
文件系统的元数据
什么是元数据
元数据(meta data):通俗一点讲就是「数据的数据」。拿一个 php 文件来说它的元数据可以是
创建时间
、
文件名
、
文件大小
或
文件所有权限
等,这类能够表明该文件基本特征的数据就是「元数据(meta data)」了。
常用元数据获取
在这一节,我们将学习一些经常需要获取的文件元数据函数,包括:
-
获取文件的最后修改时间
-
获取文件的上次访问时间
-
获取文件的路径信息
-
获取文件的绝对路径
-
获取文件类型
-
获取文件大小
-
获取文件权限
-
获取文件所属用户及用户组
话不多说,开干吧!
1、获取文件的最后修改时间
要获取文件的上次被修改时间戳,我们可以使用函数 filemtime($filename) 或 SplFileInfo::getMTime() 方法。
注意:SplFileInfo 类实例化时接收 $filename 文件路径作为参数,后续没有特别说明默认我们已经获取到了 SplFileInfo 实例才能进行 getMTime() 等类似处理。
// 文件路径请求改成你自己的文件路径
$filename = "f://filesystem/test.txt";
// 面向过程: 获取文件时间
$modifyTimestamp
= filemtime($filename);
// 面向对象
$file = new SplFileInfo($filename);
$modifyTimestamp = $file->getMTime();
2、获取文件的上次访问时间
可以使用函数 fileatile($filename) 或 SplFileInfo::getATime() 方法,来获取文件的最后被访问时间戳。
// 文件路径请求改成你自己的文件路径
$filename = "f://filesystem/test.txt";
// 面向过程: 获取文件时间
$accessTimestamp = fileatime($filename);
// 面向对象
$file = new SplFileInfo($filename);
$accessTimestamp = $file->getATime();
除了
filemtile
和
fileatime
之外,还有
filectime
来获取文件的 inode 修改时间(可认为是创建时间)。
有关时间的函数常用的就这些,为了方便记住,我们来看看它们是如何命名的:
-
2.1 面向过程 file 前缀,面向对象 get 前缀
-
2.2 a: access(访问);m:modify(修改);c:create(创建)
-
2.3 time 后缀
-
2.4 fileatime,SplFileInfo::getATime;filemtime,SplFileInfo::getMTime;filectime,SplFileInfo::getCTime。
是不是很简单呢!
注意,使用 filectime 时,对于 Windows 系统会获取创建时间,但对于类 Unix 系统是修改时间,因为在类 Unix 系统中多数文件系统并没有创建时间的概念。具体说明可以看 PHP: how can I get file creation date?。
3、获取文件的路径信息
除了时间这些元数据,另一个经常遇到的情况是获取文件的路径信息,包括:
3.1 目录信息
获取目录信息我们可以使用
pathinfo(\$filename, PATHINFO_DIRNAME)
、
dirname(\$filename)
和
SplFileInfo::getPath()
比如下面给出的文件:
$filename = 'F:\Program Files\SSH Communications Security\SSH Secure Shell\Output.txt';
将会获取到
F:\Program Files\SSH Communications Security\SSH Secure Shell
这部分目录信息。
3.2 文件名信息
这里我们所有的文件名指的是不带扩展名后缀的文件名称,比如需要获取
your_path/filename.txt
中的
filename
部分。
需要取得文件名信息,我们可以使用
pathinfo(\$filename, PATHINFO_FILENAME)
、
basename(\$filename, \$suffix)
和
SplFileInfo::getBasename(\$suffix)
获取。
这里给出的
$suffix
指不获取
$suffix
扩展名部分(比如不获取
$suffix = '.txt'
)。
请看下面的示例:
$filename = 'F:\Program Files\SSH Communications Security\SSH Secure Shell\Output.txt';
将会获取到
Output
这部分文件名信息。
3.3 扩展名信息
扩展名我们可以使用
pathinfo(\$filename, PATHINFO_EXTENSION)
和
SplFileInfo::getExtension()
方法拿到。
基于前面的了解,我们可以获取到
txt
这部分扩展信息,这里不再赘述。
3.4 basename(文件名 + 扩展名)信息
basename
指的是
文件名 + 扩展名
内容信息,可以使用
pathinfo(\$filename, PATHINFO_BASENAME)
、
basename(\$filename)
、
SplFileInfo::getBasename()
和
SplFileInfo::getFilename()
方法拿到。
虽然这里我们列出了很多的函数,但是基本上还是比较容易理解的,需要注意的是:
3.5 示例
php
$filename = 'F:\Program Files\SSH Communications Security\SSH Secure Shell\Output.txt';
$file = new SplFileInfo(
$filename);
// 目录路径
$directory1 = pathinfo($filename, PATHINFO_DIRNAME);
$directory2 = dirname($filename);
$directory3 = $file->getPath();
echo '--- directory begin: ---' . PHP_EOL;
echo $directory1 . PHP_EOL, $directory2 . PHP_EOL, $directory3 . PHP_EOL;
// 文件名
$suffix = '.txt';
$filename1 = pathinfo($filename, PATHINFO_FILENAME);
$filename2 = basename($filename, $suffix);
$filename3 =
$file->getBasename($suffix);
echo '--- filename begin: ---' . PHP_EOL;
echo $filename1 . PHP_EOL, $filename2 . PHP_EOL, $filename3 . PHP_EOL;
// 扩展名
$extension1 = pathinfo($filename, PATHINFO_EXTENSION);
$extension2 = $file->getExtension();
echo '--- extension begin: ---' . PHP_EOL;
echo $extension1 . PHP_EOL, $extension2 . PHP_EOL;
// basename = 文件名 + 扩展名
$basename1 = pathinfo($filename, PATHINFO_BASENAME);
$basename2 =
basename($filename);
$basename3 = $file->getBasename();
$basename4 = $file->getFilename();
echo '--- basename begin: ---' . PHP_EOL;
echo $basename1 . PHP_EOL, $basename2 . PHP_EOL, $basename3 . PHP_EOL, $basename4 . PHP_EOL;
它们的运行结果如下:
--- directory begin: ---
F:\Program Files\SSH Communications Security\SSH Secure Shell
F:\Program Files\SSH Communications Security\SSH Secure Shell
F:\Program Files\SSH Communications Security\SSH Secure Shell
--- filename begin: ---
Output
Output
Output
--- extension begin: ---
txt
txt
--- basename begin: ---
Output.txt
Output.txt
Output.txt
Output.txt
3.6 文件路径信息关系图
另外需要注意的一点是在使用 SplFileInfo 获取 basename 时,getBasename() 和 getFilename() 返回基本一致,但是在处理根目录下的文件名获取时表现稍有不同。
这里可以到官方文档中用户 提交的反馈 去详细了解一下。
4、获取文件的绝对路径
绝对路径由
realpath($path)
和
SplFileInfo::getRealpath()
获取。
5、获取文件类型
可以使用
filetype($filename)
和
SplFileInfo::getType()
来获取文件的类型。
返回值范围:
-
dir
-
file
-
char
-
fifo
-
block
-
link
-
unknown
可以查看 Linux 文件类型与扩展名 相关文件类型,这里我们重点关注下
dir
目录和
file
普通文件类型即可。
6、获取文件大小
可以使用
filesize(\$filename)
和
SplFileInfo::getSize()
来获取文件的大小,不再赘述。
7、 获取文件权限
可以使用
fileperms(\$filename)
和
SplFileInfo::getPerms()
来获取到文件的所属权限。
值得注意的是它们的返回值是十进制表示的权限,如果需要获取类似
0655
八进制权限表示法,我们需要对返回值进行处处理才行:
// @see http://php.net/manual/zh/function.fileperms.php#refsect1-function.fileperms-examples
$permissions = substr(sprintf("%o", fileperms($filename)), -4);
你可以通过 PHP: fileperms() values and convert these 了解更多关于 PHP 获取文件权限转换的更多细节。
基本上学习完这些文件元数据信息获取方法,差不多可以应对日常开发过程中的多数应用场景,尽管如此,还是建议仔细去阅读官方 文件系统函数,那里才是知识的源泉。
掌握文件的元数据,对我们了解文件的特性大有裨益,就好比两个人谈恋爱,懂得彼此才是最好的状态。
文件系统操作
可以说我们日常在处理文件的过程中,更多的是在操作文件或者目录(文件夹),本节我们将学习文件系统操作相关知识。
依据文件类型的不同我们可以简单的将操作分为:
-
对目录(dir)的操作
-
和对普通文件(file)的操作
目录操作使用场景
在处理目录时我们一般涉及如下处理:
-
创建目录
-
删除目录
-
打开目录
-
读取目录
-
关闭目录句柄
场景一:
我们有一套 CMS 管理系统支持文件上传处理,当目录不存在时依据文件上传时间,动态的创建文件存储目录,比如,我们依据
年/月/日(2018/01/01)
格式创建目录。这里就涉及到
目录创建
的处理。
场景二:
当然,文件上传完成了还不够,我们还需要读取各个目录下的所有文件。这里涉及
打开目录
、
读取目录
以及读取完成后
关闭目录句柄
。
有了相关概念和思路后,我们具体看看究竟 PHP 文件系统给我们提供了哪些方便处理目录的函数呢?
创建目录
在 PHP 文件系统扩展中同样给我们提供了处理 目录结构的系统函数。
其中创建一个新目录需要使用
mkdir($pathname [, $mode = 0777, $recursive = false])
函数。
-
$pathname 参数为待创建目录的路径
-
$mode 为创建目录时的访问权限,0777 意味着获取最大访问权限
-
$recursive 用于标识是否递归创建目录,默认 false 不会递归创建
请看一个示例:
$pathname = "/path/to/your/upload/file/2018/01/01";
$created = mkdir($pathname);
创建目录是不是特别的简单呢?
但是等等,我们在类 Unix 系统中满心欢喜的使用
mkdir
并采用
$mode=0777
权限来创建一个全新的目录,但为什么当我们进入到目录中看到的目录的权限却是
0755
呢?
umask 掩码
这里涉及到
umask
掩码的问题!
重点:原来我们在类 Unix 系统中创建新目录是给出的权限会默认减去当前系统的
umask
值,才是实际创建目录时的所属权限。
什么意思呢?
比如:
// 我们期望创建的文件权限
$mode = 0777;
// 当前系统中 umask 值
$umask = 0022;// 可以由 umask 命令查看当前系统 umask 值,默认是 0022
// 实际创建的文件权限
0777
- 0022
------
= 0755
现在我们来对之前的实例稍作修改,看看 PHP 如何创建目录时得到希望的系统权限吧:
$pathname = "/path/to/your/upload/file/2018/01/01";
// 将系统 umask 设置为 0,并取得当前 umask 值(比如默认 0022)
$umask = umask(0);
$created = mkdir($pathname, $mode = 0777);
// 将系统 umask 设置回原值
umask($umask);
有关 umask 函数说明可以查看官方手册。另外可以查看 Why can't PHP create a directory with 777 permissions? 这个问答了解更多细节。
目录遍历
面向过程的目录遍历提供两种解决方案:
-
通过 opendir、readdir 和 closedir 来遍历目录;
-
另一种是直接使用
scandir
遍历指定路径中的文件和目录。
目录遍历示例一,出自 官方文档:
php
$dir = "/etc/php5/";
// Open a known directory, and proceed to read its contents
if (is_dir($dir)) {
if ($dh = opendir($dir)) {
while (($file = readdir($dh)) !== false) {
echo "filename: $file : filetype: " . filetype($dir . $file) . "\n";
}
closedir($dh);
}
}
// 输出结构类似于:
// filename: . : filetype: dir
// filename: .. : filetype: dir
// filename: apache : filetype: dir
// filename: cgi : filetype: dir
// filename: cli : filetype: dir
?>
目录遍历示例二,出自 官方文档:
php
$dir = '/tmp';
$files1 = scandir($dir);
print_r($files1);
// 输出结构类似于:
// Array
// (
// [0] => .
// [1] => ..
// [2] => bar.php
// [3] => foo.txt
// [4] => somedir
// )
目录的操作处理大致就是在处理这两类问题,相比于普通文件的处理来讲简单很多,下一节我们会学习有关普通文件的处理,请大家做好战斗准备。
文件操作使用场景
可以说我们在处理文件系统时,绝大多数都是在处理一个普通文件,那么我们在操作文件时,我们究竟在做什么呢?
你可能已经想到了,没错我们多数时候就是在处理如下文件问题:
文件的读取和写入相对会复杂一些,所以这两部分的内容会在稍后详细讲解。先让我们看看其它几个常见文件处理。
创建空文件
创建空文件有两种方式:
这两个函数同其它文件系统函数使用大致相同,感兴趣的朋友可以阅读手册,这里不作展开。
删除文件
删除文件由 unlink($filename) 函数完成。
复制文件
复制文件由 copy($source, $dest) 函数完成,会将 $source 文件拷贝到 $dest 文件中。
如果需要移动文件(重命名)可以使用
rename($oldname, $newname)
完成这个处理。
以上都是相对简单的文件处理函数就不一一举例说明了。
接下来学习如何读取文件中的内容。依据二八原则,可以说我们百分之八十的时间都在处理文件写入和读取的处理,所以我们有必要理清如何对文件进行读取和写入。
读取文件
读取文件的标准流程是:
-
打开一个文件句柄;
-
使用文件读取函数读取文件;
-
判断是否到文件结尾,到结尾则结束读取,否则回到操作 2;
-
读取完成关闭句柄;
开始之前我们需要准备一个有数据的文件,比如
F:\php_workspace\php-code-kata\read.txt
,在看一个简单的文件读取示例:
php
// 这里为了贴合读取文件的标准流程,使用 do{} while 语句,你也可以修改成 while 语句。
$filename = "F:\\php_workspace\\php-code-kata\\read.txt";
// 1. 打开一个文件句柄;
$handle = fopen($filename, $mode = 'rb');
do {
// 2. 使用文件读取函数读取文件;
$content = fgetc($handle);
echo $content;
// 3. 判断是否到文件结尾,到结尾则结束读取,否则回到操作 2;
} while (!feof($handle));
// 4. 读取完成关闭句柄;
fclose($handle);
// 读取显示大致类似:
// hello world!
现在,我们来详细讲解一下上述代码做了什么处理吧:
-
使用 fopen($filename, $mode) 打开一个文件或 URL 句柄,供后续文件系统函数使用;
-
使用 fgetc($handle) 函数从文件句柄中读取一个字符;
-
使用 feof($handle) 判断文件句柄是否到文件的结尾处,否则继续读取文件;
-
当读取完成后使用 fclose($handle) 关闭打开的文件句柄,完成文件读取的所有操作。
总体来说,在读取文件时按照以上处理流程,基本上太容易出错的。不过即便如此,还是有些重点需要我们小心处理:
-
我们以什么模式打开一个文件句柄,示例中使用
$mode='rb'
r(read) 只读模式开个一个文件句柄(只读模式下不能对文件尽心写入)。另外还有几个常用模式可供使用:
-
在执行文件内容读取时除了逐字符读取(fgetc),要支持一下集中读取形式:
-
fgets($handle) 每次读取一行数据
-
fgetss($handle) 每次读取一行数据,并过来 HTML 标记
-
fgetcsv($handle) 读取 CSV 文件,每次读取一样并解析字段
-
fread($handle, $length) 每次从句柄中最多读取
$length
个字节。
-
处理可以从句柄中读取文件数据,PHP 还提供将整个文件读取的方法:
注意:读取文件操作时我们推荐使用
file
get
contents
。
到这里我们基本上就涵盖了文件读取的所有知识点,相信大家对文件读取已经有了一个比较系统的认知。
下面我们进入到文件写入处理中,看看文件写入的正确姿势。
读取写入
典型的文件写入流程基本上和文件读取流程一致:
-
打开一个文件句柄;
-
使用文件读取函数向文件中写入内容;
-
写入完成关闭句柄。
依据惯例我们来看一个简单的示例:
php
$filename = "F:\\php_workspace\\php-code-kata\\read.txt";
// 1. 打开一个文件句柄;
$handle = fopen($filename, $mode = 'ab');
// 2. 使用文件读取函数向文件中写入内容
fwrite($handle, "hello filesystem to write!\n");
// 3. 写入完成关闭句柄;
fclose($handle);
注意:这里我们以追加写入的模式
* $mode = 'ab'
* 写入文件内容。
文件写入就如同文件读取一样的简单,相信大家能够轻松掌握这方面的知识。然而,我们显示世界可能充满了荆棘,稍不留神可能就会深陷泥沼。比如: