相信大家在日常研發過程中,難免需要和各種文件糾纏不清。例如,開啟「.env」檔案並從中讀取設定資訊、把專案中的錯誤訊息寫入到日誌檔案或取得圖片的建立時間等等。在處理這些功能時,我們都需要使用到PHP檔案系統介面。
一 在什麼是檔案系統
開始之前,我們首先需要釐清我們所研究的問題領域,理解什麼是檔案系統,還有我們所研究的物件。
在電腦中,檔案系統(file system or filesystem)用於管理資料如何儲存和如何被取得的。 - 維基百科
簡單來說,就是我們應該如何管理我們的目錄(資料夾)和檔案。通常,我們將具有相似屬性的文件,儲存到同一個目錄中以便後續查找,這個常見的操作就會涉及到目錄和文件。
對於軟體工程師來講,一個非常典型的使用場景,就是在開發 MVC 專案時,將控制器、視圖和模型等模組的文件,儲存到不同的目錄結構中方便管理。
無論如何,我們依據不同特性劃分檔案和目錄都是為了解決檔案儲存和尋找的問題。
有了這些認知後,應該自然而然的想到我們目前研究的PHP 檔案系統(或者說檔案系統)其所研究的對象,簡單概括起來就是:
- #目錄(資料夾)
- 檔案
也就是說,本文我們所講解的PHP 檔案系統函數處理,基本上都是圍繞著目錄和檔案展開的。
二 深入 PHP 檔案系統
在 PHP 檔案系統中內建提供了超過 80 個可用的 檔案系統函數。由於數量繁多功能強大,自然本文無法將對所有的系統函數逐一講解。一來,時間過於倉促;再者,短時間內我們也沒有那麼多的精力將它們全部掌握。
另外,補充說明一點,PHP 標準函數函式庫不僅為我們提供了流程導向的檔案系統處理函數。同時,也封裝了常用目錄及檔案操作的物件導向介面與迭代器介面方便大家使用:
#SplFileInfo
- ##finfo
- DirectoryIterator
- RecursiveDirectoryIterator ##2.1 檔案系統的元資料
2.1. 1 什麼是元資料
元資料(meta data):通俗一點講就是「資料的資料」。拿一個 php 檔案來說它的元資料可以是 建立時間、檔案名稱、檔案大小 或 檔案所有權限 等,這類能夠顯示該檔案基本特徵的資料就是「元資料(meta data)」了。
2.1.2 常用元資料取得
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() 方法拿到。
虽然这里我们列出了很多的函数,但是基本上还是比较容易理解的,需要注意的是:
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
- 3.6 詳解PHP檔案系統完全指南图
另外需要注意的一点是在使用 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 获取文件权限转换的更多细节。
基本上学习完这些文件元数据信息获取方法,差不多可以应对日常开发过程中的多数应用场景,尽管如此,还是建议仔细去阅读官方 文件系统函数,那里才是知识的源泉。
掌握文件的元数据,对我们了解文件的特性大有裨益,就好比两个人谈恋爱,懂得彼此才是最好的状态。
2.2 文件系统操作
可以说我们日常在处理文件的过程中,更多的是在操作文件或者目录(文件夹),本节我们将学习文件系统操作相关知识。
依据文件类型的不同我们可以简单的将操作分为:
对目录(dir)的操作
和对普通文件(file)的操作
2.2.1 目录操作使用场景
在处理目录时我们一般涉及如下处理:
创建目录
删除目录
打开目录
读取目录
关闭目录句柄
- 场景一
我们有一套 CMS 管理系统支持文件上传处理,当目录不存在时依据文件上传时间,动态的创建文件存储目录,比如,我们依据 年/月/日(2018/01/01) 格式创建目录。这里就涉及到 目录创建 的处理。
- 场景二
当然,文件上传完成了还不够,我们还需要读取各个目录下的所有文件。这里涉及 打开目录、读取目录 以及读取完成后 关闭目录句柄。
有了相关概念和思路后,我们具体看看究竟 PHP 文件系统给我们提供了哪些方便处理目录的函数呢?
2.2.1.1 创建目录
在 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 掩码
这里涉及到 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? 这个问答了解更多细节。
2.2.1.2 目录遍历
面向过程的目录遍历提供两种解决方案:
通过 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 // )
目录的操作处理大致就是在处理这两类问题,相比于普通文件的处理来讲简单很多,下一节我们会学习有关普通文件的处理,请大家做好战斗准备。
2.2.2 文件操作使用场景
2.2.2.1 创建空文件
创建空文件有两种方式:
一是:以写入(w)模式使用 fopen($filename, $mode = 'wb') 打开一个文件,当文件不存在时则会创建一个新文件;
二是:使用 touch 函数创建一个新文件。
这两个函数同其它文件系统函数使用大致相同,感兴趣的朋友可以阅读手册,这里不作展开。
2.2.2.2 删除文件
删除文件由 unlink($filename) 函数完成。
2.2.2.3 复制文件
复制文件由 copy($source, $dest) 函数完成,会将 $source 文件拷贝到 $dest 文件中。
如果需要移动文件(重命名)可以使用 rename($oldname, $newname) 完成这个处理。
以上都是相对简单的文件处理函数就不一一举例说明了。
接下来学习如何读取文件中的内容。依据二八原则,可以说我们百分之八十的时间都在处理文件写入和读取的处理,所以我们有必要理清如何对文件进行读取和写入。
2.2.2.4 读取文件
读取文件的标准流程是:
打开一个文件句柄;
- 使用文件读取函数读取文件;
- 判断是否到文件结尾,到结尾则结束读取,否则回到操作 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) 只读模式开个一个文件句柄(只读模式下不能对文件尽心写入)。另外还有几个常用模式可供使用:
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。
到这里我们基本上就涵盖了文件读取的所有知识点,相信大家对文件读取已经有了一个比较系统的认知。
下面我们进入到文件写入处理中,看看文件写入的正确姿势。
2.2.2.5 读取写入
典型的文件写入流程基本上和文件读取流程一致:
打开一个文件句柄;
- 使用文件读取函数向文件中写入内容;
写入完成关闭句柄。
依据惯例我们来看一个简单的示例:
<?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 一样为我们内置了检测文件有效性的函数,规避各种错误。
2.2.2.5 如何处理文件权限及检测有效性
- 文件有效性检测
检测文件的有效性能够让我们规避常见的开发错误,比如:
当相文件中写入数据时,是不是需要检测它有可写的权限,并且它是不是一个文件而非文件夹?
读取文件内容时,是不是需要查看下我们能不能对其进行读取?
在安装项目时,我们是不是需要检测已经依据实例配置文件创建了实际的配置文件呢?
这些内容都需要使用到文件有效性检测相关知识。
判断文件是否可写我们有: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 来遍历指定路径中的文件和目录夹么?
现在我们将使用面向对象的接口来重新实现一个权限的目录遍历处理。
3 面向对象的目录遍历
使用面向对象的接口来遍历目录,是一个非常有意义的教程,这里我们所涉及使用的接口包括:
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));
4 PHP 文件系统思维导图
文件系统思维导图
推荐学习:php视频教程
以上是詳解PHP檔案系統完全指南的詳細內容。更多資訊請關注PHP中文網其他相關文章!

