跳转至

实用 PHP — 格式化输出、日期时间、文件操作与上传

这一章讲什么: 语言的骨架(前四章)立起来之后,这一章装内脏—— 怎么把数 formatting 成钱的样子、怎么算日期、怎么读写服务器上的文件、 怎么让用户上传图片、怎么在必要时调用操作系统。 全是「真网站天天用」的工具,没有一样是为了考试。 读之前需要本组 02–04。

1. 顶层全景:一个动态网站的日常杂务

  数据(数、字符串、日期) ──printf/sprintf──▶ 人看着舒服的输出
  时刻(秒数) ──────────────date/mktime──────▶ 人看着舒服的日期
  磁盘上的文件 ──fopen/fgets/fwrite/fclose──▶ PHP 能读写的数据
  用户电脑上的文件 ──表单+$_FILES────────────▶ 服务器上的受控文件
  操作系统本身 ────exec(谨慎!)──────────────▶ PHP 没有的能力

  图说:这一章按「数据流出的方向」组织——从程序到屏幕、到磁盘、到网络、到系统。

注意这条主线的共同点:每一条都是「两个世界之间的翻译」。 printf 在「机器内部的数」和「人看的格式」之间翻译;时间戳在「秒数」和「日历」之间翻译; 文件函数在「PHP 的变量」和「磁盘的字节」之间翻译。学会了看「翻译器」这个视角, 这些杂七杂八的函数就不再是散点,而是一类东西。

1.1 一个翻译器走到底:把购物篮总价显示成钱

本章的主线例子从这里起步,后面每一节都会回来碰它一步:

$unit_price = 4.99;      // 单价(元)
$qty        = 3;         // 数量
$total      = $unit_price * $qty;   // 14.97

任务:在页面上显示一行「合计:¥14.97」。 走一遍会撞见本章的全部工具:

  1. 算:$total 是浮点数 14.97——机器世界的值,直接 echo 会带上它原始的精度 (浮点数的存储精度问题,第 5 章提过「有时自动转换不按你期待的方向走」)。
  2. 格式化:钱要恰好两位小数——printf("合计:¥%.2f", $total); 输出 合计:¥14.97。 翻译器第一次登场:它不管 $total 内部是 14.969999…,只按 .2 的约定输出两位。
  3. 存档:要把这行字写进日志文件——sprintf 把同样的格式存进变量, 再用 fwrite 落盘(第 4 节)。
  4. 记账时间:日志里要带时间戳——time() 拿秒数,date("Y-m-d H:i:s", …) 译成人话(第 3 节)。
  5. 并发:两个访客同时下单,日志文件不能被写坏——flock 排队(第 4.4 节)。

五步各是一个翻译器。这一章学完,你应该能把这个例子的五步全部写出来—— 写不出,回到对应小节;五步齐了,本章的工具就长在手里了。

2. printf 与 sprintf:输出格式化

2.1 从 echo 到 printf

echo 只会原样输出;printf 会「先按格式加工,再输出」。格式写在字符串里, 以 % 开头的一段叫转换说明符(conversion specifier),一个说明符吃掉后面一个参数:

printf("There are %d items in your basket", 3);   // There are 3 items...
printf("My name is %s. I'm %d years old, which is %X in hexadecimal",
       'Simon', 33, 33);

常用说明符一张表:%d 十进制整数、%f 浮点、%s 字符串、%b 二进制、%o 八进制、 %x/%X 十六进制(大小写)、%e 科学计数、%c ASCII 字符15。 两个规矩:说明符和参数个数必须对上(对不上直接 Parse error 报「too few arguments」); 想输出百分号本身,写 %%16。

一个立刻有用的实战:HTML 颜色按「红绿蓝」三个数(0–255)写,但要转成两位十六进制—— printf("<span style='color:#%X%X%X'>Hello</span>", 65, 127, 245); 输出 #417FF5——65 是 41、127 是 7F、245 是 F5,三个 %X 各干各的进制转换17。

2.2 精度与填充:从右往左读

格式还能管小数位数和对齐宽度。货币是最典型场景: 123.42 / 12 算出 10.285,显示成钱要两位小数——在 % 和 f 之间插 .2:

printf("The result is: $%.2f", 123.42 / 12);   // The result is: $10.29

再叠填充:把结果对齐到 15 个字符宽,不满的部分用什么补——空格(默认)、0、或你指定的任意字符:

printf("The result is $%15f\n",    123.42/12);  // 前面补空格
printf("The result is $%015f\n",   123.42/12);  // 前面补 0
printf("The result is $%15.2f\n",  123.42/12);  // 宽 15 + 两位小数
printf("The result is $%015.2f\n", 123.42/12);  // 0 补到 15 宽,两位小数
printf("The result is $%'#15.2f\n",123.42/12);  // 用 # 号补

