1.6 文件与目录操作:站内仓库的存取 本节摘要:文件系统是月台旁边的站内仓库:日志、缓存、上传件、配置片段都寄存在这里。本节讲两类读写姿势(整存整取与逐行流水)、目录的创建与遍历、上传文件的落盘前置知识,重点落在并发写入与路径安全两个工程要点上,并以"带锁追加日志"实战收尾。数据库接管核心存取之前(见 2.2),仓库是服务端唯一可持久化的家当。 仓库的规矩:先想清楚谁在写 内存里的变量随请求结束而消失,要留下痕迹就得落盘。落盘之前先立三条规矩:其一,写之前想清楚会不会有别人同时写——多个请求并发追加同一天日志,不加锁就会互相穿插踩脏行;其二,路径要么来自服务器自己的配置,要么经过严格校验,绝不能把用户提交的内容直接拼进路径,否则等于把仓库钥匙交给旅客;
本节摘要:文件系统是月台旁边的站内仓库:日志、缓存、上传件、配置片段都寄存在这里。本节讲两类读写姿势(整存整取与逐行流水)、目录的创建与遍历、上传文件的落盘前置知识,重点落在并发写入与路径安全两个工程要点上,并以"带锁追加日志"实战收尾。数据库接管核心存取之前(见 2.2),仓库是服务端唯一可持久化的家当。
内存里的变量随请求结束而消失,要留下痕迹就得落盘。落盘之前先立三条规矩:其一,写之前想清楚会不会有别人同时写——多个请求并发追加同一天日志,不加锁就会互相穿插踩脏行;其二,路径要么来自服务器自己的配置,要么经过严格校验,绝不能把用户提交的内容直接拼进路径,否则等于把仓库钥匙交给旅客;其三,区分"整存整取"和"流水登记"两类作业,工具函数不同。
整存整取适合配置、缓存片段这类"一次性拿下、一次性写回"的内容:
<?php $configFile = __DIR__ . '/station.json'; // 整取:一次读入内存 $config = json_decode(file_get_contents($configFile), true) ?? []; echo $config['open_time'] ?? '未设置', "\n"; // 整存:一次写回(LOCK_EX 加独占锁,防并发写坏文件) file_put_contents($configFile, json_encode($config, JSON_UNESCAPED_UNICODE), LOCK_EX);
__DIR__ 表示当前脚本所在目录,用它拼路径可以避免"换个目录执行就找不到文件"的问题。JSON_UNESCAPED_UNICODE 让中文不被转成转义序列,日志可读性大增。
大文件或追加型内容用句柄式读写,逐行处理不占爆内存:
<?php $logFile = __DIR__ . '/logs/arrivals.log'; // 追加一行:a 模式,文件不存在会自动创建 $handle = fopen($logFile, 'a'); if ($handle === false) { exit('仓库通道打不开,检查目录权限'); } flock($handle, LOCK_EX); // 排队上锁 fwrite($handle, date('H:i:s') . " 一批旅客进站\n"); flock($handle, LOCK_UN); // 释放 fclose($handle); // 逐行读回:适合按天切割的大日志 $read = fopen($logFile, 'r'); while (($line = fgets($read)) !== false) { echo $line; } fclose($read);
flock 的排队逻辑值得念一遍:写者 A 上锁,写者 B 的 flock 会等待,A 写完释放,B 才动笔——行不会互相插穿。文件不存在自动创建是 a 模式的便利,但目录不存在会直接失败,所以写日志前先确认目录在不在。
目录操作三板斧:建、查、删。配合目录遍历,可以做出"按天分目录存日志"这类常见布局:
<?php $day = date('Ymd'); $dir = __DIR__ . "/logs/{$day}"; if (!is_dir($dir)) { mkdir($dir, 0755, true); // 第三个参数 true:父目录不存在一并创建 } // 清点货架:列出目录内容(返回 . 与 .. 两项,要过滤) $shelves = scandir($dir); foreach ($shelves as $shelf) { if ($shelf === '.' || $shelf === '..') continue; echo $shelf, "\n"; }

目录权限是 Linux 部署的高频坑:PHP 进程对库房目录没有写权限时,一切写入静默失败(函数返回 false 而不是报错弹窗)。所以写文件前先 is_writable($dir) 探一遍,部署清单里把"库房目录可写"列为检查项。
把本节要点组装成一个小模块:按天建目录、追加一行 JSON 日志、并发安全:
<?php function logVisit(string $event): void { $dir = __DIR__ . '/logs/' . date('Ymd'); if (!is_dir($dir) && !mkdir($dir, 0755, true)) { return; // 库房都建不出来,静默放弃或报警 } $line = json_encode([ 'time' => date('H:i:s'), 'event' => $event, ], JSON_UNESCAPED_UNICODE) . "\n"; $handle = fopen($dir . '/visit.log', 'a'); flock($handle, LOCK_EX); fwrite($handle, $line); flock($handle, LOCK_UN); fclose($handle); } logVisit('一批旅客经 3 号口进站'); // 落盘内容:{"time":"09:41:22","event":"一批旅客经 3 号口进站"}
变式思路:想把日志换成"最近进站旅客 Top 榜",就把事件行换成聚合统计,每分钟整存整取一次汇总文件;想扛更大的量,则把落盘换成消息队列或直接进数据库——那些是 2.2 与 2.5 节的故事,仓库这层的基础动作不变。
变式之外,补一个"文件缓存"的衔接性话题。2.5 节讲分层缓存时用的是 Redis,但在没有外置缓存的轻量环境里,文件本身就是零依赖的缓存介质:把查询结果 json_encode 后 file_put_contents 到缓存架(3.1 节目录树里的 cache 柜),命中判断加一个"文件修改时间超过若干秒即过期"的检查,十几行就是一个能用的缓存层。它的天花板也明确:并发高时文件锁会排队,多机部署时文件不共享——所以它是"单机小站的权宜之计",量大后迁往 Redis,接口设计(get、set、过期)保持一致即可平滑替换。
⚠️ 路径拼接用户输入:
$_GET['file']直接拼进读取路径,等于开放任意文件下载,路径白名单加只读根目录锁定是底线。⚠️ 忘记 fclose:脚本结束会兜底释放,但长脚本里漏关句柄会耗尽描述符,开即配对关。
LOCK_EX,流水登记用句柄加 flock,并发安全从第一天就做对。__DIR__ 锚定脚本目录,用户输入永不直接拼路径。mkdir 递归建、scandir 过滤点项清点、is_writable 先探后写。下一节回到旅客本人:表单处理。申报单怎么收、怎么验、怎么防伪造,是月台服务里最经典的完整链路。