Home >Backend Development >PHP Problem >A complete guide to PHP file systems
I believe that in the daily research and development process, everyone will inevitably need to get entangled with various documents. For example, open the ".env" file and read the configuration information from it, write the error information in the project to the log file, or get the creation time of the image, etc. When dealing with these functions, we all need to use the PHP file system interface.
Before we begin, we first need to clarify the problem area we are studying, understand what a file system is, and what we are studying.
In computers, the file system (file system or filesystem) is used to manage how data is stored and retrieved. - Wikipedia
Simply put, it is how we should manage our directories (folders) and files. Usually, we store files with similar attributes in the same directory for subsequent search. This common operation involves directories and files.
For software engineers, a very typical usage scenario is to store the files of controllers, views, models and other modules in different directory structures for easy management when developing MVC projects.
In any case, we divide files and directories according to different characteristics to solve the problem of file storage and search.
After having these understandings, we should naturally think of the object of study of the PHP file system (or file system) we are currently studying. A simple summary is:
In other words, the PHP file system function processing we explain in this article basically revolves around directories and files.
The PHP file system provides more than 80 available file system functions built-in. Due to the large number of powerful functions, naturally this article cannot explain all the system functions one by one. Firstly, time is too short; secondly, we don’t have that much energy to master them all in a short period of time.
In addition, as a supplementary note, the PHP standard function library not only provides us with process-oriented file system processing functions. At the same time, it also encapsulates the object-oriented interface and iterator interface for common directory and file operations for everyone to use:
SplFileInfo
finfo
DirectoryIterator
RecursiveDirectoryIterator
2. Get the last access time of the fileYou can use the function fileatile($filename) or the SplFileInfo::getATime() method to get the last access timestamp of the file.// 文件路径请求改成你自己的文件路径 $filename = "f://filesystem/test.txt"; // 面向过程: 获取文件时间 $modifyTimestamp = filemtime($filename); // 面向对象 $file = new SplFileInfo($filename); $modifyTimestamp = $file->getMTime();
// 文件路径请求改成你自己的文件路径 $filename = "f://filesystem/test.txt"; // 面向过程: 获取文件时间 $accessTimestamp = fileatime($filename); // 面向对象 $file = new SplFileInfo($filename); $accessTimestamp = $file->getATime();In addition to filemtile and fileatime, there is also filectime to get the inode modification time of the file (can be considered as the creation time). These are the commonly used time-related functions. To make it easier to remember, let’s take a look at how they are named:
$filename = 'F:\Program Files\SSH Communications Security\SSH Secure Shell\Output.txt';will obtain the directory information of F:\Program Files\SSH Communications Security\SSH Secure Shell.
需要取得文件名信息,我们可以使用 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 这部分文件名信息。
扩展名我们可以使用 pathinfo($filename, PATHINFO_EXTENSION) 和 SplFileInfo::getExtension() 方法拿到。
基于前面的了解,我们可以获取到 txt 这部分扩展信息,这里不再赘述。
basename 指的是 文件名 + 扩展名 内容信息,可以使用 pathinfo($filename, PATHINFO_BASENAME)、 basename($filename)、SplFileInfo::getBasename() 和 SplFileInfo::getFilename() 方法拿到。
虽然这里我们列出了很多的函数,但是基本上还是比较容易理解的,需要注意的是:
pathinfo 可以获取所有文件相关的路径信息,如果指定第二个参数选项将仅获取该部分的信息
文件名和 basename 不是特别容易理解,你可以使用完全相同的方法或函数 basename 和 SplFileInfo::getBasename() 获取他们,区别在于是否摘除指定的 $suffix 后缀。
3.5 示例
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
另外需要注意的一点是在使用 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])](http://php.net/manual/zh/func... 函数。
$pathname 参数为待创建目录的路径
$mode 为创建目录时的访问权限,0777 意味着获取最大访问权限
$recursive 用于标识是否递归创建目录,默认 false 不会递归创建
请看一个示例:
$pathname = "/path/to/your/upload/file/2018/01/01"; $created = mkdir($pathname);
创建目录是不是特别的简单呢?
但是等等,我们在类 Unix 系统中满心欢喜的使用 mkdir 并采用 $mode=0777 权限来创建一个全新的目录,但为什么当我们进入到目录中看到的目录的权限却是 0755 呢?
这里涉及到 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 // )
目录的操作处理大致就是在处理这两类问题,相比于普通文件的处理来讲简单很多,下一节我们会学习有关普通文件的处理,请大家做好战斗准备。
创建空文件有两种方式:
一是:以写入(w)模式使用 fopen($filename, $mode = 'wb') 打开一个文件,当文件不存在时则会创建一个新文件;
二是:使用 touch 函数创建一个新文件。
这两个函数同其它文件系统函数使用大致相同,感兴趣的朋友可以阅读手册,这里不作展开。
删除文件由 unlink($filename) 函数完成。
复制文件由 copy($source, $dest) 函数完成,会将 $source 文件拷贝到 $dest 文件中。
如果需要移动文件(重命名)可以使用 rename($oldname, $newname) 完成这个处理。
以上都是相对简单的文件处理函数就不一一举例说明了。
接下来学习如何读取文件中的内容。依据二八原则,可以说我们百分之八十的时间都在处理文件写入和读取的处理,所以我们有必要理清如何对文件进行读取和写入。
读取文件的标准流程是:
打开一个文件句柄;
读取完成关闭句柄;
开始之前我们需要准备一个有数据的文件,比如 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) 只读模式开个一个文件句柄(只读模式下不能对文件尽心写入)。另外还有几个常用模式可供使用:
r+ 读写模式
w(write) 覆盖写入
w+ 覆盖读写
a(append) 追加写入
a+ 追加读写
b 重点关注此模式,为增强项目可移植和健壮性,推荐所有模式添加「b」模式强制使用二进制模式
有关所有可用模式的说明可以从 模式 手册中查找。
在执行文件内容读取时除了逐字符读取(fgetc),要支持一下集中读取形式:
fgets($handle) 每次读取一行数据
fgetss($handle) 每次读取一行数据,并过来 HTML 标记
fgetcsv($handle) 读取 CSV 文件,每次读取一样并解析字段
fread($handle, $length) 每次从句柄中最多读取 $length 个字节。
处理可以从句柄中读取文件数据,PHP 还提供将整个文件读取的方法:
file($filename) 把整个文件读入一个数组中
file_get_contents($filename) 将整个文件读入一个字符串
注意: 读取文件操作时我们推荐使用 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' 写入文件内容。
文件写入就如同文件读取一样的简单,相信大家能够轻松掌握这方面的知识。然而,我们显示世界可能充满了荆棘,稍不留神可能就会深陷泥沼。比如:
我在写入文件时,同时其他人也在对同一个文件进行写入,怎么办?我们可以使用 flock($handle, LOCK_EX) 加锁函数进行独占写入。
每次都需要 打开文件、写入、再关闭 是在麻烦!有没有更简单的方式写文件呢?PHP 同样为你考虑到了这点,所以提供了 [file_put_contents($filename, $data [, LOCK_EX])](http://php.net/manual/zh/func... 将一个字符串写入文件,同样的它也支持独占写入。
到这里,我们基本上就学习完 PHP 文件系统中大多数常用的函数了。然而就如我所说的那样,现实世界总是残酷的。尤其是在读写文件时,经常会遇到各种各样的错误,我们应该如何才能避免呢?
嗯,PHP 一样为我们内置了检测文件有效性的函数,规避各种错误。
检测文件的有效性能够让我们规避常见的开发错误,比如:
当相文件中写入数据时,是不是需要检测它有可写的权限,并且它是不是一个文件而非文件夹?
读取文件内容时,是不是需要查看下我们能不能对其进行读取?
在安装项目时,我们是不是需要检测已经依据实例配置文件创建了实际的配置文件呢?
这些内容都需要使用到文件有效性检测相关知识。
判断文件是否可写我们有:is_writable($filename) 和 SplFileInfo::isWritable()。
路径目录判断:is_dir($filename) 和 SplFileInfo::isDir();文件判断:is_file($filename) 和 **SplFileInfo::isFile()。
检测文件或目录是否已经创建过,我们使用 file_exists($filename) 函数完成。
当我们能够正确的检测文件是否存在时,我们还需要面对的问题时,如果我们的文件当前用户 不可写入,我们应该如何修改权限使其可写呢?
这里就涉及修改文件权限操作,之前我们在创建目录是已经接触过 umask 掩码相关知识。这里我们将讲解那些已经创建的文件权限变更的方法。
通常,我们会使用 chmod($filename, $mode) 去修改一个文件的权限。
另外,还可以关注以下几个权限相关的处理函数:
chgrp($filename, $group) 改变文件所属的组
chown($filename, $user) 改变文件的所有者
以及,之前提到过的 umask 修改掩码函数。
文章进行到这里,其实基本上 PHP 文件系统的所有知识都已经涉及到了。那么,下回见吧?不不不...
为了应对实战(面试需要),我们可能需要进一步对目录遍历做更进一步的研究。还记得我们之前使用过 scandir 来遍历指定路径中的文件和目录夹么?
现在我们将使用面向对象的接口来重新实现一个权限的目录遍历处理。
使用面向对象的接口来遍历目录,是一个非常有意义的教程,这里我们所涉及使用的接口包括:
DirectoryIterator 创建非递归的目录迭代器
RecursiveDirectoryIterator 创建递归的目录迭代器
RecursiveIteratorIterator 创建一个递归迭代器的迭代器(用于迭代获取 RecursiveIteratorIterator 示例)
话不多说,我们看下如何创建一个功能强大的支持递归迭代的目录迭代程序:
/** * 目录扫描 * * @method listContents($path, $recursive = false) 获取目录中所有文件及文件夹 */ class DirectoryScanner { /** * 获取目录中所有文件及文件夹 * * @param $path 目录 * @param $recursive 递归获取 * * @return array */ public static function listContents($path, $recursive = false) { $iter = $recursive ? static::getRecursiveDirectoryIterator($path) : static::getDirectoryIterator($path); $result = []; foreach ($iter as $file) { if (in_array($file->getFilename(), ['.', '..'])) { continue; } $result[] = clone $file; } return $result; } /** * 获取目录迭代器 * * @param $path 目录 * * @return DirectoryIterator::class */ public static function getDirectoryIterator($path) { return new DirectoryIterator($path); } /** * 获取递归目录迭代器 * * @param $path 目录 * @param $mode 遍历模式: RecursiveIteratorIterator::SELF_FIRST 从当前目录开始遍历;RecursiveIteratorIterator::CHILD_FIRST 从子目录开始遍历 * * @return RecursiveIteratorIterator::class */ public static function getRecursiveDirectoryIterator($path, $mode = RecursiveIteratorIterator::SELF_FIRST ) { return new RecursiveIteratorIterator( new RecursiveDirectoryIterator($path, FilesystemIterator::SKIP_DOTS), $mode ); } } $path = 'F:\php_workspace\php-code-kata\direcotry-iterator\dir'; var_dump(DirectoryScanner::listContents($path)); var_dump(DirectoryScanner::listContents($path, true));
文件系统思维导图
推荐学习:php视频教程
The above is the detailed content of A complete guide to PHP file systems. For more information, please follow other related articles on the PHP Chinese website!