這篇文章要跟大家介紹的內容是關於PHP檔案系統的詳細介紹,有著一定的參考價值,有需要的朋友可以參考一下。
今天我們將開啟一個新的探索旅程,深入到 PHP 檔案系統中,系統的學習和掌握 PHP 檔案系統的基本使用。
相信大家在日常研發過程中,難免需要和各種文件糾纏不清。例如,開啟 .env 檔案並從中讀取設定資訊、把專案中的錯誤訊息寫入到日誌檔案或取得圖片的建立時間等等。在處理這些功能時,我們都需要使用到 PHP 檔案系統介面。
以下是本文所涉主題的提綱:
一什麼是檔案系統
二深入PHP 檔案系統
三物件導向的目錄遍歷
四PHP 檔案系統心智圖
本文較長,耗時約20 分鐘,請做好戰鬥準備!
開始之前,我們首先需要釐清我們所研究的問題領域,理解什麼是檔案系統,還有我們所研究的物件。
在電腦中,檔案系統(file system or filesystem)用於管理資料如何儲存和如何被取得的。 - 維基百科
簡單來說,就是我們應該如何管理我們的目錄(資料夾)和檔案。通常,我們將具有相似屬性的文件,儲存到同一個目錄中以便後續查找,這個常見的操作就會涉及到目錄和文件。
對於軟體工程師來講,一個非常典型的使用場景,就是在開發MVC 專案時,將控制器、視圖和模型等模組的文件,儲存到不同的目錄結構中方便管理。
無論如何,我們依據不同特性劃分檔案和目錄都是為了解決檔案儲存和尋找的問題。
有了這些認知後,應該自然而然的想到我們目前研究的PHP 檔案系統(或者說檔案系統)其所研究的對象,簡單概括起來就是:
#目錄(資料夾)
檔案
#也就是說,本文我們所講解的PHP 檔案系統函數處理,基本上都是圍繞目錄和檔案展開的。
在 PHP 檔案系統中內建提供了超過 80 個可用的 檔案系統函數。由於數量繁多功能強大,自然本文無法將對所有的系統函數逐一講解。一來,時間過於倉促;再者,短時間內我們也沒有那麼多的精力將它們全部掌握。
儘管如此,大家也不必氣餒,本文會將有限的時間和精力,來研究以下幾個在文件處理時的常見話題:
文件的元資料應該如何取得
檔案的MIME 類型如何取得
#檔案與目錄的操作處理
檔案和目錄的權限管理
另外,補充說明一點,PHP 標準函數函式庫不僅為我們提供了流程導向的檔案系統處理函數。同時,也封裝了常用目錄及檔案操作的物件導向介面和迭代器介面方便大家使用:
#SplFileInfo
、檔案名稱、檔案大小 或檔案所有權限 等,這類能夠顯示該文件基本特徵的資料就是「元資料(meta data)」了。 2.1.2 常用元資料取得
注意 SplFileInfo 類別實例化時接收 $filename 檔案路徑作為參數,後續沒有特別說明預設我們已經取得了 SplFileInfo 實例才能進行 getMTime() 等類似處理。
// 文件路径请求改成你自己的文件路径 $filename = "f://filesystem/test.txt"; // 面向过程: 获取文件时间 $modifyTimestamp = filemtime($filename); // 面向对象 $file = new SplFileInfo($filename); $modifyTimestamp = $file->getMTime();
可以使用函数 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)](http://php.net/manual/zh/function.pathinfo.php)**、**[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)](http://php.net/manual/zh/function.pathinfo.php)**、**[basename($filename, $suffix)](http://php.net/manual/zh/function.basename.php)** 和 **[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)](http://php.net/manual/zh/function.pathinfo.php)**、 **[basename($filename)、SplFileInfo::getBasename() 和 SplFileInfo::getFilename() 方法拿到。
虽然这里我们列出了很多的函数,但是基本上还是比较容易理解的,需要注意的是:
pathinfo 可以获取所有文件相关的路径信息,如果指定第二个参数选项将仅获取该部分的信息
文件名和 basename 不是特别容易理解,你可以使用完全相同的方法或函数 basename 和 SplFileInfo::getBasename() 获取他们,区别在于是否摘除指定的 $suffix 后缀。
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 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 获取文件权限转换的更多细节。
基本上学习完这些文件元数据信息获取方法,差不多可以应对日常开发过程中的多数应用场景,尽管如此,还是建议仔细去阅读官方 文件系统函数,那里才是知识的源泉。
掌握文件的元数据,对我们了解文件的特性大有裨益,就好比两个人谈恋爱,懂得彼此才是最好的状态。
可以说我们日常在处理文件的过程中,更多的是在操作文件或者目录(文件夹),本节我们将学习文件系统操作相关知识。
依据文件类型的不同我们可以简单的将操作分为:
对目录(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) 完成这个处理。
以上都是相对简单的文件处理函数就不一一举例说明了。
接下来学习如何读取文件中的内容。依据二八原则,可以说我们百分之八十的时间都在处理文件写入和读取的处理,所以我们有必要理清如何对文件进行读取和写入。
读取文件的标准流程是:
打开一个文件句柄;
使用文件读取函数读取文件;
判断是否到文件结尾,到结尾则结束读取,否则回到操作 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。
到这里我们基本上就涵盖了文件读取的所有知识点,相信大家对文件读取已经有了一个比较系统的认知。
下面我们进入到文件写入处理中,看看文件写入的正确姿势。
典型的文件写入流程基本上和文件读取流程一致:
打开一个文件句柄;
使用文件读取函数向文件中写入内容;
写入完成关闭句柄。
依据惯例我们来看一个简单的示例:
<?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)](http://php.net/manual/zh/function.is-dir.php) 和 **[SplFileInfo::isDir()](http://php.net/manual/zh/splfileinfo.isdir.php)**;文件判断:[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檔案系統的詳細介紹的詳細內容。更多資訊請關注PHP中文網其他相關文章!