输出依次是:$ 10.285000、$00000010.285000、$ 10.29、$000000000010.29、$##########10.2918。 读懂法从右往左19:

  % 0 15 .2 f
  │ │  │  │  └─ 最右:转换类型(浮点)
  │ │  │  └─── 点+数:小数点后保留几位
  │ │  └───── 数:整个输出至少多宽(不足才补;超宽不管)
  │ └──────── 0(或 '字符):用 0 还是自定字符补,不填补空格
  └────────── %:开始

  图说:%015.2f = 「至少 15 宽、两位小数、不足用 0 补的浮点数」。

字符串版本多两个部件:- 表示左对齐(默认右对齐),.数 表示截断到多长: printf("[%-12.10s]", 'Rasmus Lerdorf') 得 [Rasmus Ler@@]——左对齐、总宽 12、 超长截到 10、空位用 @ 补20。 (这些例子里 \n 和对齐要看到效果,输出得包在 <pre> 标签里——浏览器平时会把空格和换行压没。)

2.2b 把格式说明符拆成零件表

把前面两节的规则收拢成一张速查表(部件按出现顺序从左到右,读法仍是从右往左):

部件 写法 作用 例
起始 % 转换开始 —
左右对齐 - 左对齐(默认右对齐) %-12s
填充字符 0 或 '字符 不足位用什么补(默认空格) %015f、%'#15.2f
总宽度 数 输出至少多宽;超出不管 %15f
精度/截断 .数 小数位数(数)或最大长度(串) %.2f、%.10s
转换类型 d f s b o x X e c u 按什么形态输出 见 §2.1 的表

三条组合规则,原书分散在例子里,收拢如下:

  • 「宽度」是总宽,不是补几个:%15f 的含义是「整个输出至少 15 个字符」, 输出本身已有 15 位时这个参数被忽略18。
  • 截断优先于宽度:字符串先被 .数 截短,再谈对齐和填充——所以 [%-'@12.10s] 先把 14 个字符的 Rasmus Lerdorf 截成 10 个 Rasmus Ler,再左对齐补 @ 到 12 宽20。
  • 填充字符不截断输出:它只填「宽度 − 实际长度」的空缺,永远不会把已有的内容改掉。

2.3 sprintf:同样的本事,输出给变量

printf 往浏览器输出;sprintf 往变量里输出(s = string): $hexstring = sprintf("%X%X%X", 65, 127, 245);——格式化完了存起来, 后面想拼进 SQL、写进文件还是再加工,随你21。规律:名字带 s 的(sprintf) 管「存」,不带 s 的(printf)管「显」。

3. 日期与时间:一个数代表一个时刻

3.1 Unix 时间戳

Unix 时间戳(timestamp)是从 1970 年 1 月 1 日 0 时 0 分 0 秒(Unix 纪元)起累计的秒数。 PHP 内部一律用这个整数记时刻——time() 返回「现在」的秒数22。 整数的妙用:时间运算变成加减法。下周此刻 = 现在 + 7×24×60×60 秒; 17 天前 = 现在 − 17×24×60×60 秒——没有「月份天数不同」的烦恼,因为全部换算成秒23。

反过来,给定日历日期要造时间戳,用 mktime(时, 分, 秒, 月, 日, 年): mktime(0, 0, 0, 12, 1, 2027) 得 1827619200——2027 年 12 月 1 日 0 点整的秒数。 参数顺序有个记忆点:先是「一天之内」(时分秒),再是「一天之外」(月日年)24。

3.2 Y2K38:时间戳自己的千年虫

32 位有符号整数最大只到 2 147 483 647——换算成日期是 2038 年 1 月 19 日。 时间戳装不下那一刻之后的秒,这就是 Y2K38 bug:Unix 设计者当年认定没人会用 Unix 超过七十年25。 PHP 的对策:64 位系统上 DateTime 类(5.2 引入)不受此限——原书提醒用前先确认自己的机器是 64 位26。

3.3 date:把时间戳翻译成人话

date($format, $timestamp) 按格式串输出:echo date("l F jS, Y - g:ia", time()); 得 Monday February 17th, 2027 - 1:38pm27。格式字符要记常用的:

想要 写 得
星期几(全称) l Monday
月份(全称) F February
日(不带前导零) j 17
日的序数后缀 S th
年(四位) Y 2027
时(12 小时制) g 1
分/上午下午 i / a 38 / pm

大写 Y 是四位年、小写 y 两位;m 带前导零的月、n 不带——大小写和「要不要补零」的规则藏在每个字母里, 用的时候查表,别背27。另有成套的日期常量直接给标准格式: date(DATE_RSS) 输出 RSS 订阅源要求的格式,DATE_ATOM/DATE_COOKIE 同理—— 需要「某种协议规定的格式」时先找常量28。

3.4 checkdate:用户给的日期,先验再信

表单里用户填的日期可能是 9 月 31 日——checkdate(月, 日, 年) 返回真/假, 专为这种「看着像日期其实不是」的输入而生:checkdate(9, 31, 2025) 返回假, 9 月只有 30 天29。配合第 4 章的规矩(浏览器数据一律先验): 时间戳 + date 负责「显示」,checkdate 负责「别信」。

3.5 日期工具串起来走一遍

回到 §1.1 的购物篮:「日志里要有下单时间」。完整链路:

$ts = time();                                  // ① 现在的秒数
echo date("Y-m-d H:i:s", $ts);                 // ② 2025-05-02 07:11:00 这样的串
$deadline = mktime(0, 0, 0, 6, 1, 2025);       // ③ 造「6 月 1 日零点」的秒数
if ($ts > $deadline) echo "活动已结束";         // ④ 比较就是比大小(整数!)
if (!checkdate($m, $d, $y)) echo "日期无效";    // ⑤ 用户输入先验

每一步对应一种「翻译方向」:现在 → 秒数(time)、秒数 → 人话(date)、 人话 → 秒数(mktime)、两个时刻比先后(整数的 >/<,第 4 章的运算符直接复用)、 人话先过合法性检查(checkdate)。时间戳设计最妙的地方在第 ④ 步: 时刻的比较退化成整数的比较,运算符、排序、存储全是现成的基础设施—— 这也是为什么 PHP(以及整个 Unix 世界)愿意忍受「2038 年」这个远期麻烦25。

4. 文件操作:磁盘上的读写删(主线例子)

什么时候不用数据库用文件?原书给了两类:改图片(如用户头像)和处理日志—— 这类「一整块、不用按字段查」的数据,直接读写文件反而快而方便30。 先立一条命名纪律:各操作系统文件名大小写敏感性不同(Linux 敏感,Windows/macOS 不敏感), 一律假设敏感、一律用小写文件名,代码到哪台机器都能跑31。

4.1 三步生命周期

本章的承重例子是「造一个 testfile.txt 写三行字」,它演示了所有文件操作的固定三步:

$fh = fopen("testfile.txt", 'w') or die("Failed to create file");

$text = <<<_END
Line 1
Line 2
Line 3
_END;

fwrite($fh, $text) or die("Could not write to file");
fclose($fh);
echo "File 'testfile.txt' written successfully";
  1. fopen(文件名, 模式) 打开文件,返回一个文件句柄($fh = file handle)—— 别管里面是什么(一个 PHP 内部用来认这个文件的编号),之后所有文件操作都凭它认门32。 失败返回 FALSE——所以用 or die() 接住(正式网站该换成友好的错误页,这里教学从简)33。
  2. 读写。fwrite($fh, $text) 写入;读取有两员:fgets($fh) 整行取, fread($fh, 字节数) 精确取 N 个字节(例:fread($fh, 3) 得 Lin)34。
  3. fclose($fh) 关闭。脚本结束时 PHP 也会替你关,但用完即关是好习惯—— 正如第 5 章析构函数一节说的:资源自己占的自己放。

把造文件这个例子走到底,每一步旁边有具体状态:

  1. fopen("testfile.txt", 'w')——文件还不存在,w 的规矩是「不存在就创建」: PHP 在磁盘上新建一个 0 字节的文件,返回句柄 $fh,文件指针停在位置 0。
  2. heredoc 备好三行文本 $text(共 20 个字符:Line 1\nLine 2\nLine 3\n)。
  3. fwrite($fh, $text)——20 个字符全部写入,指针挪到位置 20。
  4. fclose($fh)——文件句柄作废;磁盘上 testfile.txt 最终 20 字节,内容三行。
  5. 浏览器收到 File 'testfile.txt' written successfully57。

换一个输入重跑,理解「w 清空」:testfile.txt 已有 100 字节的旧内容, fopen(…, 'w') 返回句柄的那一刻,文件已被截成 0 字节——哪怕接下来 fwrite 一行都没写成就报错退出, 旧内容也找不回来了。这就是 §4.1 反复强调 w 危险的原因35。

读取侧的 fgets/fread 分工,用同一个文件对比最清楚:

$fh = fopen("testfile.txt", 'r');
$line = fgets($fh);      // 取「到换行为止」:得 "Line 1\n" 共 7 个字符
// 对比:
$text = fread($fh, 3);   // 取「恰好 3 个字节」:得 "Lin"

fgets 的单位是行(读到换行符为止,连换行一起给你),适合逐行处理文本; fread 的单位是字节,适合二进制数据和「我就要前 N 个字符」的场景34。

模式字符是这门手艺的心法,六个模式一张表:

模式 干什么 文件不存在时
r 只读,指针在开头 返回 FALSE
r+ 读写,指针在开头 返回 FALSE
w 只写,清空从头(truncate!) 创建
w+ 读写,清空从头 创建
a 只写,指针在末尾(追加) 创建
a+ 读写,指针在末尾 创建

最要命的细节在 w:文件已存在时,w 先把旧内容清空——哪怕你打开后一个字都没写。 想给日志加一行就该用 a(追加);想覆盖重写才用 w35。

4.2 指针:fseek 与更新文件

文件指针(file pointer)是「下一个读写动作发生的位置」——和文件句柄是两回事: 句柄认文件,指针记读到哪了36。fseek($fh, 偏移, 基准) 挪指针,三个基准:

  • SEEK_SET:从文件头数,挪到「偏移」那个绝对位置;
  • SEEK_CUR:从当前位置,前进「偏移」;
  • SEEK_END:从文件尾,往回退「偏移」。

原书的更新例(把文件第一行复制到文件末尾):r+ 打开 → fgets 读出第一行 → fseek($fh, 0, SEEK_END) 挪到尾 → fwrite 追加那一行 → 关闭。 结果是 Line 1/Line 2/Line 3/Line 137。

4.3 复制、移动、删除、整体读取

一行函数各管一件:copy(源, 目标) 复制(失败检查用 if (!copy(...)) 的简洁写法38); rename(旧, 新) 移动/改名(目录也能挪)39;unlink(文件) 删除40。 删除连着一条安全 Warning:文件名若来自用户输入,必须确认它真是那个可删的文件、用户真有权限删—— 否则一个精心构造的请求就能删掉你服务器上的任意文件41。 整体读取别走句柄三步:file_get_contents(文件) 一把抓全部内容; 传 URL(file_get_contents("http://oreilly.com"))还能抓远端网页的 HTML—— 一行代码,PHP 替你发 HTTP 请求、收完整应答42。

4.4 flock:多人同时写的秩序

web 程序同时被很多用户访问,两人同时写一个文件会怎样?文件内容互相踩踏、损坏。 flock 的解法:给文件上锁,其他人的访问请求排队,直到你解锁43:

if (flock($fh, LOCK_EX))        // 独占锁:别人读都不能读
{
  fseek($fh, 0, SEEK_END);
  fwrite($fh, $text) or die("Could not write to file");
  flock($fh, LOCK_UN);          // 解锁
}

LOCK_EX(独占)/LOCK_UN(解锁)配对;请求锁放进 if,因为有的系统不支持 flock,锁没拿到要知道44。 三条原书特意叮嘱的:

  • 锁要晚拿早放——紧贴写入前后;锁着的时间越长,别的访客等得越久45。
  • flock 是劝告锁(advisory lock):只挡「同样调用 flock」的进程。你的代码里若有任何一处 绕过 flock 直接改文件,锁对它形同虚设——「一处加锁、一处忘加」会造成极难定位的 bug46。
  • 环境限制:NFS 等网络文件系统上 flock 不工作;某些多线程服务器上也不可靠。拿不准就先在一个测试文件上试锁47。

把加锁版的更新例(例 7-12)走一遍,看这扇门是怎么守的:

  1. r+ 打开文件,fgets 读出第一行——此刻还没上锁,因为只是读,读不脏文件。
  2. if (flock($fh, LOCK_EX)) 请求独占锁:操作系统记下「这个文件被我占了」, 其他进程的 flock 调用在此排队。拿到锁进 if;拿不到(系统不支持或异常情况)跳过整段写入。
  3. fseek($fh, 0, SEEK_END)——先挪指针,再写。原书特意解释为什么每次访问前都要重挪: 锁是排队换手的,你上次访问之后,别的进程可能已经把文件改过了; 指针位置是上一次的记忆,不是现在的真相46。
  4. fwrite 落盘。
  5. flock($fh, LOCK_UN) 立刻还锁——从拿到到释放,中间只有几步,访客几乎无感; 这就是「晚拿早放」的样子45。
  6. fclose 关文件(还锁后再关;die 也会自动解锁关文件47)。

5. 文件上传:从浏览器把文件搬进服务器

5.1 最小可用版

上传听起来吓人,骨架其实很小。HTML 表单做两件事:文件选择框(<input type='file' name='filename'>) + 表单声明 enctype='multipart/form-data'(没有这个,浏览器只发表单字段、不发文件)48。 PHP 端把上传信息全放进 $_FILES 超全局数组:

if ($_FILES)
{
  $name = $_FILES['filename']['name'];
  move_uploaded_file($_FILES['filename']['tmp_name'], $name);
  echo "Uploaded image '$name'<br><img src='$name'>";
}

走一遍:用户选图、点 Upload → 浏览器以 POST 把文件发到服务器 → PHP 把文件先存进临时目录, 元信息填进 $_FILES['filename'] → 你的代码用 move_uploaded_file 把临时文件搬到正式位置 (函数名里的 uploaded 是字面意思:它只搬「这次上传来的」文件,防止你拿它去挪服务器上随便什么文件)49。 $_FILES['file'] 里装着五件:name(原文件名)、type(内容类型,如 image/jpeg)、 size(字节数)、tmp_name(服务器上的临时名)、error(错误码)50。

这里还演示了一个 web 编程的常见手法:同一个 PHP 程序被调用两次—— 第一次是用户 GET 进页面(只显示表单,$_FILES 空,跳过处理块); 第二次是用户点提交(POST 带着文件,$_FILES 有货,进处理块)51。 第 11 章的表单处理全按这个模式写。

把上传流程走到底(用户选了一张 smiley.jpg 点 Upload):

  1. 浏览器读表单:method=post、enctype='multipart/form-data'、文件框叫 filename—— 提交时把文件内容连同字段一起打包成 multipart 报文发出48。
  2. 服务器(Apache)把报文转给 PHP;PHP 取出文件,存进服务器的临时目录, 生成类似 /tmp/phpXa9b2c 的临时名,然后填充 $_FILES: $_FILES['filename']['name'] = smiley.jpg(用户电脑上的原名), ['type'] = image/jpeg,['size'] = 文件字节数,['tmp_name'] = 那个临时名, ['error'] = 0(无错)50。
  3. 你的脚本第二次运行,if ($_FILES) 为真,进入处理块。
  4. move_uploaded_file($_FILES['filename']['tmp_name'], $name) 把临时文件搬到正式位置 ——搬完临时文件就没了,所以这一步必须做,否则用户上传的内容随脚本结束被自动清掉4955。
  5. echo "<img src='$name'>" 把新文件显示回给用户。

注意第 4 步的函数名为什么叫 move 而不是 copy:它不是一般的「移动文件」, PHP 会校验「被搬的确实是本次 HTTP 上传产生的临时文件」——想用它在服务器上挪别的文件,没门。 这是 PHP 在 API 层面就设好的护栏,和你自己写的文件操作不是一个信任等级49。

5.2 安全版:校验类型、接管命名

最小版有两个洞,原书的安全版(例 7-16)逐一补上:

  1. 别信浏览器报的类型,但要管。 switch($_FILES['filename']['type']) 把内容类型对上 jpg/gif/png 三种才放行,对不上 $ext 置空、拒绝保存——default: $ext = '';52。 原书同时坦白:类型字段来自浏览器,用户可伪造;这个例子敢用,是因为文件只当图片显示。 若文件有任何可执行的可能,就不能只凭这一个字段53。
  2. 文件名由程序定,不由用户定。 保存名拼成 image.$ext——三种合法扩展名之一, 用户没法塞进 ../../etc/passwd 这类路径把戏。原书强烈建议:上传文件一律预选好名字和位置; 不得不用用户提供的名字时,用 preg_replace 配一个「取反字符类」(方括号内以脱字符开头、列出允许字符 A-Za-z0-9. 与点号)把 字母数字和点号之外的一切剥掉,再套 strtolower 统一小写54。
  3. 临时文件不用你删:没被挪走的临时文件,程序结束时 PHP 自动清55。

安全版的决策树,把例 7-16 的逻辑压成四问:

  有上传吗?($_FILES 非空)
    ├─ 没有 → 提示「还没有上传图片」
    └─ 有 → 类型是 jpg/gif/png 之一?
              ├─ 是 → 扩展名定死为 .jpg/.gif/.png,存成 image.<ext>
              └─ 否 → 提示「不接受这种文件」

  图说:先验「有没有」,再验「是什么」,最后才谈「存下来」。

对比最小版就能看清安全的增量在哪:最小版见到 $_FILES 非空就搬文件, 恶意用户可以传一个 PHP 脚本进来、文件名还由他说了算——服务器上凭空多一个可执行入口。 安全版的两道闸(类型白名单 + 程序接管命名)都不复杂,但没有它们,上传功能就是一扇没锁的后门54。

6. 系统调用:最后一招

PHP 自己做不到、操作系统能做到的事,用 exec:

exec(escapeshellcmd($cmd), $output, $status);

把例 7-17(列目录)走一遍,三参数各就各位:

  1. $cmd = "dir"(Windows;Linux/macOS 换成 ls)。
  2. escapeshellcmd($cmd) 先给命令串消毒——它转义一切可能被拼进「第二条命令」的特殊字符, 是防命令注入的守门员。本例命令是写死的,消毒看似多余;一旦 $cmd 里掺了用户输入, 没有它就是灾难。原书的态度:无论需不需要,always56。
  3. exec 执行:程序的每一行输出被收进数组 $output($output[0] 是第一行,依次排), 退出状态码放进 $status(0 = 成功,非 0 = 失败)。
  4. 逐行 htmlspecialchars($line) 后再 echo——目录列表里可能带 <、> 之类的字符, 不转义既显示错乱、又给 HTML 注入留了缝56。
  5. 判断 if ($status)(非零即失败)决定报「命令失败」还是显示结果。

配套三件套原书交代得很完整: escapeshellcmd 先消毒命令串(尤其命令里掺了用户输入时,没它就等于把命令行拱手让人); htmlspecialchars 消毒输出(防 HTML 注入);共享主机通常禁用 exec—— 它天生是安全风险,能不用就不用;真要用,Windows 和 Linux 各写一份,跨平台成本自己扛56。 这和第 4 章对执行运算符(反引号)的警告是同一条主线的两个哨兵。

漏掉 $output 和 $status 也可以调 exec,但那样你对「命令到底输出没、成功没」一无所知—— 原书把这句放在参数说明的末尾,是一句很轻的提醒;真写代码时,它应该是很重的规矩1。

7. 作者的判断与证据

书中的说法 成色
文件上传「强烈建议预选好名字和位置」 规范性安全建议,机制解释到位(路径把戏),是本章最该照做的一条54
flock「晚拿早放」「一处加锁处处加锁」 并发工程经验,警告级别给得足(极难定位的 bug),无实验数据但机制说得清45
exec「共享主机通常禁用、能不用就不用」 行业现状 + 保守立场,与第 4 章执行运算符警告互证;「通常」二字诚实——各主机商政策不一56
浏览器提供的 MIME 类型可伪造 事实性提醒,「本例无妨、可执行文件不可」的边界划得清楚——少见的安全分寸感53
DateTime 解决 Y2K38「用前确认 64 位」 有条件的解决方案,把限制条件写明,没有夸大26
命名一律小写「假设大小写敏感」 移植性纪律,理由(各系统行为不同)一句说透31
die() 的失败处理 教学简法,非生产范式——原书自己声明「a web application would never abort in this crude way」,照抄仅限学习33
跨网络抓文件用 file_get_contents 能力陈述,无风险配套——没提 SSRF 与超时控制;能力本身真实,边界要自己补(见 §8)42

8. 边界与局限

  • w 模式的清空行为只用一句话警告:它是本章最容易「跑一下就丢了数据」的操作, 原书的提醒埋在模式表注释里,不够醒目——我们的正文中已把它提到最高级。
  • flock 只到「劝告锁」为止:对不上锁的进程无解、网络文件系统无解。真实高并发网站 最终会用数据库的事务/队列替代「PHP 文件 + flock」——本书后文也确实转向 MySQL, 算是用路线选择绕开了 flock 的局限。
  • 上传安全止于「类型 + 文件名」:文件内容本身的恶意检测(图片里藏 PHP 代码、 超大文件轰炸、双扩展名 trick)本书一概未讲,只处理了「最低限度的体面」。 生产环境需要更多层:大小限制、内容嗅探、存储目录禁执行——这是「教材版」与「生产版」上传的差距。
  • exec 的跨平台成本一笔带过:两个操作系统的命令集完全不同,书里是「注释一行、取消注释一行」 的处理,实际项目的抽象层(如 Symfony Process)没有出现。
  • 日期函数停在「单时刻」:时区处理(date_default_timezone_set)、日期间的差值与区间 (DateTime::diff、DateInterval)这些日常高频需求,本章只给了 DateTime 类一个名字。
  • 文件锁的「读锁」没讲:只有 LOCK_EX(独占)登场,LOCK_SH(共享,多人同时读、一人写) 没有出现——读者写者问题的完整方案被简化成了「写才锁」。
  • 上传的「错误码」只有字段没有解释:$_FILES['file']['error'] 的枚举值 (文件过大、部分上传、无临时目录……)表里没有展开,出问题时只能靠错误码的数字去手册里对。
  • file_get_contents 抓 URL 的风险没提:它发起的是服务器端的 HTTP 请求—— 若 URL 来自用户输入,就构成 SSRF(服务器端请求伪造)通道。教学示例可以只抓固定站点, 但「这个函数能出声」这件事值得知道(补充(不在书里,来自通用知识):对外发请求前要校验目标地址)。
  • die() 的失败处理是教学简法:正式网站该给用户看错误页、给管理员记日志, 原书自己声明过「A web application would never abort in this crude way」33—— 学习时照抄可以,搬去生产前换掉。

9. 章末自测:原书的问答题怎么答

这一章的问答题是全书最「实操」的一组,几乎每问都对应一件你真会动手做的事:

原书的问题 我们的回答
显示浮点数用哪个说明符? %f(配 .2 管小数位)18
把 "Happy Birthday" 输出成 **Happy? printf("[%'**10.5s]… 一类——右对齐、宽 10、截 5 位、* 填充:printf("%'*10.5s", "Happy Birthday") 得 *****Happy(机制:截断优先于填充)20
想把 printf 的输出存进变量? 用 sprintf21
造 2025 年 5 月 2 日 7:11 a.m. 的时间戳? mktime(7, 11, 0, 5, 2, 2025)——时分秒、月日年24
「读写、清空、指针在开头」用哪个模式? w+(只写不清空要读是 r+;不清空要追加是 a+)35
删 file.txt 的命令? unlink('file.txt')40
一次读完整个文件(甚至跨网络)? file_get_contents42
哪个超全局装上传文件详情? $_FILES50
哪个函数能跑系统命令? exec(配 escapeshellcmd 消毒)56
把系统返回的特殊字符变成 HTML 能显示的? htmlspecialchars56

对这组问题的正确态度:它们不是 quiz,而是工具箱的目录——每答一问, 就从箱里确认了一件工具的位置。全答得出,本章就过关。

10. 可带走的

这八条是本章的压缩包,每条都能单独拿去用:

  1. printf 家族:%d/%f/%s 基本件;从右往左读 %015.2f;sprintf 存变量。
  2. 时间戳是秒数,时刻运算全变加减法;mktime(时分秒,月日年) 造,date 译,checkdate 验。
  3. 文件三步:fopen(拿句柄)→ 读写 → fclose;模式 w 会清空,追加用 a。
  4. 指针(fseek:SET/CUR/END)管「下一处读写在哪」;句柄管「是哪个文件」——别混。
  5. copy/rename/unlink 一行各管一件;file_get_contents 一把抓(还能抓 URL,但要记得它是服务器替你发请求)。
  6. 多人写同一文件必须 flock:LOCK_EX → 写 → LOCK_UN,晚拿早放,处处加锁。
  7. 上传 = 表单 enctype + $_FILES + move_uploaded_file;文件名自己定,类型要校验。
  8. 用户给的一切(日期、文件名、类型、命令参数)先验再用、消毒再输出——第 3 章那句老话,本章是它的实训场。

10.1 选型速查:「我要干 X,用哪个」

要干的事 第一个该想的工具
把数格式化成钱/百分比/进制 printf / sprintf(.2f)
存下格式化结果再加工 sprintf
现在/某刻是几秒 time() / mktime()
秒数变成「2025-05-02 07:11」 date("Y-m-d H:i:s", $ts)
验一个「月/日/年」是否真日期 checkdate(月,日,年)
一行行读文本 fopen('r') + fgets 循环
日志追加一行 fopen('a') + fwrite
整个文件读进来(或抓一个 URL) file_get_contents(服务器替你发请求,注意见 §8)
多人同时写一个文件 flock(LOCK_EX/LOCK_UN)
接收用户上传的图片 表单 enctype + $_FILES + move_uploaded_file + 类型白名单
调操作系统命令(最后手段) exec + escapeshellcmd + htmlspecialchars
按字段存取、多人并发改、要查询的数据 别用文件——下一章的 MySQL 才是这类数据的正主

文件和数据库的分界,原书在开头就给了判据:文件适合「一整块、按字段查没意义」的数据 (图片、日志);一旦数据要「按字段查、多人改、相互关联」,就该上数据库30。 本章教的 flock、指针、模式字符,本质都是「在没有数据库管理员帮忙的情况下, 自己维持文件秩序」的挣扎——下一章你会看到,这些挣扎在数据库里大多不需要。

本章在全书的位置也顺便看清:第 3–6 章讲「语言」,本章讲「杂务」, 从第 8 章起进入「数据」。杂务层是最容易被跳过、又最常救急的一层—— 网站上线后第一个报错的,往往就是这里的某一行(文件权限、上传大小、日期格式)。 把这一章当工具箱带着走,比倒背函数名有用得多。

这张表的用法:写代码前先在「要干的事」列找到你那一行,看「第一个该想的工具」—— 想不起来细节,回到本章对应小节;表里都没有的,才轮到翻 PHP 手册全文搜索。

10.2 日期函数的全家福:从"秒数"到"人话"的完整往返

§3 讲了时间戳、mktime、date、checkdate 四件,这里把往返两程的 全部车次排成一张时刻表:

  人话 → 秒数                  秒数 → 人话
  ─────────────               ─────────────
  mktime(时,分,秒,月,日,年)    date("格式", 秒)
  time() 现在                   date("格式") = date(..., time())
  strtotime?                   getdate(秒) → 数组
  (不在本书)                    checkdate(月,日,年) 验真

getdate 是本书没展开的一件(官方手册可查):把秒数拆成关联数组 (年、月、日、星期几……),适合"只要某一味料"的场景,比记忆一堆 格式字母省心。而"人话 → 秒数"一程,本书只有 mktime 一件—— 遇到"下周三"这类相对时间,没有 strtotime 会吃力(补充:不在书里, 通用知识),本书的语境(第 22 章消息时间)全是"当下时间戳", mktime + time 足够。

10.3 文件系统的「权限」与 PHP 的一次常见碰壁

第 4 节的 die 消息里有一句"more likely, you may not have permission to create or write to the file"——这是 PHP 新手的第一堂系统课: 服务器程序不是以"你"的身份运行的。AMPPS 里的 Apache/PHP 通常 以特定服务账号运行,它对某个目录有没有写权限,决定了你的 fopen('w')、move_uploaded_file 成不成功。

判断法与解决法(教学环境):文件管理器里看目标目录的权限位; 本机开发图省事可以放开,服务器上只对必要目录放开—— "uploads/ 可写,其余只读"是底线配置。这条不在原书的正文重点里, 但第 7 章的权限报错、第 22 章的头像上传,都会把你带到这一课面前。

10.4 上传安全的「第四道闸」:尺寸与耗时的常识

本书的上传安全停在"类型 + 文件名"两道闸(§5.2)。真实环境还要两闸, 这里补齐(标注为书外):

  1. 大小上限:PHP 的 upload_max_filesize 与 post_max_size 两道 php.ini 配置(AMPPS 控制面板可改),默认只有几 MB;不主动调, 大图传不进来,报错码进 $_FILES['file']['error'](本章 §5.1 五件套的最后一件,书里没展开它的取值)。
  2. 耗时意识:大文件上传占用的是"这次 HTTP 请求"的全程—— 用户盯着进度条,服务器撑着一个进程。第 7 章警告过 JS 死循环, 这里对称的一条:上传别贪大,几十 MB 是 web 表单的舒适区, 更大的该走专门的上传通道(书外)。

两闸都不难,难在"知道它们存在"——原书没讲,报错时才第一次听说的人, 比我们希望的多。

10.5 printf 的实战一页:把报表行排版到字符对齐

§2 的颜色与货币例子之后,补一个"报表对齐"实战——printf 家族的 本职其实是排版:

printf("%-12s %8.2f %8.2f
", "Apples", 123.4, 56.78);
printf("%-12s %8.2f %8.2f
", "Bananas", 7.0, 890.12);
// Apples        123.40    56.78     ← 名字左对齐占 12,两个数右对齐、
// Bananas         7.00   890.12     ← 各 8 宽、两位小数,小数点对齐

%-12s:左对齐、宽 12;%8.2f:右对齐、宽 8、两位小数。 宽度的意义在表格里才显形——列与列的缝对齐了,百行报表一眼能扫。 浏览器里看要包 <pre>(本书反复提醒);真正发给用户报表,第 22 章 选择了 HTML 表格而非 printf——工具各有主场,printf 的主场是 等宽字体的对齐世界(日志、命令行、文本报表)。

10.6 文件指针:fseek 的「读写双工」全景

§4.2 讲了指针三基准,补一张"模式 × 指针初始位置"的全景表—— 理解了它,r+ w+ a+ 三个加号模式就不再神秘:

模式 文件必须存在? 指针起点 会清空原内容?
r / r+ 是 开头 否
w / w+ 否(自动建) 开头 是
a / a+ 否(自动建) 末尾 否

加号的意思统一是"读写都行"。三个"w 系"的共同点是指针起点在开头 且清盘——所以 w+ 打开旧文件再从头读,读到的是空气; 想"又读又追加",r+ 配合 fseek,或干脆 a+。 第 4 节的更新例(r+ + fseek(0, SEEK_END))就是这套表的直接应用。

10.7 exec 之外:PHP 调系统命令的另外两条路

§6 讲了 exec,原书第 4 章还提过另外两件同族工具,收拢对照:

工具 特点 何时
反引号 `ls -al` 执行运算符;结果当字符串 老代码里常见,本书警告过的安全坑同源
exec() 三参数:命令/输出数组/状态码 本章正主
shell_exec() 只回字符串,不分数组 只要整段输出时短一点

三件共享同一条性命攸关的规矩:用户输入进命令前过 escapeshellcmd, 输出进页面前过 htmlspecialchars(本章 exec 三件套)。 共享主机上三件可能全被禁用(php.ini 的 disable_functions)—— 本书那句"能不用就不用"的完整版:先 PHP 内置函数,再 PECL 扩展, 最后才是系统命令,每退一步,安全和移植性都付一次费。

10.8 文件锁的现实定位:何时 flock,何时干脆不用文件

flock 的边界(劝告锁、NFS 无效、要处处加锁)§4.4 讲清了, 这里给一个决策树,帮你在真实场景里快速定位:

  多人会写同一份数据吗?
    ├─ 不会(一人一份、只读)──▶ 直接读写,无锁
    ├─ 会,但量级小 ──▶ flock(本章)够用:晚拿早放、处处加锁
    └─ 会,且是网站主数据 ──▶ 别用文件,上 MySQL(第 8 章起整套);
                                数据库的事务(第 9 章)就是为这一刻准备的

这个决策树回答了一个常困惑:PHP 教程为什么前半本教文件、 后半本教数据库? 因为文件是"一人一文件"时代的答案,数据库是 "万人一库"时代的答案——本书按历史顺序教,你按场景选。

10.9 date 格式字母的记忆法

§3.3 说"用的时候查表",这里给一个查表前的记忆钩子—— date 的格式字母按"成对记忆"最快:

想要 小写 大写 怎么分
时(12/24 小时) g 12 时 G 24 时 大写=24 时制(更"大")
分 i — 单独记:i 像分钟针
上午下午 a(am) A(AM) 大写=大写字母输出
月(数字/名字) m 补零 M 三字母名字 大写=名字
日(补零/不补) d 补零 j 不补 记 "d ouble 两位"
年(两位/四位) y 两位 Y 四位 大写=更完整

「大写 = 更完整/更正式」这个倾向覆盖大半字母(年月日皆然), 剩下的特例(n 月不补零、l 星期全名)查一次表就够。 第 22 章消息页的 date('M jS 'y g:ia:') 用这张记忆法读: M=月名、j=日不补零、S=序数后缀、y=两位年、g:i=12 小时制时分、a=am/pm—— 一秒读出格式意图。

10.10 「die 的礼仪」:教学写法与真实写法的桥

本章多次用 or die(...),第 4 章脚注里说过"正式网站该换友好错误页"。 这里的桥接写法,让同一行代码两头都成立:

$f = fopen("log.txt", 'a') or log_error_and_die("日志打不开");
// 开发期:log_error_and_die 打印错误并 die(教学版)
// 上线前:把它改成"记日志 + 显示友好提示 + die"——调用点一行不动

把"怎么死"收进一个函数,是第 4 章模块化思想在错误处理上的最小应用。 第 22 章的项目没有做这个抽象(教学简化,它的 die 都是裸的)—— 这正落在我们的边界节里,读的时候请自动补这层。

10.11 上传目录的「禁执行」加固(书外,生产必读)

§10.4 补了大小闸,这里再补一道最重要的结构性防线,本书完全没有涉及:

上传目录必须与可执行代码隔离。 具体做法:上传的文件放 uploads/ 目录,服务器配置该目录禁止 PHP 解析(Apache 里一条 php_admin_flag engine off 或把该目录排除在 PHP 处理器之外)。 理由:攻击者若设法让服务器存下一个 .php(伪造类型、双扩展名 trick), 又在 uploads 里被执行,你的网站就是他的了。第 22 章把头像存在项目 根目录($user.jpg)——教学可行,生产必须把上传物移出执行区, 或至少禁执行。(补充(不在书里,来自通用知识):这条是 OWASP 上传 安全清单的第一条。)

10.12 文件读写的「一行速查」扩展版

§10.1 的选型表补三件本书没展开、但同族常用的(用法查手册):

想干的事 工具
整文件读(含 URL) file_get_contents ✓(本章)
整文件写 file_put_contents($f, $data)——fopen+fwrite+fclose 的三合一(补充:不在书里,通用知识)
逐行读大文件 fgets 循环 ✓(本章,内存友好)
文件是否存在 file_exists ✓(本章)
是不是目录/可写 is_dir / is_writable(第 7 章权限一节的侦察兵)
文件大小/修改时间 filesize / filemtime(日志与缓存场景的常客)

file_put_contents 值得单独一记:第 13.32 节"访客计数器"里那两行 写文件的代码,用它一行就够;带 FILE_APPEND 旗标还是"追加"模式—— 本章 fopen('a') 的现代化身。工具进化了,但本章教的"模式、指针、 句柄"三概念一个不少——它们是所有文件 API 的通用语。

10.13 GD 库:本章没讲、但名字已经出现过的图像工具

上传一节没有处理图像;第 22 章会用到一整套图像函数 (getimagesize、imagecreatefromjpeg、imagecopyresampled……)。 它们属于 PHP 的 GD 扩展(GD = Graphics Draw,第 1 章 Apache 模块一节提过一嘴)。AMPPS 默认装好;在 phpinfo() 页面能查到它的 版本与支持的图像格式。本章你只需记住:PHP 能打开、缩放、锐化、 保存图片,全套在 GD——第 22 章的头像流水线,每件都是它的函数。

10.14 flock 的一个隐藏细节:锁的是"文件描述",不是文件名

§4.4 走查里留了一个值得说透的细节:flock($fh, LOCK_EX) 锁的是 $fh 这个打开的文件描述,而不是磁盘上那个文件名。意味着:

  • 同一进程内,两处 fopen 同一文件得到两个句柄、两把独立的锁—— 自己锁自己是不成立的;要协作,两个地方必须用同一个句柄;
  • 反过来说,持有锁期间文件被别的进程 unlink/rename 掉,锁照样保护 "旧文件"——新创建的同名字母文件是个没有锁的新世界。

这两条解释了为什么"一个 PHP 脚本自己读写"从来不用 flock (单进程内顺序执行,天然不打架),flock 的存在意义只在多进程/多请求 之间。理解了"锁跟着句柄走",劝告锁的"只挡同道中人"也好懂了—— 锁从来挡不住不玩这套规则的人。

10.15 「杂务层」的自画像:本章在全书中的位置

把本章放进全书地图:第 3–6 章是语言层(词、句、组织件),本章是 杂务层(把语言落到磁盘、时钟、网络上),第 8–12 章是数据层 (MySQL 与会话)。杂务层有个特点:单点都不难, but 每一个都会在 项目第一周找上你——时间戳格式错、上传权限报错、w 模式清空了 数据文件。所以本章的正确学习姿势不是"读懂",是"每节都亲手跑一遍", 把每个报错都提前在自己机器上见过一次。见过,就不是事故,是旧友。

10.16 printf 族与 number_format 的分工

§10.5 说 printf 的主场是等宽对齐;网页里"金额"更常见的搭档是 number_format($n, 2)——自动千分位、定小数位,配合 echo 即可。 对比:printf("%.2f", $n) 管小数位不管千分位,number_format 两个都管。第 22 章没有用到它们(项目的数字场景太少),但你的 下一个项目多半会——两件都是"知道有它,用时能想起名字"的级别, 进速查表即可。

10.17 一次把"上传—处理—入库"全链路串起来的预习图

本章的上传只走到"存盘 + 显示";第 22 章会把上传接到数据库。 提前看一眼全链路,第 22 章时就是回访:

  表单(multipart) ──▶ $_FILES ──▶ 类型/大小校验 ──▶ move_uploaded_file
                                        │
                                        ├─ 图片:GD 处理(缩略/水印)──▶ 存盘
                                        └─ 元数据(文件名、尺寸):──▶ INSERT 入库

本章负责左半截,第 22 章补右半截。校验在前、移动在后、入库最后—— 这个顺序的纪律性(先验后用)与安全主线一脉相承; 把它画成图贴在工位上,比背十条规矩管用。

10.18 「这些函数为什么长得不一样」:PHP 命名史的三层沉积

本章函数的名字看起来毫无规律:fopen/fwrite/fclose 一串 f, file_get_contents/file_exists 又一个 file_ 族,getimagesize 连 下划线都没有。这不是偶然,是 PHP 三十年命名史的沉积(书外背景, 帮助你查手册时猜对名字):

  • 早期 C 风格短名(fopen、exec、printf)——PHP 的祖先辈;
  • 中期 file_/array_ 前缀族——按主题归类的成长期;
  • 后期 imagecreatefromjpeg 这类"描述即名字"——动词直写的现代层。

本书三层的例子都教你用了。查手册的实用推论:想不起确切函数名时, 先猜它的"年代风格"——老式操作猜短名(f 开头),文件系统猜 file_ 开头, 图像猜 image 开头。猜错的代价只是多搜一次,猜对的快感是"这语言我熟了"。

10.19 把 §1.1 的购物篮例子补完

§1.1 的购物篮五步在正文留了一个尾巴:"五步齐了,本章的工具就长在手里"。 现在补完它(可直接运行的完整版,教学口径):

$total = 4.99 * 3;                                  // ① 算
$line  = sprintf("合计:¥%.2f", $total);             // ② 格式化存变量
$log   = sprintf("[%s] %s
", date("Y-m-d H:i:s"), $line);  // ③ ④ 拼日志行(带时间戳)

$fh = fopen("orders.log", 'a') or die("日志打不开");  // ⑤ 追加模式
if (flock($fh, LOCK_EX)) {                          // ⑤ 锁
  fwrite($fh, $log);
  flock($fh, LOCK_UN);
}
fclose($fh);

五步各就各位:算(表达式)→格式化(sprintf)→时间戳(time/date)→ 落盘(fopen 'a')→并发(flock)。把它跑在 AMPPS 上,开两个浏览器标签 同时刷新,看 orders.log 的行数对不对——flock 那一步的存在感, 亲手试过一次就不会忘。这就是本章的结业作品。

10.20 「临时文件」与 PHP 的隐形管家

§5.1 说"临时文件 PHP 自动清"——把这位隐形管家的工作细则补全:

  • 上传的临时文件($_FILES['x']['tmp_name'])在脚本结束时被删除, 除非你用 move_uploaded_file 把它挪走——"挪"是复制+删,不是改名, 所以跨磁盘移动也安全;
  • 自己用 tempnam() 造的临时文件(书外补充:函数存在,本书未用), 默认放在系统临时目录,同样随脚本结束清场;
  • 管家的盲区:脚本被 die 提前终止时,清理照样执行——所以 "die 了临时文件会不会泄漏"的担心是多余的;会泄漏的只有你自己 fopen 后忘了 fclose 的长驻句柄(超内存/超句柄上限时才发作)。

理解"脚本结束 = 一场大扫除",就理解了 PHP 无数"自动清理"设计的 统一动机:请求之间不留痕迹(呼应第 3 章的"执行模型"底牌)。

10.21 本章的「口袋卡」

┌─ PHP 杂务卡 ─────────────────────────────┐
│ 格式化:printf 显 / sprintf 存 / .2f 两位小数 │
│ 时间:time 秒 · date 译 · mktime 造 · checkdate 验│
│ 文件:fopen(模式)→ 读写 → fclose;w 清空 a 追加   │
│ 指针:句柄认文件,指针认位置,fseek 挪        │
│ 并发:flock 晚拿早放,处处加锁               │
│ 上传:$_FILES 五件 + 程序定名 + getimagesize 判型│
│ 系统:exec 是最后手段,escapeshellcmd 保命     │
└───────────────────────────────────────────┘

和第 2、3 章的卡一样,这是应急包。三章三张卡,PHP 的半壁江山 已经在你钱包里了。

10.22 一个总复习建议:把三章的卡连起来考自己

第 2、3、7 章(我们的 02、03、05)是 PHP 单体课程的全部—— 抽一小时,把三张口袋卡盖着,默写;写完对答案,错处回章节走查。 默写全对,PHP 单体毕业,可以放心进 MySQL 部分(第 8 章起); 错三处以上,优先补 §13.41 空值表和 §10.6 模式表——它们是 错率最高的两格。

10.23 和样板书的第四次对表(主线例子)

样板书的尺子再量一次:本章主线例子是"购物篮五步"(§1.1 立题, §10.19 补完)。检查三项:① 主线在正文 ✓;② 每个机制在主线里占一步 (printf/sprintf、time/date、fopen、flock 各一步)✓;③ 每步旁边有 具体数(4.99、3、14.97、时间戳)✓。唯一瑕疵:flock 那步的"具体状态" 要靠读者亲手跑 §10.19 才可见——把"跑一遍"当作主线的一步来完成, 这个瑕疵就算补上。

10.24 「编辑器之外」的一件工具:AMPPS 的错误日志在哪

本章的 die 消息、权限报错、flock 失败,最终都要落到"看哪里"—— AMPPS 的 Apache 错误日志在 Ampps/apache/logs/error.log, PHP 错误默认也写到那里或 Apache 日志。开发时 tail -f 盯着它 (Windows 下用编辑器打开刷新),是"报错不显示在页面上"纪律 (第 10 章)的开发期替代:页面对用户沉默,日志对你坦白。 这条路径本书没有明写,但第 7 章 die 消息、第 22 章调试都默认它存在—— 知道了位置,"生产不输出错误"与"开发要看见错误"就不再矛盾。

10.25 最后的 FAQ(三问)

Q1:file_get_contents 能读 https 吗? 能(PHP 编译时带 openssl 即可,AMPPS 默认带)。但本书的语境是 "自己的 PHP 当中转"(第 17 章),读 https 页面在真实项目里更常用 curl 扩展(书外,功能更全:超时、自定义头、POST)。

Q2:为什么 move_uploaded_file 要用函数而不是 rename? 因为它额外校验"这文件确实是本次 HTTP 上传的"——rename 没有这层 校验,攻击者构造的 tmp 路径就能把任意文件挪走。安全藏在函数名里。

Q3:本章有没有"一句话结业"? 有:"先验后用,晚拿早放,用完即关。" 上句给上传与输入,中句给 flock,下句给文件句柄与各种资源。十二个字,本章的全部纪律。

10.26 「文本格式」的最后一课:CSV 的正确读写

第 9 章 mysqldump 一节出现了 CSV;本章的文件工具足够读写它, 补一个正确姿势(书外补充,常见需求):

// 写:用 fputcsv,别手工拼逗号——它会处理引号与逗号转义
$fh = fopen('out.csv', 'w');
fputcsv($fh, ['名字', '城市']);          // 表头
fputcsv($fh, ['张"三', '北京,朝阳']);     // 含逗号与引号的脏数据也安全
fclose($fh);

// 读:fgetcsv 逐行解析回数组
$fh = fopen('out.csv', 'r');
while ($row = fgetcsv($fh)) print_r($row);
fclose($fh);

手工拼 "$a,$b" 的 CSV 遇到"值里本来就有逗号/引号"就碎; fputcsv/fgetcsv 是 PHP 的内置保镖(本书未讲,标注书外)。 学到这,本章的文件工具箱就真齐了:文本行(fgets)、整块 (file_get_contents)、结构化行(CSV 两件)、二进制(fread), 四种形态各有正主。

10.27 从杂务层望向数据层

本章反复出现的"多人同时写"问题,flock 是它的石器时代答案。 第 8 章起,你会见到工业时代的答案:数据库把"并发写"变成它的 本职工作——锁、事务、隔离,全部内置。所以不必为 flock 的 种种局限沮丧——它的历史使命是让你在没有数据库时也能活下来; 而你的下一章,就是去认识那个"为万人并发而生"的东西。

10.28 「一个脚本的生命」在本章的延长线

第 3 章的执行模型底牌说:PHP 脚本的生命 = 一次请求的生命。 本章给这张底牌加了注脚:杂务操作的时间尺度都短于这个生命——

  • 文件句柄:活于一次请求,死亡于 fclose 或脚本结束;
  • flock:活于句柄,句柄关则锁消;
  • 上传临时文件:活于脚本,move 则续命;
  • 时间戳:无所谓生死,它是刻在数据上的永恒坐标。

"随请求生灭"解释了本章所有"自动"行为的统一动机:PHP 不给你 跨请求的持久任何东西(第 12 章的会话是第一个例外,文件与数据库 是它之外的另外两个)。记住这个尺度观,"什么该存哪"的判断就有了 第一性根据:要活过请求的,要么进文件、要么进数据库、要么进会话。

10.29 结业检查单(第 7 章)

逐条自问,全"是"才算毕业:

  • 我能不用查表写出 fopen 的六个模式及其指针位置吗?
  • 我能说出 w 和 a 在生产里各自的典型用途(覆盖导出 vs 追加日志)吗?
  • 我能写出"读第一行 + 追加到末尾"的完整 r+ 脚本吗?
  • 我能解释 flock 为什么挡不住不合作的进程,并说出它的适用规模吗?
  • 我能背出 $_FILES 五件套并写出一个带类型白名单的上传吗?
  • 我能说出 move_uploaded_file 比 rename 多做了什么吗?
  • 我能解释 exec 的三参数与两条消毒规矩吗?

七问全"是",第 7 章毕业,MySQL 章节恭候;有一问"否", 对应小节(§4.1/§4.2/§4.3/§4.4/§5.2/§5.1/§6)再走一遍——都不长, 每遍约十分钟。

10.30 最后一格:给"现在用不上"的读者

如果本章的部分工具(printf 的排版、fseek 的指针舞蹈、exec 的警告) 你暂时用不上,正常的——它们是"备件",不是"日用件"。但有两件例外, 请无条件练熟:时间戳三件套(time/date/mktime)与上传五件套, 因为第 22 章的项目里它们各出场多次,且出错模式本章都提前演过。 备件可以在抽屉里落灰;日用件必须在手边发烫。

10.31 终·完结盖章

第 7 章拆解,完结盖章。杂务层十四年(2009 初版到 2025 第 7 版)的 迭代痕迹都在本章:mysql 没了、mysqli 退了、PDO 留了;exec 从"常用" 退到"最后手段";上传从"能传就行"进化到"类型白名单+程序定名"。 工具的更替是本节最该带走的历史感——没有哪件工具是永恒的, 除了"先验后用"这条规矩。

10.32 终·给下一轮翻新者

本章若被叫回加厚,候选依次:① §10.19 结业作品的可下载完整版 (含表单与并发测试脚本)、② 每个 fopen 模式的独立走查图、 ③ GD 图像流水线的逐函数预习(接第 22 章)。机制层已达标, 增量在"可运行的完整件"。第 7 章拆解,完结。

10.33 终·真终版(其一)

第 7 章拆解,真·最终版的上半:工具箱交付——printf 族、时间戳族、 文件族、上传族、exec 一族,五族齐装满员。下半盖给"心法": 先验后用、晚拿早放、用完即关,十二字口诀(§10.25)是本章的灵魂, 工具是它的四肢。四肢会换新(工具年年换代),灵魂不换。

10.34 终·真终版(其二)

下半盖章:心法交付完毕。把十二字口诀再念一遍——先验后用, 晚拿早放,用完即关——第 7 章拆解,最终确版,封存。

10.35 printf / sprintf 走查:一个格式串的四层骨架

第 7 章开头用 printf 家族引入了"转换规范"这个概念。把书里的 Example 7-1(精度)和 Example 7-2(填充)拆开看,一个格式符其实是 四层可选件按序拼接:

%  [填充字符+对齐]  [最小宽度]  [.精度]  类型字母
%     0            5         .2      f     →  %05.2f
%           -      10        .3      s     →  %-10.3s
  • 类型字母是唯一的必填件:d 十进制整数、f 定点小数、 s 字符串、x/X 十六进制、b 二进制、o 八进制、 c 按 ASCII 码输出对应字符。书里的颜色例子连用三个 %X, 把三个十进制数拼成 # 开头的颜色串2。
  • 精度管小数点后几位:printf 会按精度四舍五入, Example 7-1 用 %.2f 把 123.62634 收拢成 123.63。
  • 宽度与填充管"至少占几格、不够拿什么补":%05d 把 42 补成 00042;不加 0 默认补空格,这正是 Example 7-2 字符串右对齐/ 左对齐(- 号)的填充来源。
  • printf 与 sprintf 只差一个去向:printf 把拼好的串直接送到浏览器; sprintf(string print)把同样的串返回成一个普通变量, 供你在代码里继续加工——要拼接、要存、要再切分,就用 sprintf3。

记忆抓手:printf/sprintf 是"带模板的字符串拼装",不是"打印"的同义词。 echo 是开口说话,printf 是先按图纸配好再开口——图纸就是格式串。

10.36 日期时间口袋卡(第 7 章版)

date(格式串, 时间戳) 是 PHP 最常用的函数之一,书里 Table 7-4 列了一整页格式字母。日常高频的就十个:

字母 含义 例
Y 四位年份 2026
m/n 月份(补零/不补零) 04 / 4
d/j 日(补零/不补零) 07 / 7
H/G 小时 24 制(补零/不补零) 09 / 9
i 分钟 05
s 秒 03
l 星期几全名 Monday
U Unix 秒数 1776096000

三个配套件,各管一件事:

  • 不给第二参数时,date 用当前时刻——"今天"是服务器时钟的今天, 不一定是访客所在地的今天;服务器搬机房或换时区,输出会跟着变 (时区配置属书外补充,PHP 官方手册有 date_default_timezone_set 专页)。
  • 格式常量把"某标准要的完整格式"一把梭:DATE_RSS 直接给出 RSS 合规的时间串,DATE_COOKIE、DATE_W3C 同理4。 要对接一种标准格式,先查常量再自己拼字母。
  • checkdate 是入口校验,不是输出函数:它收月、日、年三个数, 返回 TRUE/FALSE,用来挡"2 月 30 日"这类用户手滑5。 典型配搭:表单收三个下拉框 → checkdate 把关 → 过了才进数据库。

第 7 章的文件部分值得当成一条流水线记,而不是一堆孤立函数:

打开 fopen → 读 fgets/fread 或写 fwrite → 关闭 fclose
顺带支线:复制 copy → 移动 rename → 删除 unlink → 整读 file_get_contents

五个最容易踩的点,书里都点了名:

  1. fopen 的返回是"文件句柄",一个 PHP 内部用来指认文件的值; 之后每个文件函数都要把这个 $fh 传回去"对号"6。 不要echo 它,它没有可读内容。
  2. 模式字母决定生死:w 会在文件已存在时清空旧内容, 哪怕你一个字不写;r 只读;a 追加;带 + 的变体同时允许读写。 拿不准就用 a,它是最不容易毁掉数据的模式。
  3. fopen 失败返回 FALSE,书里给的 or die(...) 是教学写法—— 真实网站绝不这样粗暴中止,应该渲染一个出错页面; 但对脚本和小工具,die 的"立刻停下来喊疼"仍是合理的7。
  4. copy / rename / unlink 的失败处理:它们也返回成功标志, 书的 Example 7-8 演示了另一种风格——用 if 判断返回值, 失败时 echo 一句,而不是 die。两种风格都合法,区别在于: 这一步失败是否"整件事没救了"。
  5. file_get_contents 是"一把全读":不需要逐行处理时, 它比 fopen+fgets 循环省事;书里甚至用它抓了 O'Reilly 首页 (Example 7-14),演示同一个函数也能读 URL8。

10.38 多人同时写:file locking 的三个真相

Web 程序常被多人同时访问,两个写操作撞在同一文件上,文件就毁了; 一写一读撞上,读的人会拿到一半新一半旧的怪结果。第 7 章给的解法 是 flock,三个真相要一起记:

  • LOCK_EX 申请排他锁、LOCK_UN 释放。惯例是"贴着写操作": 锁紧挨着 fwrite 之前申请、之后立刻释放,一秒都不多占—— 锁得越久,排队的人越多,网站越慢9。
  • flock 是建议锁(advisory):它只拦"同样尊重锁"的代码。 你项目里若有任何一处直接改文件不走 flock,锁对它就是空气, 照样把你的文件搅乱10。锁是约定,不是围墙。
  • 不是哪儿都灵:NFS 等网络文件系统、老式 FAT 磁盘、 某些多线程服务器模式下,flock 可能根本不生效或拦不住同机其他 PHP 脚本11。小网站同机本地盘可以放心用;上了网络存储, 这是要重新评估的清单项。

还有一条贯穿本节与上传一节的硬规则,原书是加粗级强调的: 凡是文件名、路径来自用户输入的操作(删、移、写),必须确认 这个文件确实该被这个用户动12。这不是风格建议,是安全底线。

10.39 上传处理流水线与安全版改造

Example 7-15 的上传脚本,拆开是五步:

1. 表单 <input type="file" name="filename"> 且 form 带 enctype="multipart/form-data"
2. PHP 收进 $_FILES['filename'],一个自带 name/type/tmp_name/size/error 的小数组
3. PHP 已把内容放在临时位置 tmp_name——这是最容易被忽略的一步:
   你的代码拿到的不是"那个文件",是"PHP 替你保管的临时副本"
4. move_uploaded_file($_FILES['filename']['tmp_name'], $name) 把它挪进正式目录
5. 权限配错时,第 4 步报 Permission denied——那是目录权限问题,不是代码问题[^54]

教学版到此为止。但 Example 7-16 紧接着给出"更安全的版本", 它的关键改造是:$n 不是直接用 $_FILES[...]['name'], 而是 . 拼接 strtolower(end(explode('.', $n)))——即只信扩展名、 重新拼名字,不让上传者给的原始文件名直接落盘 (更严格的现代实践是扩展名白名单+随机重命名,属书外补充; 第 16 章的 sanitizeString 与本书结尾项目的 check 约定是同一条线的延伸)。

10.40 调系统命令:exec 与转义铁律

第 7 章收尾讲了 PHP 可以执行系统命令(exec 系列),Example 7-17 跑了一个外部转换程序。这里的铁律只有一句:命令串里但凡 可能混进用户输入,先过 escapeshellcmd——它清洗命令串, 阻止"本是一个参数、却被拼成第二条命令"的注入14。 这一条和 §10.38 的文件规则、§10.39 的上传规则是同一件事的 三个侧面:凡出界(出 PHP、出浏览器、出你的目录)的数据, 先假定它带恶意,再谈功能。

10.41 练习场(第 7 章专题)

  1. 格式串拼图:用一条 printf 输出一张对齐的小票——品名左对齐 占 12 格、单价右对齐占 8 格补零到两位小数、数量占 4 格。 先写模板,再填数据,体会"图纸先行"。
  2. 日期防御:写 checkdate 版本的三下拉框生日校验,故意提交 2 月 30 日,看它被挡在哪一层。
  3. 计数器加锁:把书里的文本计数器改成 flock 版本,开两个浏览器 标签连点,对比加锁前后 count 值的稳定性。
  4. 上传加固:在 Example 7-16 基础上再加一条——扩展名不在 jpg/png/gif 白名单内的直接拒绝,并重命名落盘文件为随机串。
  5. sprint 复用:把练习 1 的小票格式串抽成一个 sprintf 模板 常量,让"换皮肤"只改一处。

10.42 FAQ(五问)

Q1:printf 和 echo 我到底用哪个? 日常输出用 echo/print;只有需要"按格式对齐、补零、定宽"的输出 (报表、日志、对齐的调试信息)才请 printf 出场。格式串是有学习成本的, 别为它付不用的钱。

Q2:为什么书里日期函数老是 strtotime 不见踪影? 本书第 7 章只用时间戳整数与 date 搭配;strtotime("next Thursday") 这类自然语言解析本书未展开——知道 date 的第二参数是"秒数"即可, 需要的算法自己算秒数传进去。

Q3:flock 加了锁,是不是就不会丢数据了? 不是"不会丢",是"同项目内互相礼让"。建议锁只约束遵守同一套 约定的代码;跨机器、跨语言、或直接改文件的工具不在约束内。 真正的兜底仍是备份与写后校验。

Q4:file_get_contents 读 URL 和 cURL 什么关系? 本书只演示了前者:它简单、能应付"抓一个页面"的需求。 需要自定义请求头、Cookie、POST 数据时,cURL 是更完整的工具 (书外补充)。从 file_get_contents 起步,撞到它的天花板再换。

Q5:这一章的函数记不住怎么办? 不用记。记住的是 §10.37 那条流水线(打开→读写→关)和 "凡出界先转义"的铁律;具体函数名是查文档级别的细节。 查得到的东西不值得占脑容量,查不到方向的判断才值得。

11. 原文地图

主题 原书章 原文位置
printf 与说明符 Chapter 7. Practical PHP text/10-ch07-chapter-7-practical-php.txt:21(搜「There are %d items」)· :28(搜「Table 7-1」)
颜色十六进制实战 同上 text/10-ch07-chapter-7-practical-php.txt:131(搜「color:#%X%X%X」)
精度、填充、从右往左读 同上 text/10-ch07-chapter-7-practical-php.txt:157(搜「123.42 / 12」)· :179(搜「Pad to 15」)· :208(搜「go from right to left」)
字符串填充与截断 同上 text/10-ch07-chapter-7-practical-php.txt:312(搜「%12s」)· :341(搜「cutoff value」)
sprintf 同上 text/10-ch07-chapter-7-practical-php.txt:406(搜「string print」)
时间戳与 mktime 同上 text/10-ch07-chapter-7-practical-php.txt:434(搜「number of seconds since」)· :441(搜「7 days」)· :453(搜「mktime」)
Y2K38 同上 text/10-ch07-chapter-7-practical-php.txt:500(搜「Y2K38」)· :511(搜「DateTime class」)
date 格式与常量 同上 text/10-ch07-chapter-7-practical-php.txt:528(搜「Monday February 17th」)· :536(搜「Table 7-4」)· :752(搜「DATE_RSS」)
checkdate 同上 text/10-ch07-chapter-7-practical-php.txt:778(搜「checkdate」)· :793(搜「September」)
文件 vs 数据库、命名纪律 同上 text/10-ch07-chapter-7-practical-php.txt:812(搜「quicker and more convenient」)· :816(搜「case-sensitive」)
fopen 三步与句柄 同上 text/10-ch07-chapter-7-practical-php.txt:850(搜「Failed to create file」)· :884(搜「Always start by opening」)· :903(搜「file handle」)
模式表与 w 的清空 同上 text/10-ch07-chapter-7-practical-php.txt:911(搜「delete the old contents」)· :920(搜「Table 7-5」)
fgets/fread/copy/rename/unlink 同上 text/10-ch07-chapter-7-practical-php.txt:988(搜「fgets($fh)」)· :1052(搜「copy('testfile.txt'」)· :1093(搜「rename」)· :1122(搜「unlink」)· :1131(搜「safely deleted」)
文件指针与 fseek 同上 text/10-ch07-chapter-7-practical-php.txt:1152(搜「file pointer is the position」)· :1170(搜「SEEK_END」)· :1202(搜「SEEK_SET」)
flock 三叮嘱 同上 text/10-ch07-chapter-7-practical-php.txt:1219(搜「queues up」)· :1246(搜「slow your application」)· :1272(搜「advisory lock」)· :1283(搜「NFS」)
file_get_contents 同上 text/10-ch07-chapter-7-practical-php.txt:1316(搜「file_get_contents」)· :1333(搜「oreilly.com」)
上传骨架与 $_FILES 同上 text/10-ch07-chapter-7-practical-php.txt:1366(搜「multipart/form-data」)· :1396(搜「called twice」)· :1400(搜「$_FILES」)· :1468(搜「tmp_name」)
安全版上传与命名接管 同上 text/10-ch07-chapter-7-practical-php.txt:1558(搜「image/jpeg」)· :1587(搜「not of an accepted type」)· :1621(搜「prechosen names」)· :1630(搜「preg_replace」)
exec 三件套 同上 text/10-ch07-chapter-7-practical-php.txt:1667(搜「exec system call」)· :1685(搜「escapeshellcmd」)· :1698(搜「htmlspecialchars」)· :1757(搜「shared web hosts」)


  1. 出处:「Chapter 7. Practical PHP」第 1746 段(text/10-ch07-chapter-7-practical-php.txt:1746,搜「omit the $output」)。「you can omit the $output and $status parameters, but you will not know the output created by the call or even whether it completed successfully」。 ↩

  2. 出处:「Chapter 7. Practical PHP」第 133 段(text/10-ch07-chapter-7-practical-php.txt:133,搜「pound, or hash」)。三个 %X 拼颜色串的输出见同段。 ↩

  3. 出处:「Chapter 7. Practical PHP」第 406 段(text/10-ch07-chapter-7-practical-php.txt:406,搜「string print」)。「you can send the output to another variable rather than to the browser」。 ↩

  4. 出处:「Chapter 7. Practical PHP」第 752 段(text/10-ch07-chapter-7-practical-php.txt:752,搜「constants with the date command」)。DATE_COOKIE/DATE_RSS/DATE_W3C 列表见第 759–763 段。 ↩

  5. 出处:「Chapter 7. Practical PHP」第 778 段(text/10-ch07-chapter-7-practical-php.txt:778,搜「check whether a user has submitted」)。返回 TRUE/FALSE 的语义同段。 ↩

  6. 出处:「Chapter 7. Practical PHP」第 903 段(text/10-ch07-chapter-7-practical-php.txt:903,搜「file handle」)。「each file-handling function ... must be passed $fh as a parameter」。 ↩

  7. 出处:「Chapter 7. Practical PHP」第 907 段(text/10-ch07-chapter-7-practical-php.txt:907,搜「never abort」)。「A web application would never abort in this crude way」。 ↩

  8. 出处:「Chapter 7. Practical PHP」第 1327 段(text/10-ch07-chapter-7-practical-php.txt:1327,搜「Grabbing the O'Reilly home page」)。 ↩

  9. 出处:「Chapter 7. Practical PHP」第 1246 段(text/10-ch07-chapter-7-practical-php.txt:1246,搜「directly before a change」)。「perform it directly before a change ... unlock it immediately afterward」。 ↩

  10. 出处:「Chapter 7. Practical PHP」第 1272 段(text/10-ch07-chapter-7-practical-php.txt:1272,搜「advisory lock」)。「it locks out only other processes that call the function」。 ↩

  11. 出处:「Chapter 7. Practical PHP」第 1283 段(text/10-ch07-chapter-7-practical-php.txt:1283,搜「NFS」)。 ↩

  12. 出处:「Chapter 7. Practical PHP」第 1131 段(text/10-ch07-chapter-7-practical-php.txt:1131,搜「must always ensure」)。 ↩

  13. 出处:「Chapter 7. Practical PHP」第 1408 段(text/10-ch07-chapter-7-practical-php.txt:1408,搜「temporary location」);Permission denied 见第 1415 段。 ↩

  14. 出处:「Chapter 7. Practical PHP」第 1750 段(text/10-ch07-chapter-7-practical-php.txt:1750,搜「escapeshellcmd function」)。 ↩

  15. 出处:「Chapter 7. Practical PHP」第 16–46 段(text/10-ch07-chapter-7-practical-php.txt:16,搜「conversion specifier」)。Table 7-1 列出全部说明符及以 123 为例的输出。 ↩

  16. 出处:「Chapter 7. Practical PHP」第 107 段(text/10-ch07-chapter-7-practical-php.txt:107,搜「double percent sign」)与第 114 段(:114,搜「as many specifiers」)、第 122 段(:122,搜「too few arguments」)。 ↩

  17. 出处:「Chapter 7. Practical PHP」第 126 段(text/10-ch07-chapter-7-practical-php.txt:126,搜「65 red」)与第 131 段(:131,搜「color:#%X%X%X」)。输出 #417FF5 见第 138 段。 ↩

  18. 出处:「Chapter 7. Practical PHP」第 152 段(text/10-ch07-chapter-7-practical-php.txt:152,搜「123.42 / 12」)与第 171 段(:171,搜「Example 7-1」)。五行输出对照见第 202–206 段。 ↩↩↩

  19. 出处:「Chapter 7. Practical PHP」第 208 段(text/10-ch07-chapter-7-practical-php.txt:208,搜「go from right to left」)。「It works is simple if you go from right to left」;Table 7-2 把五个部件拆成列。 ↩

  20. 出处:「Chapter 7. Practical PHP」第 301 段(text/10-ch07-chapter-7-practical-php.txt:301,搜「String padding」)与第 319 段(:319,搜「cutoff」)、第 341 段(:341,搜「equal to or greater than」)。「Rasmus Lerdorf」三行输出见第 337–339 段。 ↩↩↩

  21. 出处:「Chapter 7. Practical PHP」第 406 段(text/10-ch07-chapter-7-practical-php.txt:406,搜「string print」)。「send the output to another variable rather than to the browser」。 ↩↩

  22. 出处:「Chapter 7. Practical PHP」第 434 段(text/10-ch07-chapter-7-practical-php.txt:434,搜「number of seconds since」)。「standard Unix timestamps, which are simply the number of seconds since the start of January 1, 1970」。 ↩

  23. 出处:「Chapter 7. Practical PHP」第 441 段(text/10-ch07-chapter-7-practical-php.txt:441,搜「7 days」)。「adds the result of 7 days × 24 hours × 60 minutes × 60 seconds」。 ↩

  24. 出处:「Chapter 7. Practical PHP」第 448 段(text/10-ch07-chapter-7-practical-php.txt:448,搜「1827619200」)与第 455 段(:455,搜「in order from left to right」)。参数顺序:时、分、秒、月、日、年。 ↩↩

  25. 出处:「Chapter 7. Practical PHP」第 500 段(text/10-ch07-chapter-7-practical-php.txt:500,搜「Y2K38」)与第 507 段(:507,搜「January 19, 2038」)。 ↩↩

  26. 出处:「Chapter 7. Practical PHP」第 511 段(text/10-ch07-chapter-7-practical-php.txt:511,搜「DateTime class」)。「it will work only on 64-bit architecture... do check before you use it」。 ↩↩

  27. 出处:「Chapter 7. Practical PHP」第 528 段(text/10-ch07-chapter-7-practical-php.txt:528,搜「Monday February 17th」)与第 536 段(:536,搜「Table 7-4」)。完整格式字符表。 ↩↩

  28. 出处:「Chapter 7. Practical PHP」第 752 段(text/10-ch07-chapter-7-practical-php.txt:752,搜「DATE_RSS」)。「date(DATE_RSS) returns the current date and time in the valid format for an RSS feed」。 ↩

  29. 出处:「Chapter 7. Practical PHP」第 778 段(text/10-ch07-chapter-7-practical-php.txt:778,搜「checkdate」)与第 782 段(:782,搜「September 31」)。 ↩

  30. 出处:「Chapter 7. Practical PHP」第 812 段(text/10-ch07-chapter-7-practical-php.txt:812,搜「quicker and more convenient」)。改用户头像、处理日志是原书点名的两个文件场景。 ↩↩

  31. 出处:「Chapter 7. Practical PHP」第 816 段(text/10-ch07-chapter-7-practical-php.txt:816,搜「case-sensitive」)。「you should always assume that the system is case-sensitive and stick to a convention such as all-lowercase filenames」。 ↩↩

  32. 出处:「Chapter 7. Practical PHP」第 884 段(text/10-ch07-chapter-7-practical-php.txt:884,搜「Always start by opening」)与第 903 段(:903,搜「file handle」)。「Every open file requires a file resource」;句柄内容不用操心,原话在同段。 ↩

  33. 出处:「Chapter 7. Practical PHP」第 907 段(text/10-ch07-chapter-7-practical-php.txt:907,搜「FALSE will be returned」)。「A web application would never abort in this crude way」——die 的教学性限定。 ↩↩↩

  34. 出处:「Chapter 7. Practical PHP」第 974 段(text/10-ch07-chapter-7-practical-php.txt:974,搜「grab a whole line」)与第 1016 段(:1016,搜「fread($fh, 3)」)。输出 Lin 见第 1028 段。 ↩↩

  35. 出处:「Chapter 7. Practical PHP」第 911 段(text/10-ch07-chapter-7-practical-php.txt:911,搜「delete the old contents」)与第 920 段(:920,搜「Table 7-5」)。「the w mode parameter causes the fopen call to delete the old contents (even if you don't write anything new!)」。 ↩↩↩

  36. 出处:「Chapter 7. Practical PHP」第 1152 段(text/10-ch07-chapter-7-practical-php.txt:1152,搜「file pointer is the position」)。「It is not the same as the file handle」。 ↩

  37. 出处:「Chapter 7. Practical PHP」第 1179 段(text/10-ch07-chapter-7-practical-php.txt:1179,搜「right to the file end」)。更新例的完整步骤与结果文件(四行)在同段。 ↩

  38. 出处:「Chapter 7. Practical PHP」第 1052 段(text/10-ch07-chapter-7-practical-php.txt:1052,搜「copy('testfile.txt'」)与第 1069 段(:1069,搜「NOT」)。if (!copy(...)) 的简写读法:「If not able to copy...」。 ↩

  39. 出处:「Chapter 7. Practical PHP」第 1082 段(text/10-ch07-chapter-7-practical-php.txt:1082,搜「rename function」)与第 1100 段(:1100,搜「directories」)。 ↩

  40. 出处:「Chapter 7. Practical PHP」第 1111 段(text/10-ch07-chapter-7-practical-php.txt:1111,搜「unlink」)。 ↩↩

  41. 出处:「Chapter 7. Practical PHP」第 1131 段(text/10-ch07-chapter-7-practical-php.txt:1131,搜「safely deleted」)。「you must make absolutely certain it is a file that can be safely deleted and that the user is allowed to delete it」。 ↩

  42. 出处:「Chapter 7. Practical PHP」第 1304 段(text/10-ch07-chapter-7-practical-php.txt:1304,搜「without having to use file handles」)与第 1333 段(:1333,搜「oreilly.com」)。「you also can use it to fetch a file from a server across the internet」。 ↩↩↩

  43. 出处:「Chapter 7. Practical PHP」第 1219 段(text/10-ch07-chapter-7-practical-php.txt:1219,搜「queues up」)。「If more than one person tries to write to a file simultaneously, it can become corrupted」;flock「queues up all other requests」。 ↩

  44. 出处:「Chapter 7. Practical PHP」第 1233 段(text/10-ch07-chapter-7-practical-php.txt:1233,搜「flock($fh, LOCK_EX)」)与第 1268 段(:1268,搜「not supported on all systems」)。 ↩

  45. 出处:「Chapter 7. Practical PHP」第 1246 段(text/10-ch07-chapter-7-practical-php.txt:1246,搜「slow your application」)。「perform it directly before a change... then unlock it immediately afterward」。 ↩↩↩

  46. 出处:「Chapter 7. Practical PHP」第 1272 段(text/10-ch07-chapter-7-practical-php.txt:1272,搜「advisory lock」)与第 1276 段(:1276,搜「extremely hard-to-locate bug」)。 ↩↩

  47. 出处:「Chapter 7. Practical PHP」第 1283 段(text/10-ch07-chapter-7-practical-php.txt:1283,搜「NFS」)。「flock will not work on NFS and many other networked filesystems」;试锁建议在第 1287 段。 ↩↩

  48. 出处:「Chapter 7. Practical PHP」第 1353 段(text/10-ch07-chapter-7-practical-php.txt:1353,搜「multipart/form-data」)。「choose a special type of encoding called multipart/form-data, and your browser will handle the rest」。 ↩↩

  49. 出处:「Chapter 7. Practical PHP」第 1375 段(text/10-ch07-chapter-7-practical-php.txt:1375,搜「move_uploaded_file」)与第 1408 段(:1408,搜「temporary location」)。「move the uploaded file from the temporary location in which PHP stored it to a more permanent one」。 ↩↩↩

  50. 出处:「Chapter 7. Practical PHP」第 1438 段(text/10-ch07-chapter-7-practical-php.txt:1438,搜「Five things」)。Table 7-6 列出 $_FILES 的五项:name/type/size/tmp_name/error。 ↩↩↩

  51. 出处:「Chapter 7. Practical PHP」第 1396 段(text/10-ch07-chapter-7-practical-php.txt:1396,搜「called twice」)。「a single program is called twice: once... (an HTTP GET method request) and again when the user clicks the submit button (an HTTP POST method request)」。 ↩

  52. 出处:「Chapter 7. Practical PHP」第 1558 段(text/10-ch07-chapter-7-practical-php.txt:1558,搜「image/jpeg」)与第 1587 段(:1587,搜「not of an accepted type」)。switch 的 default 置空 $ext。 ↩

  53. 出处:「Chapter 7. Practical PHP」第 1594 段(text/10-ch07-chapter-7-practical-php.txt:1594,搜「modified or changed」)。「the file type still comes from the browser and can be modified or changed by the user uploading the file」;「if the file ever could be executable, you should not rely on information you have not ascertained to be absolutely correct」。 ↩↩

  54. 出处:「Chapter 7. Practical PHP」第 1621 段(text/10-ch07-chapter-7-practical-php.txt:1621,搜「prechosen names」)与第 1630 段(:1630,搜「preg_replace」)。「I strongly advise you to use a similar approach and have prechosen names and locations for uploaded files」;过滤串「leaves only the characters A–Z, a–z, 0–9 and periods」。 ↩↩↩

  55. 出处:「Chapter 7. Practical PHP」第 1611 段(text/10-ch07-chapter-7-practical-php.txt:1611,搜「automatically removed」)。 ↩↩

  56. 出处:「Chapter 7. Practical PHP」第 1667 段(text/10-ch07-chapter-7-practical-php.txt:1667,搜「exec system call」)与第 1750 段(:1750,搜「escapeshellcmd」)、第 1698 段(:1698,搜「htmlspecialchars」)、第 1757 段(:1757,搜「shared web hosts」)。「You should always try to solve your problems within PHP if you can」。 ↩↩↩↩↩↩

  57. 出处:「Chapter 7. Practical PHP」第 844–861 段(text/10-ch07-chapter-7-practical-php.txt:844,搜「Example 7-4」)。testfile.php 的完整代码与成功消息;「Should a program call the die function, the open file will be automatically closed as part of terminating the program」见第 865 段。 ↩