同福

PHP创建、删除、授权文件夹和读写文件的方法V1.2【20210402】

介绍

介绍

今天福哥带着大家来学习PHP读写文件的方法,虽然现阶段做项目需要用到文件操作的情况不多了,但是免不了在特殊情况下还是需要用到这个技术的。

今天福哥还会给大家讲解PHP创建、删除、授权文件夹的方法,这个技术在提供用户上传功能的项目当中是非常常见的,通过动态建立文件夹将用户上传的文件分布式地保存在不同文件夹下面,避免单个文件夹下面的文件数量过多造成系统故障。

PHP创建、删除、授权文件夹以及读写文件是依靠内建的函数实现的,PHP提供了多种读写文件的函数,我们来逐个介绍一下。

文件夹操作就是目录操作,在Windows系统里面文件夹叫folder,翻译过来就是文件夹,在Linux系统里面文件夹叫directory,翻译过来就是目录。所以创建、删除、授权文件夹就是创建、删除、授权目录。

基本原则

读写文件有一些常识需要大家先了解一下。

  • 读写文件可以是本地电脑上面的文件,也可以是远程网络上面的文件,只要授权了就可以操作。

  • 文件夹操作可以是本地电脑上面的文件夹,也可以是远程网络上面的文件夹,只要授权了就可以操作。

  • 要创建文件需要对创建文件的文件夹有写权限。

  • 读写已经存在的文件只需要对文件有权限。

  • 文件内容分为普通模式和二进制模式,普通模式通过字符串操作,二进制模式通过字节操作。

  • 写文件分为重置写入和追加写入,前者会清空已有内容,后者不会。

  • 通过文件指针可以精确控制读写文件内容的具体位置,但是写入只会覆盖已有内容而不会像编辑器一样插入内容。

  • 当前文件夹通过“.”表示,上一级文件夹通过“..”表示。

  • 任何文件夹都会有“当前文件夹”和“上一级文件夹”。

文件夹

遍历

遍历文件夹用到opendir、readdir、closedir几个函数,可以通过is_dir、is_file判断文件夹下面的项目是文件还是文件夹,可以通过is_readable判断文件/文件夹是否可读,可以通过is_writable判断文件/文件夹是否可写。

$currDir = __DIR__;

$do = opendir($currDir);
if($do) {
    while ($fi = readdir($do)) {
        if($fi != "." && $fi != ".."){
            $fullPath = __DIR__. "/". $fi;
            $r = (is_readable($fullPath)) ? "可读" : "不可读";
            $w = (is_writable($fullPath)) ? "可写" : "不可写";
            if(is_file($fullPath)){
                echo "<span style='color: gray;'>文件:[". $r. "][". $w. "]". $fullPath. "</span><br />";
            }
            else if(is_dir($fullPath)){
                echo "<span style='color: blue;'>文件夹:[". $r. "][". $w. "]". $fullPath. "</span><br />";
            }
        }
    }
    closedir($do);
}

home/topic/2021/0403/06/c424c5ea3cfe05d81157f3751100c649.jpg

递归遍历

如果需要遍历一个文件夹下面的所有子级文件和文件夹需要用到递归遍历,就是无论是子级还孙级都要遍历出来。

递归遍历很简单,只要在普通遍历的基础之上稍加改动即可实现,首先需要将普通遍历程序封装为一个方法,然后在普通遍历到的项目为文件夹的时候调用一下方法自己,就可以了。

private function listFolder(string $folder){
    $do = opendir($folder);
    if($do) {
        while ($fi = readdir($do)) {
            if($fi != "." && $fi != ".."){
                $fullPath = $folder. "/". $fi;
                $r = (is_readable($fullPath)) ? "可读" : "不可读";
                $w = (is_writable($fullPath)) ? "可写" : "不可写";
                if(is_file($fullPath)){
                    echo "<span style='color: gray;'>文件:[". $r. "][". $w. "]". $fullPath. "</span><br />";
                }
                else if(is_dir($fullPath)){
                    echo "<span style='color: blue;'>文件夹:[". $r. "][". $w. "]". $fullPath. "</span><br />";
                    // 递归遍历关键,自己调用自己
                    $this->listFolder($fullPath);
                }
            }
        }
        closedir($do);
    }
}

protected function user_process(){
    $currDir = __DIR__;
    
    $this->listFolder($currDir);
}

创建

创建文件夹使用mkdir函数,创建之后可以通过chmod授权,通过chown设置角色。

$currDir = __DIR__;
$newDir = $currDir. "/newFolder";

$ret = mkdir($newDir);
var_dump($ret);
if($ret){
    chmod($newDir, 0755);
    chown($newDir, "tfums", "tfums");
}