本文比較了酸和基本數據庫模型,詳細介紹了它們的特徵和適當的用例。酸優先確定數據完整性和一致性,適合財務和電子商務應用程序,而基礎則側重於可用性和

本文討論了確保PHP文件上傳的確保,以防止諸如代碼注入之類的漏洞。它專注於文件類型驗證,安全存儲和錯誤處理以增強應用程序安全性。

本文討論了在PHP中實施API速率限制的策略,包括諸如令牌桶和漏水桶等算法,以及使用Symfony/Rate-limimiter之類的庫。它還涵蓋監視,動態調整速率限制和手

本文討論了使用password_hash和pyspasswify在PHP中使用密碼的好處。主要論點是,這些功能通過自動鹽,強大的哈希算法和SECH來增強密碼保護

本文討論了OWASP在PHP和緩解策略中的十大漏洞。關鍵問題包括注射,驗證損壞和XSS,並提供用於監視和保護PHP應用程序的推薦工具。


熱AI工具

Undresser.AI Undress
人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover
用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool
免費脫衣圖片

Clothoff.io
AI脫衣器

AI Hentai Generator
免費產生 AI 無盡。

熱門文章

熱工具

SAP NetWeaver Server Adapter for Eclipse
將Eclipse與SAP NetWeaver應用伺服器整合。

Atom編輯器mac版下載
最受歡迎的的開源編輯器

ZendStudio 13.5.1 Mac
強大的PHP整合開發環境

VSCode Windows 64位元 下載
微軟推出的免費、功能強大的一款IDE編輯器

禪工作室 13.0.1
強大的PHP整合開發環境