删除

删除文件夹使用rmdir函数。

$currDir = __DIR__;
$newDir = $currDir. "/newFolder";

$ret = rmdir($newDir);
var_dump($ret);

递归删除

如果文件夹含有子级文件/文件夹则需要先删除子级项目才能删除文件夹,这个时候就需要用到递归遍历技术了。

private function removeFolder(string $folder){
    $do = opendir($folder);
    if($do) {
        while ($fi = readdir($do)) {
            if($fi != "." && $fi != ".."){
                $fullPath = $folder. "/". $fi;
                if(is_file($fullPath)){
                    // 删除文件夹下面的文件
                    unlink($fullPath);
                }
                else if(is_dir($fullPath)){
                    $this->removeFolder($fullPath);
                }
            }
        }
        closedir($do);
    }
    // 删除清空后的文件夹
    rmdir($folder);
}

protected function user_process(){
    $currDir = __DIR__;
    $newDir = $currDir. "/newFolder";

    $this->removeFolder($newDir);
}

文件

创建

创建文件可以有两种方式,最简单的是通过file_put_contents直接创建文件并写入内容,除此之外还有标准方式通过fopen、fclose实现。

file_put_contents

使用file_put_contents创建文件非常简单,返回值为写入文件内容长度。

$currDir = __DIR__;
$newFile = $currDir. "/newFile.php";

$writeLen = file_put_contents($newFile, "这是福哥新创建的文件");

fopen

使用fopen创建文件需要了解更多知识,首先我们需要指定“文件打开方式”这个概念,使用fopen实际上知识打开了一个文件,需要我们指定一个打开文件想要干什么的模式,这个就是文件打开方式。

文件打开方式包括:

  • r,读模式

  • r+,可读可写模式,文件指针在文件开头

  • w,写模式,重置文件内容

  • w+,可读可写模式,重置文件内容

  • a,写模式,文件指针在文件末尾

  • a+,可读可写模式,文件指针在文件末尾

$currDir = __DIR__;
$newFile = $currDir. "/newFile.php";

$fo = fopen($newFile, "w");
if($fo){
    $writeLen = fwrite($fo, "这是福哥新创建的文件");
    fclose($fo);   
}

删除

删除文件使用unlink函数。

$currDir = __DIR__;
$newFile = $currDir. "/newFile.php";

unlink($newFile);

读文件

读文件有两种方式,简单方式和标准方式。

file_get_contents

使用file_get_contents可以简单的将文件内容读取出来。

$currDir = __DIR__;
$newFile = $currDir. "/newFile.php";

$fc = file_get_contents($newFile);

var_dump($fc);

home/topic/2021/0403/06/2d4745a110661e38b38fdef998d19d07.jpg

fopen

使用fopen读文件需要用到while语句配合feof函数通过fgets循环读取。

$currDir = __DIR__;
$newFile = $currDir. "/newFile.php";

$fc = "";
$fo = fopen($newFile, "r");
if($fo){
    while(!feof($fo)){
        $fc .= fgets($fo);
    }
    fclose($fo);
}

var_dump($fc);

home/topic/2021/0403/06/2d4745a110661e38b38fdef998d19d07.jpg

写文件

写文件也有两种方式,简单方式和标准方式

file_put_contents

使用file_put_contents可以简单的将内容写入文件当中。

$currDir = __DIR__;
$newFile = $currDir. "/newFile.php";

file_put_contents($newFile, "福哥说这是全部内容了");

$fc = file_get_contents($newFile);

var_dump($fc);

home/topic/2021/0403/06/7189ee8dee00e5bc66331c8c5a9d94d4.jpg

fopen

使用fopen写文件需要用到fwrite函数。

$currDir = __DIR__;
$newFile = $currDir. "/newFile.php";

$fo = fopen($newFile, "w");
if($fo){
    fwrite($fo, "福哥说这是全部内容了");
    fclose($fo);
}

$fc = "";
$fo = fopen($newFile, "r");
if($fo){
    while(!feof($fo)){
        $fc .= fgets($fo);
    }
    fclose($fo);
}

var_dump($fc);

home/topic/2021/0403/06/7189ee8dee00e5bc66331c8c5a9d94d4.jpg

追加写文件

有时候我们不想将文件内容全部重置了,需要在文件现有内容后面追加内容怎么办?

file_put_contents

使用FILE_APPEND标识开启追加模式。

$currDir = __DIR__;
$newFile = $currDir. "/newFile.php";

file_put_contents($newFile, "福哥又说话了", FILE_APPEND);

$fc = file_get_contents($newFile);

var_dump($fc);

home/topic/2021/0403/06/85b2ebeb321ee3320b8c6a32a10cd8a0.jpg

fopen

将文件打开方式设置为追加写模式。

$currDir = __DIR__;
$newFile = $currDir. "/newFile.php";

$fo = fopen($newFile, "a");
if($fo){
    fwrite($fo, "福哥又说话了");
    fclose($fo);
}

$fc = "";
$fo = fopen($newFile, "r");
if($fo){
    while(!feof($fo)){
        $fc .= fgets($fo);
    }
    fclose($fo);
}

var_dump($fc);

home/topic/2021/0403/06/85b2ebeb321ee3320b8c6a32a10cd8a0.jpg

文件指针

我们以可读可写模式打开文件后,通过fseek移动文件指针,在指定位置进行读写操作,这种方式可以避免将文件全部内容加载到内存当中就可以完成很多情况的读写操作,可以解决针对超大文件内容的格式化数据的编辑的难题。

文件数据库

举例:我们制作一个文件数据库,将用户名和密码存储到一个文件里面,用户名和密码格式如下

[用户名(最长50个字符)][密码(取MD5哈希串,所以固定32个字符)][换行]

然后我们只要保证每一组用户信息都是一样的固定长度就可以通过简单计算知道某一个用户ID对应的用户信息在文件的哪个位置上面,进而通过fseek就可以快速定位并读取用户信息了。

home/topic/2021/0403/17/377b294e8b26cfe1f5c0d8d7c138a757.jpg

因为我们规定了文件内的用户数据格式,那么通过操控文件指针就可以实现这个文本数据库的增、删、改、查功能了,是不是觉得很神奇?

文本数据库定义

这是文本数据库TFSimpleTextDb的定义。

class TFSimpleTextDb{
    private string $fp;
    private $fo;
    private string $fs;
    private int $lastUserId;
    private int $lastLoginUserId;

    public function conn():bool {
        $this->fp = __DIR__. "/users.tongfu.net.txt";

        // 数据库文件不存在就创建它
        if(!file_exists($this->fp)){
            file_put_contents($this->fp, "");
        }

        // 打开数据库文件
        $this->fo = fopen($this->fp, "r+");

        // 记得当前文件尺寸
        $this->fs = filesize($this->fp);

        return ($this->fo != null);
    }

    public function add(string $user, string $pwd):bool {
        if(strlen($user) > 50){

            return false;
        }

        // 移动文件指针到文件末尾
        fseek($this->fo, $this->fs);

        // 写入一行数据保存用户名和密码
        // 用户名用strpad补位,保证数据结构一致
        $writeLen = fwrite($this->fo, sprintf("%s%s\n",
            str_pad($user, 50, ' ', STR_PAD_RIGHT),
            md5($pwd)
        ));
        $this->fs += $writeLen;

        // 立即写入新内容到磁盘
        fflush($this->fo);

        // 新用户ID就是最新一行的行号
        $this->lastUserId = intval($this->fs/(50+32+1));

        return true;
    }

    public function mod(int $userId, string $pwd):bool {
        // 移动文件指针到指定userId对应的用户信息位置
        fseek($this->fo, (50+32+1)*($userId-1));

        // 按约定数据长度读取数据栏位上的数据
        $userInDb = trim(fread($this->fo, 50));
        $pwdInDb = fread($this->fo, 32);
        if($userInDb == ""){

            return false;
        }

        // 修改密码
        // 后退32个字符,在密码栏位开始处开始写
        fseek($this->fo, ftell($this->fo)-32);
        fwrite($this->fo, md5($pwd));

        // 立即写入新内容到磁盘
        fflush($this->fo);

        return true;
    }

    public function del(int $userId):bool {
        // 移动文件指针到指定userId对应的用户信息位置
        fseek($this->fo, (50+32+1)*($userId-1));

        // 按约定数据长度读取数据栏位上的数据
        $userInDb = trim(fread($this->fo, 50));
        $pwdInDb = fread($this->fo, 32);
        if($userInDb == ""){

            return false;
        }

        // 修改密码
        // 后退82个字符,在用户名栏位开始处开始写
        // 写入82个空字符表示用户已经被删除了
        fseek($this->fo, ftell($this->fo)-82);
        fwrite($this->fo, str_repeat(" ", 82));

        // 立即写入新内容到磁盘
        fflush($this->fo);

        return true;
    }

    public function login(string $user, string $pwd=""):int {
        $fo = fopen($this->fp, "r");
        if($fo){
            while(!feof($fo)){
                // 解析出用户名和密码
                $userInDb = trim(fread($fo, 50));
                $pwdInDb = fread($fo, 32);
                fread($fo, 1);

                // 验证用户名
                if($user == $userInDb){
                    // 验证密码
                    if(md5($pwd) == $pwdInDb){
                        // 计算登录用户ID
                        $this->lastLoginUserId = intval(ftell($fo)/(50+32+1));

                        return 0;
                    }
                    else{

                        return 1; // 密码错误
                    }
                }
            }
            fclose($fo);
        }

        return 2; // 用户名不存在
    }

    public function close(){
        fclose($this->fo);
    }

    public function getLastUserId():int {

        return $this->lastUserId;
    }

    public function getLastLoginUserId():int {

        return $this->lastLoginUserId;
    }
}

文本数据库测试

下面是对这个TFSimpleTextDb的测试代码。

$myTFSimpleTextDb = new TFSimpleTextDb();

$myTFSimpleTextDb->conn();

if($myTFSimpleTextDb->login("福哥") == 2){
    echo ("<h3>创建用户福哥</h3>");
    $myTFSimpleTextDb->add("福哥", "123456");
    var_dump($myTFSimpleTextDb->getLastUserId());
}
if($myTFSimpleTextDb->login("鬼谷子叔叔") == 2) {
    echo ("<h3>创建用户鬼谷子叔叔</h3>");
    $myTFSimpleTextDb->add("鬼谷子叔叔", "abcdef");
    var_dump($myTFSimpleTextDb->getLastUserId());
}
if($myTFSimpleTextDb->login("同福大哥") == 2) {
    echo ("<h3>创建用户同福大哥</h3>");
    $myTFSimpleTextDb->add("同福大哥", "123456");
    var_dump($myTFSimpleTextDb->getLastUserId());
}
echo ("<h3>修改鬼谷子叔叔的密码</h3>");
var_dump($myTFSimpleTextDb->mod(2, "123456"));
echo ("<h3>使用新密码登录鬼谷子叔叔</h3>");
var_dump($myTFSimpleTextDb->login("鬼谷子叔叔", "123456"));
var_dump($myTFSimpleTextDb->getLastLoginUserId());
echo ("<h3>删除鬼谷子叔叔</h3>");
var_dump($myTFSimpleTextDb->del(2));
echo ("<h3>再次登录鬼谷子叔叔</h3>");
var_dump($myTFSimpleTextDb->login("鬼谷子叔叔", "123456"));

$myTFSimpleTextDb->close();

home/topic/2021/0403/17/e36adf15fd8c83a4eb652c21b1b774c5.jpg

标准方式的意义

学完前面的知识后会有一个疑问,既然file_get_contents和file_put_contents那么简单又可以达到目的,为什么我们还要学习fopen呢?

这是因为fopen的文件指针功能是file_get_contents/file_put_contents无法实现的,而在一些特殊情况下必须通过文件指针来解决读文件、写文件的需求。

将一个10G文件里的“福哥”换成“鬼谷子叔叔”

这个巨大的文件如果我们要通过file_get_contents和file_put_contents操作,系统就直接瘫痪了。。。

怎么办?

这个时候我们可以通过开启两个文件指针,一个文件指针负责读现有文件,一个文件指针负责写新的临时文件,完成后删除现有文件,再将临时文件重命名为原来的文件,就搞定了~~

$currDir = __DIR__;
$newFile = $currDir. "/newFile.php";
$tmpFile = $currDir. "/newFile.php.tmp";

$foNew = fopen($newFile, "r");
$foTmp = fopen($tmpFile, "w");
if($foNew && $foTmp){
    while(!feof($foNew)){
        $tmpLine = fgets($foNew);
        $tmpLine = str_replace("福哥", "鬼谷子叔叔", $tmpLine);
        fwrite($foTmp, $tmpLine);
    }
    fclose($foNew);
    fclose($foTmp);
    unlink($foNew);
    rename($tmpFile, $newFile);
}

var_dump(file_get_contents($newFile));

home/topic/2021/0403/07/8ac1c4043ad05b03d0adcba3ecd7c9b6.jpg友情提示:福哥的例子是一个小文件所以最后用file_get_contents打印文件内容给大家看了,如果真的是巨大的文件千千万万不要用file_get_contents函数去读取内容哦~~

总结

好了,今天童鞋们跟着福哥系统地将PHP语言操作文件夹、操作文件的方法学习了一遍,有了这些技术之后,今后在项目当中处理各种有关文件夹/文件的问题就不会发怵了!

要注意一点哦,文件夹操作、文件操作属于IO操作,是有一定风险的,一定不要把文件夹/文件的路径搞错了,要不把系统文件或者程序文件写坏了,系统有可能就完蛋了~~