综合项目 Robin's Nest — 用三件套从零造一个社交网站
综合项目 Robin's Nest — 用三件套从零造一个社交网站
这一章讲什么: 全书的落点。前面二十一章教的每一件手艺—— PHP 语法、PDO、会话、密码哈希、表单、上传、GD 图像、JavaScript 事件、 fetch 异步、CSS 布局——在这里第一次同台演出,搭出一个能跑的社交网站: 注册、登录、资料、好友、私信,一样不缺。 读法也和前各章不同:这不是「学新东西」,是「看旧东西怎么配合」。 读之前需要本组 02–12 全部——每一节都在回收前文。
1. 顶层全景:一张文件地图
项目叫 Robin's Nest(用的话记得换掉 logo 和名字)。功能清单一共六项: 注册登录、会话控制、带头像的个人资料、成员目录、加好友、公开/私信1。 全部代码约十几个文件,摆成三层:
共享层(每个页面都靠它们)
functions.php ── 数据库连接 + destroySession() + showProfile()
header.php ── session_start + 菜单骨架(登录前后两套)
javascript.js ── byId / style / by 三小函数
styles.css ── 全站样式
安装层(跑一次)
setup.php ── CREATE TABLE IF NOT EXISTS × 4(members/messages/friends/profiles)
功能层(每个一个文件)
index.php 首页 · signup.php 注册(+checkuser.php 异步查重)
login.php 登录 · logout.php 登出
profile.php 资料(文本+图片) · members.php 成员/加友
friends.php 好友列表 · messages.php 消息
数据流:浏览器 ──▶ 某功能页 ──▶ (header.php ──▶ functions.php ──▶ PDO) ──▶ MySQL
│
└─▶ HTML 里嵌 JS(fetch 回 call 功能页)
图说:所有页面共用一条「头部+连接」骨架;功能页只写自己的一段;JS 只在注册页出场。这张地图本身就是第 4 章「模块化」主张的兑现:一个文件一个功能, 公共件收进 include——没有单文件巨怪,也没有逻辑散落一地2。
1.1 一条完整的时间线(全书合龙的主线例子)
把「新访客 Jill 从进门到发出第一条私信」走一遍——全项目一半以上的文件 都会在这条线上出场,每一步旁边有具体状态:
- t0 安装:管理员跑 setup.php → 四张表建成;Jill 打开
localhost/robinsnest→ index.php 经 header 渲染,「Welcome Guest」+ 三键菜单。 - t1 注册:Jill 填用户名 jill、移开光标 → blur → fetch 到 checkuser.php → members 表查 0 行 → 回「✔ available」显示在输入框旁。 填密码点 Sign Up → signup.php 查重再查一次 → 占位符 INSERT 一行 (jill, 密码哈希) → 显示「Account created,Please Log in」。
- t2 登录:jill 输密码 → login.php 占位符查出哈希 → password_verify
通过 →
$_SESSION['user']='jill'→ header('Location: members.php?view=jill') → 浏览器跳到成员页,header now 显示「Logged in as: jill」,菜单变六键。 - t3 资料:Jill 在 profile.php 写「Coffee, code, cats.」,传一张 800×600 的猫图 → move 存成 jill.jpg → getimagesize 判 JPEG → 等比缩到 100×75 → 卷积锐化 → imagejpeg 落盘 → showProfile 回显:头像居左 + 那句话。
- t4 社交:成员页见朋友 Jack → 点 follow → members.php?add=jack 占位符 INSERT 进 friends(user=jill, friend=jack)。 Jack 也 follow 了 jill → friends.php 里 array_intersect 算出互粉, 两人的好友页都显示「mutual friends: 对方」。
- t5 私信:Jill 在 messages.php?view=jack 写「hi」,选 Private → INSERT 一行(auth=jill, recip=jack, pm=1, time=秒数, message=hi)。 Jack 在自己的消息页看到 whispered: hi(斜体);别的用户看不到这行。
- t6 登出:Jill 点 Log out → destroySession 三连 → 跳回首页, 页面重新显示「Welcome Guest」。
七个时刻,每一步都是前文某一章的原语在干活——这个项目没有一行新魔法, 它把旧魔法排成了一条流水线。下文按文件逐个拆开看。
2. 地基三件套(主线例子的起点)
2.1 functions.php:连接 + 两个公共函数
凭据(教学默认 robinsnest/password)+ PDO 连接,前文 login.php 的原样复用;
顶上两个函数服务全站:destroySession()(登出用,第 12 章三件套:
清 $_SESSION、过期 cookie、session_destroy)3;
showProfile($user, $pdo)(显示某用户资料:有 $user.jpg 就 <img> 显示,
再查 profiles 表打印「about me」,文本过 htmlentities;查不到就显示占位句)4。
把 showProfile 走一遍,因为它是全项目「展示他人内容」的唯一通道—— 所有页面显示任何人的资料都走它,消毒因此也只有这一处要守:
- 调
file_exists("$user.jpg"):这个用户的头像文件在不在磁盘上? 在 →<img src='$user.jpg'>浮左显示。文件名来自用户名, 而用户名入库时已被注册流程约束过(非空),此处不再注入风险。 prepare("SELECT * FROM profiles WHERE user=?")+execute([$user])——占位符查询该用户的资料行。fetch()取行:有 →htmlentities($row['text'])输出「about me」 (用户写过的任何 HTML 在这里全部转义,防 XSS);无 → 输出 「Nothing to see here, yet」占位。<br style='clear:left'>清掉头像的浮动,让后续内容回落正常流。
成员页、好友页、消息页、资料页四处显示资料,全走这一个函数—— 消毒点收得越少,守得越牢(第 11 章「htmlentities 只在输出那一刻做一次」 的多页面版)4。
注意一个细节:showProfile 需要操作数据库,所以它把 $pdo 当参数收进来—— 不靠全局,靠参数(第 3 章「参数正门」的又一次现身)。 原书还专门 Note:这一版从旧版的 mysql/mysqli 全面换到 PDO + 占位符, 注入防线贯穿全项目5。
2.2 header.php:每个页面的「出生证明」
每个功能页第一行都是 require_once 'header.php'。header.php 干四件事:
session_start()—— 开启会话(必须在任何输出前);- include functions.php(所以功能页只 require 一次 header 就够);
- 认人:
if (isset($_SESSION['user']))→$loggedin = TRUE, 取出用户名(过 htmlentities)写进页头问候;否则$loggedin = FALSE、 显示「Welcome Guest」6; - 输出 HTML 头 + 双菜单:未登录只见 Home/Sign Up/Log In, 登录后见 Home/Members/Friends/Messages/Edit Profile/Log out 全套7。
把 header.php 的一次执行走到底(它每个页面都跑一遍,值得看清):
- 某功能页被请求 → 第一行 require header.php → PHP 转入 header。
- header 第一行
session_start()——PHP 检查请求里的会话 cookie: 有,就按 ID 打开服务器上的会话数据(第 12 章的「凭号领物」); 没有,就新建一个(本页会写入,响应头带新 cookie)。 - require functions.php——
$pdo此刻建成,全页可用; destroySession 与 showProfile 两个函数就绪。 - 判定
$_SESSION['user']:有值 → 会话里躺着用户名 → $loggedin=TRUE, 用户名字符串过 htmlentities 存进$user_html_entities(与它的脏版隔离); 无值 → $loggedin=FALSE。 - 输出
<!DOCTYPE html>…<body>的开头 HTML:标题里嵌问候语 (<title>Robin's Nest: Logged in as: jill</title>),链上 styles.css、 javascript.js、Bootstrap Icons CDN。 - 按 $loggedin 二选一输出菜单块;随后
</div></div><div class="content">开内容容器——收尾留给功能页(各功能页最后自己补</div></body></html>)。 - 控制回到功能页,从自己的业务代码继续。
这个「骨架 include」模式是全书反复出现的组织技巧:会话、连接、菜单、页头 只写一次,每个功能页从第 5 步之后开始写自己的内容。加一个新页面 = 写一个新文件 + 一行 require,骨架零拷贝67。
$loggedin 是全站的门禁变量:每个功能页拿到它,第一件事就是
if (!$loggedin) die(...) 踢人——未登录直接终止,连表单都不渲染8。
图标用 Bootstrap Icons(<i class="bi-house-door-fill"></i> 这类,
CDN 引入)9。
2.3 setup.php:四张表
CREATE TABLE IF NOT EXISTS 四连——建 members(user + pass)、
messages(id/auth/recip/pm/time/message)、friends(user + friend)、
profiles(user + text);带索引(用户名前 6 字符的前缀索引,第 8 章的手艺)。
IF NOT EXISTS 让 setup 可以反复跑不报错10。库和用户要先用第 8 章的
CREATE DATABASE/CREATE USER 建好(教学口令仍是 password,生产必须换)11。
跑 setup 的顺序不能乱:原书给了 Warning——先建好 $db 指定的数据库、 给 $dbuser 授权,再跑 setup.php;否则四条 CREATE TABLE 全部失败, 后续每个页面都会抛 PDO 异常。这是「依赖先行」这个工程常识的第一次现身: 安装脚本自己也是用户,也得先通过数据库的认证与授权11。
把四张表的「将来怎么用」摆在一起看,设计意图就清楚了:
| 表 | 列(简) | 回答的问题 |
|---|---|---|
| members | user(索引)、pass(255) | 谁是注册用户、他的密码哈希 |
| friends | user、friend(都索引) | 谁关注了谁(单向一行) |
| profiles | user(索引)、text(4096) | 谁的「about me」写了什么 |
| messages | id(主键,自增)、auth、recip、pm、time、message | 谁给谁、何时、公开还是私、说了什么 |
两张表的设计决策值得单独点名:members.pass 留 255——第 12 章 password_hash 可能变长的迁移余量;messages 的 auth 和 recip 各建一个 前缀索引——成员页按 recip 拉消息、按 auth 显示来源,两条查询路径都要快; friends 不建主键、允许方向行重复存储——关系不靠约束,靠查询时的语义 (这是教学简化,第 9 章规范化的严格版会加约束)10。
3. 注册:异步查重的第一次实战(主线例子)
signup.php 是全书第一次把「双端验证 + 异步通信」放进真实功能:
PHP 端(提交后):字段非空检查 → 占位符查重(SELECT … WHERE user=?)→
已被占则报错;没占则用 password_hash($_POST['pass'], PASSWORD_DEFAULT)
存哈希(第 12 章:只存影子不存本人)INSERT 进 members,然后
die('<h4>Account created</h4>Please Log in.') 收场12。
把 PHP 端的决策树走一遍(每一支都对应过前文的某章):
POST 到了 signup.php
├─ 会话里已有 user?(残留登录态)→ destroySession() 清掉 ← 第 12 章
├─ user 或 pass 有空? → $error = 'Not all fields…' ← 第 11 章非空校验
├─ SELECT user=? 有行? → $error = 'That username…' ← 第 16 章查重
└─ 以上都过 → INSERT (user, password_hash(pass))
→ die('Account created…') ← 第 12 章哈希
最后:表单回显 $user/$error,均过 htmlentities ← 第 10/11 章消毒两个设计动作值得点名:其一,注册前主动 destroySession——若浏览器里
还挂着别人(或自己)的半登录态,先清干净再注册,免得新账号和旧会话搅在一起;
其二,成功后用 die() 终止——直接输出完成页,不让代码落到下面表单渲染
的部分(「处理完即止」的原教旨写法,比第 16 章「重定向」的土)12。
JS 端(输入时):用户名字段的 blur 事件(焦点离开)挂一个箭头函数——
空值直接返回;有值就 fetch('checkuser.php', {method:'post', body: FormData(user: 输入值)}),回来把结果(红叉「taken」或绿勾「available」)
写进输入框旁的 <span id="used">。这就是第 1 章 Gmail 用户名检查那个例子
的亲手复刻——第 17 章练的 fetch,在这里第一次为真实功能服务13。
checkuser.php 极简:收到 user 就占位符查 members,rowCount 为 1 输出 带 ✘ 的「taken」,为 0 输出带 ✔ 的「available」;颜色由 CSS 类 taken/available 控制14。
把这次异步问答的完整时序走一遍(两个文件、一次无刷新往返):
signup.php(浏览器) checkuser.php(服务器)
─────────────────── ───────────────────
blur 触发(field.value 非空)
→ FormData 装 user=jill
→ fetch('checkuser.php', {post}) ──▶ require functions.php($pdo 就绪)
prepare('SELECT * FROM members WHERE user=?')
execute(['jill']) → rowCount
├─ 1 → echo "<span class='taken'>✘ …is taken</span>"
└─ 0 → echo "<span class='available'>✔ …is available</span>"
◀── 响应体是一段 HTML ──────────────
.then(text => byId('used').innerHTML = text)
→ span#used 的内容被替换
→ CSS 类把字染红或染绿
图说:PHP 回的不是「数据」而是「一段可直接显示的 HTML」——
最粗但也最省事的通信格式(第 17 章 JSON 形式的反面)。两个细节值得注意:其一,返回的是 HTML 而非 JSON——与第 17 章 jsonget.php
的 {html, color} 结构相比,这里把「染红/染绿」也一并装进 HTML(类名),
JS 端一行 innerHTML 即完工;代价是两端耦合(改样式要动 PHP)。
其二,这段 HTML 进了 innerHTML——而它的内容里有 $_POST['user'] 的回显
(虽然过了 htmlentities)。innerHTML 插用户内容正是第 17 章警告的场景;
此处靠 checkuser.php 里的消毒挡着,但假如将来有人改这段代码忘了消毒,
洞就开了——innerText 是更稳的选择(第 17 章注脚的实战教训)14。
把「注册一次」从头到尾走一遍(这一步覆盖全项目一半的手艺):
- 访客填 Robin,光标移开用户名框( blur )。
- JS 端 fetch 异步发到 checkuser.php;PHP 占位符查 members → 0 行 → 回传绿勾「available」→ span 里出现提示。页面没有刷新。
- 访客填密码点 Sign Up → POST 到 signup.php 自身(第 11 章模式)。
- signup.php 经 header 拿到会话;先
destroySession()清掉任何旧登录态 (防半登录残态);再查重、插库(密码哈希入库)。 - 成功:die 输出「Account created,Please Log in」。
- 访客去 login.php 登录。
4. 登录:password_verify 与「登录态的诞生」
login.php 流程:非空检查 → 占位符按用户名查 members →
password_verify($_POST['pass'], $result[0]['pass'])——库里是哈希,
不能比对字符串,只能把「用户输入的密码」和「库里哈希」一起喂给 verify;
不通过则「Invalid login attempt」(不区分「没这用户」和「密码错」,
避免给攻击者免费的用户名探测)15。
通过后的三行是全书最重要的小段之一:
$_SESSION['user'] = $user;
header('Location: members.php?view=' . $user);会话变量 user 被赋上用户名——从此 header.php 在每个页面都能认出这个人;
header('Location: …') 是 HTTP 重定向,登录成功直接把浏览器踢去成员页。
原书特意叮嘱:会话里只存用户名,不存密码更不存哈希——会话数据躺在
服务器文件里,存了就是明文风险16。
把「登录态诞生」与「页面认人」的接力赛走清(这是第 12 章会话机制的完整实战):
- login.php 验证通过 →
$_SESSION['user'] = $user——此刻 PHP 把 这个键值写进服务器端的会话文件;响应头里,会话 cookie(若本轮新建) 被浏览器收下。 - 浏览器带着 cookie 请求 members.php → header.php 的 session_start()
按 cookie 里的 ID 打开同一份会话文件 →
$_SESSION['user']有值 → $loggedin=TRUE、菜单变六键。 - members.php 要用用户名:从
$_SESSION['user']取——不碰表单、不碰 URL, 身份只从会话读(view 参数另算,那是「看谁的资料」,不是「我是谁」)。 - 点 Log out → destroySession 三连 → 会话文件销毁、cookie 过期 → 下一跳请求,header 看到的 $_SESSION 是空的,一切回到 Guest。
第 12 章那句「跨页传递的不是数据,是一个号」在这里长成了血肉: 整个项目所有页面的「我是谁」,都系于会话文件里的一行16。
5. profile.php:资料 + 图片流水线
功能:一段「about me」文本 + 一张头像(自动缩放到最长边 100 像素)。
文本:preg_replace('/\s\s+/', ' ', …) 把连续空白(含换行)压成单空格;
有资料就 UPDATE,没有就 INSERT——两条 prepare 只 execute 一次
(第 10 章占位符命名版);没提交时反向查库,把已有文本(过 htmlentities)
预填进 textarea17。
图片:表单 enctype="multipart/form-data" + <input type="file">
(第 7 章的骨架),处理后是一条五站流水线:
move_uploaded_file($_FILES['image']['tmp_name'], "$user.jpg")—— 文件名用用户名定死(Jill → Jill.jpg),不给用户输入文件名的机会 (第 7 章安全纪律的实战版)18;getimagesize()验明正身——Warning 原话:别信$_FILES里的 type 字段,那是浏览器报的,可伪造;getimagesize 是读文件内容判型, 只放行 GIF/JPEG/PNG 三种 IMAGETYPE19;- 按类型用
imagecreatefromgif/jpeg/png载入成 PHP 可处理的图像; - 算缩放:最长边压到 $max=100,短边等比;
imagecreatetruecolor开画布,imagecopyresampled重采样,imageconvolution锐化(重采样会轻微糊, 用卷积拉回来),imagejpeg存回 $saveto; imagedestroy释放两块画布(第 5 章「资源自己占的自己放」)20。
把缩放数学走一遍(设 $max = 100,上传图 800×600):宽大于高,
进入第一分支——$th = 100 / 800 × 600 = 75,$tw = 100:新图 100×75,
长边恰压到 100、比例不变。若传的是竖图 600×800,走第二分支得 75×100;
方图 800×800 走第三分支,两边同压 100×100。三个分支合起来就是
「等比缩到最长边 100」的完整实现,改 $max 一个数就换缩略尺寸。
重采样(imagecopyresampled)比简单压缩像素平滑得多,代价是轻微发糊——
所以紧跟一个 3×3 卷积核(-1 -1 -1 / -1 16 -1 / -1 -1 -1,除数 8)
做锐化;这是图像处理课本里「反锐化掩模」的极简版20。
保存后再调 showProfile 把当前资料渲染出来,用户即时看到结果21。
6. members.php:目录与「加友/解除」
三种模式一个文件:?view=某人 看其资料(顺便给 View messages 链接);
?add=某人 占位符防重复 INSERT 进 friends;?remove=某人 占位符 DELETE22。
view 模式值得单独走一遍——它是「看别人的资料」的完整链路,也是参数消毒 的又一个样板:
isset($_GET['view'])为真 →$view = $_GET['view']——URL 里的参数 原样收下,但立刻分流:一个脏版($view,用于查库、比较), 一个干净版($view_html_entities = htmlentities($view),用于输出和拼 URL)。- 判断
$view === $user:看的是自己 → 标题用「Your Profile」; 看的是别人 → 「jill's Profile」(用干净版拼)。 - 调
showProfile($view, $pdo)—— 用脏版查库(占位符会处理好它), 输出的消毒在 showProfile 内部完成。 - 附一个「View jill's messages」按钮,链接的 view 参数也用干净版。
die("</div></body></html>")——view 模式到此为止,不落到下面的成员列表。
脏版走数据库、净版走 HTML——这一个「一进两出」的纪律,在本章的每个文件里 都反复出现(signup 的 $user/$user_html_entities、members 的 $view/ $view_html_entities、messages 的三份 _html_entities)。它是第 10 章 「进消毒、出消毒」在工程规模下的常态:每个页面维护自己的成对变量22。
成员列表是本章最有教学密度的一段循环:每取一个成员,做两次 friends 查询——
t1:对方在 friends 里是不是 user=对方,friend=我(我关注对方);
t2:user=我,friend=对方(对方关注我)。t1 有 →「you are following」+ drop 链接;
t2 有 →「is following you」+ recip(回关)链接;t1+t2>1 →「mutual friend」。
方向性全部靠「user/friend 两列谁是谁」表达,查两次、看组合——
这是第 9 章多对多中间表(简化版)的直接复用23。
把 t1/t2 的四种组合摆成真值表,这段循环的每个分支都有了坐标:
| t1(我→对方) | t2(对方→我) | 显示文字 | 给出的链接 |
|---|---|---|---|
| 0 | 0 | (无关系标注) | follow |
| 1 | 0 | you are following | drop |
| 0 | 1 | is following you | recip |
| 1 | 1 | is a mutual friend | drop |
循环体的骨架因此只有四层:取一个成员 → 两次占位符查询算 t1/t2 → 按上表输出关系词 → 按 !$t1 还是 $t1 给 add/remove 链接。 「加好友」在这里不含糊成一句 SQL,而是「一次带查重的插入」: 先 SELECT 同样两列的行,rowCount 为 0 才 INSERT——重复点击、重复提交 都不会造出两行关系22。
7. friends.php:互粉 = 数组求交
friends 表存「我→你」单向行;互粉是两行都在。列表页的做法:查出两个数组 ——$followers(以我为 user 的所有 friend)与 $following(以我为 friend 的所有 user),然后:
$mutual = array_intersect($followers, $following);
$followers = array_diff($followers, $mutual);
$following = array_diff($following, $mutual);交集是互粉,差集是单向——三行数组函数把「三个分类」算得清清楚楚, 再用 foreach 分组显示($name1/$name2/$name3 的小把戏让看自己的列表时 显示 Your/You are 而不是用户名)24。第 6 章数组函数在这里打了一场 漂亮的实战。
把三行函数的数据流走一遍(设:$followers=['jack','anna'], $following=['jack','bob']):
array_intersect对两个数组取交——只有 'jack' 两边都在 → $mutual = 'jack';array_diff($followers, $mutual)——在 followers 里但不在 mutual 里的: 'anna'(我的单向粉丝);array_diff($following, $mutual)——'bob'(我在单向关注的人)。- 三个数组各 foreach 一段,分组标题用 $name2/$name3 区分「看别人的列表时 显示 jill's mutual friends,看自己的列表时显示 Your mutual friends」; sizeof 判空,三组全空则「You don't have any friends yet」。
这六行(查两次 + 三行数组函数 + 三个 foreach)就是「好友页」的全部业务逻辑。 对比 members.php 用 t1/t2 逐人判定,friends.php 用集合运算一次性分组—— 同一批数据,两种算法,各有所长:前者要逐人给链接(没法预分组), 后者只分组不连线(不用逐人判定)24。
8. messages.php:公开与私信
发消息:INSERT 进 messages,pm 字段 0 公开 / 1 私密(单选按钮),
time 存时间戳(第 7 章的秒数);?erase=消息id 时占位符 DELETE
(只删「收件人是我」的,不能删别人的)25。
看消息:WHERE recip=? ORDER BY time DESC 拉全部,显示时判定:
if (!$pm || auth===我 || recip===我) —— 公开的全可见;私密的只有
收发双方可见;私密显示成 whispered 斜体。时间戳用 date('M jS 'y g:ia:', $row['time']) 翻译成「Jul 4th '25 2:30pm」样式,并先
date_default_timezone_set('UTC') 定口径26。
最后给一个「Refresh messages」链接(轮询式刷新,朴素但诚实)。
把可见性判定走成一张矩阵——这是 messages.php 的核心权限模型:
| 消息类型 | 浏览者=发件人 | 浏览者=收件人 | 第三者 |
|---|---|---|---|
| 公开(pm=0) | 可见(wrote:) | 可见(wrote:) | 可见(wrote:) |
| 私密(pm=1) | 可见(whispered: 斜体) | 可见(whispered: 斜体) | 不可见——整行跳过 |
代码里这一个条件 !$pm || $auth === $user || $recip === $user
把三列一次管住:公开的绕过身份、私密的只认两端。
erase 链接只发给收件人(if ($row['recip'] === $user)),
DELETE 语句又带 AND recip=? 双保险——界面不显示不算数,SQL 里再拦一道:
你手工拼个 ?erase=别人的消息id 也没用,占位符那条 WHERE 找不到行25。
发消息一侧的 pm 字段值得多看一眼:单选按钮 value="0"(公开,默认勾选)
与 value="1"(私密),INSERT 时 (int)$_POST['pm'] 强转整数后入库——
(int) 是第二道小闸:就算攻击者把 pm 改成 "1; DROP TABLE",
强转后只剩 1,拼不进 SQL(其实占位符 already 挡死,双保险)25。
9. 收尾三文件与全书纪律的最终盘点
logout.php 三行:destroySession(); header('Location: index.php');
——第 12 章登出三件套的成品形态27。
把 logout 的一次请求走一遍(它的简短正因为它站在 destroySession 肩上):
浏览器请求 logout.php → header.php 先行(session_start、$pdo、菜单)——
等等,这里有个细节:logout 也会输出菜单 HTML 的开头部分,但紧随其后的
destroySession + header('Location: index.php') 发出重定向头——
header 重定向必须在任何输出之前(header.php 已经输出了 !)。
实际流程是:有登录态时 destroySession 后 header 跳转,已输出的 HTML 被浏览器
弃置(重定向头优先于已缓冲的输出,PHP 默认输出缓冲够小时常成立,但严格说
这是本项目又一处「靠默认配置侥幸成立」的写法;最干净的做法是先判断再输出)——
读这段时值得意识到「能跑」与「规范」之间的距离27。
styles.css 一张:* 通用选择器定全局字体;.content 用 max-width+margin:auto
做「窄屏自适应居中」;.button 系列把链接画成按钮(:hover 变色、:focus 蓝光、
<i> 图标配色);.taken 红 / .available 绿 / .whisper 斜体——状态色全在 CSS28。
javascript.js 就三函数:byId/style/by(例 19-4 的原样回收)29。
顺带一提:这是全书「脚手架函数」的最终归宿——第 19 章为教学造的三小函数,
在一个真实项目里继续服役(signup.php 的 blur 处理器里那声 byId('username')
就是它们)。好脚手架的标志:跨章、跨项目,不随着例子结束而被丢弃29。
把 CSS 按「分工」读,一张表看清每个选择器在管谁:
| 选择器 | 管什么 | 对应章节的手艺 |
|---|---|---|
* | 全局字体/字号 | 第 18 章通用选择器 |
.content | max-width 700px + margin auto | 第 18 章「内容窄屏自适应居中」 |
.button / :hover / :focus | 链接画成按钮;悬停灰底、焦点蓝晕 | 第 18 章伪类与 transition 思想 |
.username / #logo | 页头条:配色、字影、97px 大标 | 第 18 章 text-shadow |
.taken / .available / .whisper | 状态色:红=被占、绿=可用、斜体=私语 | 第 18 章类选择器;语义色集中管理 |
img / #robin | 图片统一边框阴影;logo 图单独去阴影 | 第 18 章 ID 选择器优先级 |
这张表也说明了一件事:这个项目的前端样式全部用第 18 章的基础件拼成 ——没有 flexbox/grid 布局(页面是单列流式),没有 React,没有构建工具。 样式层的「朴素」是刻意的:这本书的重心在 PHP/MySQL/JS 的合流, 呈现层够用就好28。
把这章的安全纪律收拢成一张「全书纪律执行清单」——项目里每一处都在执行:
| 纪律(出自哪章) | 项目里的执行 |
|---|---|
| 占位符防注入(第 10 章) | 全项目每一句 SQL 都 prepare+execute,无一拼串5 |
| htmlentities 防 XSS(第 10/11 章) | 用户名、资料文本、消息、错误提示,输出即消毒6 |
| 密码哈希(第 12 章) | password_hash 入库、password_verify 校验,明文永不落盘12 |
| 会话只存用户名(第 12 章) | $_SESSION['user'] 之外别无它物16 |
| 最小权限 DB 用户(第 8 章) | 专用 robinsnest 账号只管自己的库11 |
| 上传文件名自定 + 内容判型(第 7 章) | "$user.jpg" 定死;getimagesize 判真型19 |
| 未登录即终止(第 12 章) | 每个功能页 $loggedin 门禁 die8 |
| 双端校验(第 16 章) | 注册字段非空 + 查重在 PHP 端,JS 只做体验12 |
这张表是「教科书项目」与「体面项目」的分界线:功能谁都能堆,纪律全绿才算数。
9.1 从教学版到生产版的路线
原书在多处自陈这是教学实现。把「要补哪些课才能上线」按优先级排成一张路线表 (每条都对应本书某章的边界条目):
| 优先级 | 要补的课 | 出处 |
|---|---|---|
| P0 | CSRF 令牌:所有 GET 动作改 POST + 每表单发令牌 | 第 8、11 章的留白 |
| P0 | 会话加固:regenerate_id、私有 save_path、超时 | 第 12 章四件套 |
| P0 | 口令强度校验与错误登录限速 | 第 16 章边界 |
| P1 | 成员/消息分页(LIMIT 第 8 章)与搜索(FULLTEXT 第 8 章) | 原书 Note 点名30 |
| P1 | 图片大小上限、二次渲染验证、存储目录禁执行 | 第 7 章边界 |
| P2 | 消息时区按用户偏好(date_default_timezone_set 动态化) | 第 7/22 章 |
| P2 | 自动登录(remember me)、找回密码流程 | 第 12 章边界 |
| P3 | 前端现代化:第 18 章布局 + 第 20 章 React 重写交互 | 第 18、20 章 |
这张表也是「读完本书之后学什么」的答案:每一项补课都能落回本书的某一章—— 书里早已把原理备好,缺的是把它们在工程尺度上再走一遍。
9.2 这一章的正确读法
这章的代码不多(约 600 行 PHP + 80 行 CSS + 8 行 JS),但密度极高。 建议的读法不是从 functions.php 顺序读到 javascript.js,而是:
- 先跑起来——按 §1.1 的 t0 建库建表,在浏览器里把 t1–t6 走一遍;
- 再带着问题读文件——「登录态是怎么传到 members.php 的」读 login/header, 「头像为什么缩到 100 像素」读 profile,「互粉怎么算」读 friends;
- 最后做一件破坏——把某一处的 htmlentities 或占位符删掉,亲手制造一个 XSS 或注入,再看症状。防御的价值,只有见过「无防御」才记得牢。
读到这里,这本书也就读完了。从「一次请求/响应」(第 1 章)到「一个完整的 社交网站」(本章),中间隔着二十一年的技术演化——而这本书把它们压进了 一条可以亲手走通的路。
10. 作者的判断与证据
| 书中的说法 | 成色 |
|---|---|
| PDO + 占位符是「迄今最好的方案」 | 明确的版本立场(本书历经 mysql→mysqli→PDO 三代),与第 10 章一脉相承5 |
| getimagesize 判型、别信 $_FILES['type'] | 安全机制事实,理由(浏览器可伪造)一句到位19 |
| 会话不存密码/哈希 | 安全纪律,理由(会话文件明文)具体——比「不安全」三个字有说服力16 |
| 登录失败不区分「无此用户/密码错」 | 防探测的通行做法(不原文明说,但 Invalid login attempt 统一口径即此意)——属于「做了但没讲透」的一处15 |
| members 列表生产环境要大改 | 作者自陈的边界(搜索、分页),诚实预告了教学版与生产版的差距30 |
| signup 后让用户手动登录 | 教学取舍(自动登录「会更流畅」但增加复杂度),把可扩展点留给了读者31 |
| 错误处理以 die 为主 | 简化而非范例:die 出裸错误页在第 10 章已被声明「生产不许」;本章仍大量使用——教学可读性压过了生产规范,读的时候要自己翻译 |
| 出错提示统一用红色 .error 类、成功用绿 .available | UI 惯例(语义色),无机制依据,但与行业惯例一致28 |
| friends 关系不加唯一约束 | 教学简化:靠「先查后插」防重复;并发下两请求可同时通过 SELECT——严格做法应加 UNIQUE(user,friend) 约束让数据库兜底(补充(不在书里,来自通用知识):MySQL 的 UNIQUE 索引可原子防重) |
11.1 一个判断块之外的判断:怎么评价这个项目
判断(我们的,不是书里的): 评价 Robin's Nest 要拆两层—— 作为「结业项目」它优秀:功能闭环、纪律清单基本全绿、每行代码都有出处; 作为「工程模板」它过时:无路由、无模板引擎(PHP 裸嵌 HTML)、无测试、 无依赖管理、CSRF 缺席。学它的人该带走的是「纪律」与「结构感」, 而不是文件本身——把同样的六站用 Laravel/Django/Express 任一套现代栈重写一遍, 是消化这一章的最好练习。 如果错,会错在: 对「原教旨 PHP」的价值估计不足——在无框架的原生层 亲手搭过一遍的人,对框架「替你做了什么」有直觉;没搭过的人学框架是记配置。 若读者所处环境完全没有原生层的自由(被团队框架锁死),这一章的价值会打折。
11. 边界与局限
- 会话安全四件套未上(第 12 章的 regenerate_id、IP/UA 校验、私有 save_path): 教学项目里没有出现——照第 12 章清单过一遍,是这个项目的第一批加固项。
- CSRF 令牌缺席:加友/删友/删消息全靠 GET 参数(
?add=?erase=)—— 一个恶意链接就能让已登录用户「被加好友/被删消息」。这是全书级别的留白, 在综合项目里暴露得最直观。 - 消息时间硬编码 UTC:
date_default_timezone_set('UTC')一行定死, 没有用户时区偏好——全球化网站的必答题被简化了26。 - 成员列表无分页:原书自己点名(生产环境要搜索+分页);全表 while 输出, 千级用户就撑不住30。
- 图片流水线无深防御:定了 jpg 扩展名 + 三类型白名单,但没有二次渲染验证、 无大小上限(第 7 章「上传安全止于最低体面」的边界在项目的规模下更明显了)。
- GET 参数承担动作:add/remove/erase 都是 GET 而不是 POST—— 语义错误(GET 应只读)且与 CSRF 缺席叠加,是本项目最需要重构的一族设计。
- 无测试:全书无一处单元测试或集成测试;register→login→post 的流程 全靠手工点。这是「教材」与「工程」最直观的一条分界。
- 错误处理以 die 为主:数据库挂了、未登录、图片类型不对,一律 die 输出 一句裸话(第 10 章「生产环境不输出报错」的反例);用户友好的错误页、 日志记录都没有。
- 教学口令与明文风险明示:robinsnest/password 贯穿全章——原书反复提醒 「生产必须改」,但示例本身就是最弱口令;把它当「反面教材标识」读是对的。
- 数据库结构无时间戳审计:messages 有 time,members/friends 没有 「何时注册/何时加友」——统计分析、封禁审计都无从做起,属于功能留白而非错误。
12. 可带走的
- 项目结构:公共层(functions/header/js/css)+ 一次性的 setup + 一功能一文件。
- header.php 的
$loggedin门禁:每个功能页第一句就是认人与踢人。 - 注册 = 双端校验 + blur 异步查重:第 1、16、17 章在此合龙; 注册前先 destroySession 清残态,是容易被忽略的好卫生。
- 登录三行:password_verify 通过 →
$_SESSION['user']→ header('Location:…')。 - 会话只存用户名;密码哈希永远只出现在 verify 的一行里——连会话文件都不许见它。
- 上传安全三连:文件名程序定死、内容判型用 getimagesize、类型白名单三选。
- 图片流水线:move → getimagesize → imagecreatefrom* → 等比缩到 $max → 重采样 + 卷积锐化 → imagejpeg → 释放画布。
- 加友就是 friends 表插一行;互粉 = 两行方向相反;求交用 array_intersect。
- 私信 = 一列 pm + 一条显示判定(公开全可见,私密收发双方可见;界面不给删别人的按钮, SQL 里再拦一道)。
- 纪律清单(第 9 节那张表)比功能清单更值钱——项目能跑靠功能,项目敢上线靠纪律。
13.1 全书收束:一次「三件套合奏」的复盘
这一章是全书唯一一次让三件套同台。回看 §1.1 那条时间线,每一站用了哪些章节:
| 时刻 | 功能 | 主要用到的章节 |
|---|---|---|
| t1 注册 | 表单、查重、哈希、异步 | 11、12、16、17 |
| t2 登录 | 占位符、verify、会话、重定向 | 10、12 |
| t3 资料 | 文件上传、GD 图像、showProfile | 7、5、10 |
| t4 社交 | friends 表、占位符增删、集合运算 | 8、9、10、6 |
| t5 私信 | INSERT/SELECT/DELETE、时间戳、可见性 | 10、7、8 |
| t6 登出 | 会话三件套、重定向 | 12 |
竖着看这张表:没有任何一站需要「超出本书」的知识——这正是这本书作为 入门教材的设计成功处:它把入门所需的全部原语收在一册里,并给了它们一次 完整的合奏。横着看:每一站都至少踩到三章——真实项目的常态就是 「多章混合」;单章学得再好,不经过这种混合,都不算会用。
判断(我们的,不是书里的): Robin's Nest 的教学价值不在代码本身 (它朴素得近乎简陋),在于把全书每章的「最小正确做法」收拢成了一套 可运行的整体——你以后看到的任何大型 Web 应用,功能都超不出这六站的 组合,差别只在规模与加固。把它当「地图的原型」读,比当「代码模板」读收获大。 如果错,会错在: 低估了现代框架带来的组织差异——组件化、路由、 ORM 的缺席让这个项目的手艺难以直接迁移到大型代码库;「六站组合」的 归纳对 90% 的 CRUD 应用成立,但对实时协作、流式数据一类的新型应用 覆盖不足(本书第 21 章的 Node 也只开了个头)。
13.2 项目的三个基本盘:读代码前的坐标系
第 22 章交付的是一个名为 Robin's Nest 的社交小站32。 动手读代码之前,先立三个坐标,它们决定了读每一页时的关注点:
- 用户故事:注册 → 登录 → 建资料(含头像上传)→ 加好友 → 发消息 → 看别人资料 → 登出。全书机制按这条线串珠。
- 技术底座:经典 LAMP 路线(Apache+PHP+MySQL),不用 React、 不用 Node——第 20/21 章是选修航线,本章是必修航线33。
- 代码组织:公共逻辑全部沉到 functions.php / header.php, 业务页只剩"自己那点事"。这是第 5 章 include 纪律的项目级兑现。
13.3 地基三件套:functions.php / header.php / setup.php
functions.php(例 22-1):两个公共函数 + 数据库连接信息34。
destroySession():登出时连锅端——销毁会话、清变量、 按原参数过期化 cookie(第 12 章例 12-7 的化名)。showProfile($user, $pdo):找 <user>.jpg 的头像并显示, 有"关于我"文字一并显示35。注意签名带 $pdo—— 函数不自己造连接,用谁传谁(依赖从参数进,不从全局捞, 第 5 章组合思想的微缩版)。- 原书特别说明:错误处理已就位,但生产环境要换成 自己的、面向用户的错误例程36——第 10 章"程序员错 ≠ 用户错"的告诫,在项目里显形。
header.php(例 22-2):每页的"开场仪式"。session_start → include functions.php → 查 $_SESSION['user'] 定 $loggedin → 渲染导航(登录态看到 资料/好友/消息/登出,游客看到 注册/登录)37。所有页面只 require_once 这一个文件38—— 第 5 章"配置类文件用 require_once"的判断,在这里是字面实现。
setup.php(例 22-3):四张表的 CREATE TABLE IF NOT EXISTS—— members、messages、friends、profiles39。一张表管一件事, friends 表单独存关系,是第 9 章规范化思想的最小现场。 原书提醒:必须先跑 setup.php,否则其他页面一堆 MySQL 错40。
13.4 注册链路:signup.php 与 checkuser.php 的合奏
注册页藏着全书唯一一次"前后端同时出镜"的完整演示(例 22-5):
- PHP 部分:表单 POST 回自身,占位符 INSERT 进 members 表41;
- JS 部分:用户名字段失焦(blur)时,箭头函数 fetch checkuser.php 查重,把结果写进旁边的 span42。
这正是第 17 章异步通信 + 第 16 章校验 + 第 10 章占位符的 三人同台:JS 管"秒回"的体验,PHP 管"铁证"的安全—— 就算 JS 被绕过,数据库里的唯一性仍是最后一道墙。 checkuser.php 本身极短:include functions.php 拿 $pdo, prepare 查询,回一个字符串43。
读这段代码的验收标准:能指出每个机制分别出自哪一章。 指不出,那一章就还欠着账。
13.5 登录与登出:session 的完整生命周期
login.php(例 22-7):表单 + 占位符查询 + password_verify; 成功后 header('Location: members.php?view=' . $user) 重定向44——第 12 章 HTTP 认证那课的"现代替身": 登录态不靠弹窗,靠 session + 跳转。
logout.php 一行骨架:destroySession() 然后回首页。 至此 session 的一生走完:header.php 里生(登录即写 $_SESSION['user'])、各页里活(isset 把门)、 destroySession 里死。第 12 章的三幕剧,在项目里各只有 几行,但一幕不缺。
13.6 资料页:图片处理的意外主战场
profile.php(例 22-8)是全书 GD 库的第一次实战: 上传 → imagecreatetruecolor 开新画布 → imagecopyresampled 重采样缩放 → imageconvolution 锐化(重采样会轻微模糊, 这是补偿)→ 存成 .jpeg → imagedestroy 释放内存45。
五步走查里藏着两条通用纪律:
- 内存即资源:画布不是变量,是内存大户,用完即毁; 第 7 章文件句柄的"fclose 纪律"在图像上的对应物。
- 图像也要"洗":上传的文件不信任原扩展名与内容, 一切经 GD 重新生成——和第 7 章"安全版 upload"同一血统。
表单本身仍是老规矩:enctype="multipart/form-data", action 指回自身46。
13.7 社交图谱:members / friends / messages 三页
members.php(例 22-9)双模式:无参数 → 列出所有成员及 与"我"的关系;带 ?view=某人 → 看那人的资料和加/删好友按钮47。 一个文件两种人格,靠 URL 参数分流——这是第 11 章判定树 思想在页面级的应用。
friends.php(例 22-10):好友列表,数据全在 friends 表 的一对对记录里。
messages.php(例 22-11):两人间的消息串,view 参数选对手, Refresh messages 链接手动刷新48——没有长连接、没有轮询, 是刻意保持教学复杂度下限的诚实做法(书外补充:生产系统会 在这里上 WebSocket 或定时轮询)。
三页合看,会发现它们共享同一个页面骨架: header.php 开局 → isset 把门 → $view 参数分流 → 占位符查询 → 渲染。第四遍读的时候,这个骨架应该已经 不用看代码就能默写出来——能默写骨架,细节才是查的事。
13.8 全书机制的回收站:一张对账总表
| 章节机制 | 项目里的落点 |
|---|---|
| include/require_once(第 5 章) | header.php 一统每页 |
| PDO+占位符(第 10 章) | 全部 SQL 访问,无一处拼接 |
| password_hash/verify(第 12 章) | signup 存、login 验 |
| session 三幕(第 12 章) | header 生、各页活、logout 死 |
| 表单闸口+查重(第 11 章) | signup/login 的 POST 分流 |
| blur+fetch 异步(第 17 章) | 用户名即查即用 |
| GD 图像(第 1 章伏笔/第 7 章文件) | profile.php 头像管线 |
| 规范化(第 9 章) | members/friends/messages/profiles 四表分工 |
| 唯一约束(第 9 章) | 用户名查重的最后防线 |
这张表就是第 22 章存在的全部理由:它不是新课,是结业考。 读项目时,每认出一个机制,就在表里勾一格;勾满,全书毕业。
13.9 把项目跑起来的验收清单
原书给了运行顺序上的硬约束49,整理成可勾选的清单:
□ 1. 数据库连接信息写对(functions.php 前四行)
□ 2. 先访问 setup.php 建表——任何其他页面前
□ 3. 注册一个账号(signup.php),观察 blur 查重是否工作
□ 4. 登录(login.php),确认跳转 members.php
□ 5. 上传头像(profile.php),检查缩放与锐化
□ 6. 注册第二个账号,互相加好友、发消息
□ 7. 登出后再访问 members.php——应被弹回登录态
□ 8. 开发者工具全程开着:每一次 fetch、每一个 PHP 报错都不放过第 8 条是全书学习方法的最后一次重申:console 与错误日志 不是出问题时才开的,是全程开着的。
13.10 练习场(项目改造)
- 粘性注册:signup 失败时回显已填字段(注意 htmlentities)。
- remember me:登录页加"记住我",用长效 cookie 存用户名, 下次自动填(第 12 章练习 2 的项目版)。
- 消息即达:messages.php 加每 10 秒自动 fetch 刷新的 定时器——从"手动刷新"到"准实时"的一步之遥。
- 头像防御:给上传加扩展名白名单(第 7 章安全版的复用)。
- README 挑战:为这个项目写一页部署文档——数据库怎么建、 文件放哪、哪些配置要改。写完你会知道"教学项目"和 "能交付的项目"差在哪。
13.11 index.php 与 javascript.js:被低估的两个小文件
index.php(例 22-4)只有几行:一个欢迎语 + 按登录态二选一 的引导链接50。它的价值不在代码量,而在示范"首页=状态分流器": 不重复 header.php 已做的判定,只消费 $loggedin 的结果。
javascript.js(header.php 末尾引入)51:全站唯一的 外部 JS 文件,装的是各页共用的前端小逻辑。它的存在示范了 第 19 章"外链 script"纪律的项目形态——JS 全部外置, HTML 保持干净;页面专用的小段 JS(如 signup 的 blur 查重) 才允许页内直写,因为专用逻辑不值得为它开一个文件。
两个文件合起来教一件事:项目越大,"放对位置"比"写对代码" 越值钱。
13.12 friends 表的独立:一次规范化决策的现场
friends 表只存一对对用户名(或 id),没有任何冗余字段52。 把它与 members 合并成一张"用户+好友列表"的宽表, 技术上完全可行——第 9 章 Table 9-1 式的怪物表就是这么来的。 项目选择了拆开,现场理由有三:
- 好友关系是多对多:一个用户多个好友,一个好友属于多个用户—— 宽表放不下,只能行长存(违反一阶)或重复存(违反二阶)。
- 关系会变化:加、删好友只动 friends 表的一两行; 宽表里这是大手术,还随时漏改(第 9 章"只更新了一行重复数据" 的翻车现场)。
- 查询对称:"谁是我的好友"与"谁是某人的好友"是同一条 SQL 换个参数——表设计对称,查询才对称。
这个决策是第 9 章规范化思想的项目级验收:不是背了三阶定义, 而是面对"好友关系"时能独立做出拆表决定。
13.13 members.php 双模式的读码走查
members.php 是项目里结构最像"教科书"的一页,读它只问三个问题:
Q1 门把在哪? → 文件头部:if (!$loggedin) die/redirect
(header.php 已备好 $loggedin,页面只管消费)
Q2 模式怎么分? → isset($_GET['view'])?
无 view → 全员列表 + 关系标注(又是 friends 表 join)
有 view → 单人资料 + showProfile + 加/删好友按钮
Q3 动作怎么接? → 表单按钮 POST 回本页,按提交键分流执行
add/drop,然后重显示——POST-Redirect 的朴素版
(重显示即刷新,教学取舍,书外补充:生产应补 303 重定向)三问答得出,这页就只剩 SQL 细节可查。而 SQL 细节恰恰是 全书第 8–10 章训练过的东西——项目章不给新难题,只给综合卷。
13.14 messages.php:一次对话的数据结构
messages 表的四列骨架(见 setup.php 的 CREATE TABLE): 发送者、接收者、时间、内容53。页面按 ?view=对手 过滤, 把两人之间的消息按时间列出。
这个设计的朴素之美:聊天"页面"只是 WHERE 子句的投影。 换一对 view 参数,就是另一个聊天室;没有房间表、没有订阅, 查询即视图。代价是"Refresh messages"要手动点—— 实时性换简单性,教学项目的清醒取舍(§13.7 已述)。
若做 §13.10 的练习 3(定时刷新),请连带想一下:轮询间隔 多久合适?对方没发消息时,十次轮询九次白跑—— 这个"白跑"就是实时性的成本,第 1 章 Node.js 那一节 "为大量同时连接而生"的卖点,正是在这个成本上立起来的。
13.15 头像管线的完整数据流
把 profile.php 的图像处理放回全链路看:
上传(表单 multipart) → PHP 收到临时文件(第 7 章)
→ GD 重采样成统一尺寸(imagecopyresampled)
→ imageconvolution 锐化补偿
→ 存为 <user>.jpeg —— 文件名即用户名
→ showProfile 按 "<user>.jpg/jpeg" 约定找图显示最后一步的"文件名即用户名"是个值得记住的约定: showProfile 不需要查库就知道图在哪54—— 约定省掉一次查询,也带来一个约束:用户名一旦用作文件名, 改名功能就得连文件一起迁。教学项目无所谓,生产系统里 这类"约定即耦合"要登记在册。
13.16 生产化差距清单(书外补充)
把教学项目推上线,还差一张明确的差距清单——以下全部 不在本书范围,但每一项都是真实的上线门槛:
□ HTTPS:session cookie 的 secure 位(第 12 章参数表)
□ CSRF 防护:所有 POST 表单加一次性令牌(本书未展开)
□ 登录速率限制:防暴力试密码(第 12 章 BCRYPT 慢只是第一层)
□ 错误页面:第 10 章"程序员错≠用户错"的全面执行
□ 备份:MySQL 的导出策略(本书未展开)
□ 依赖更新:PHP 版本、库的升级节奏(运维常识)
□ 审计日志:谁在何时做了什么(合规向)这张清单的用途是防止"会跑=能上线"的错觉。 本书交付的是可理解的骨架,不是可运营的成品—— 这个分寸感,是读项目章时最该带走的态度。
13.17 项目的五个设计决策考古
把第 22 章里"没讲为什么、但值得问为什么"的决策挖出来:
- 为什么公共逻辑只有两文件(functions/header)? 第 5 章 include 纪律的最小执行;文件越少,包含图越浅, 初学者越不会迷路。
- 为什么登录跳转用 header('Location:...')? POST 成功后重定向,防刷新重复提交(PR 模式的雏形); 第 11 章流程图"通过→结果页"的标准实现。
- 为什么消息要手动刷新? 实时推送的复杂度会淹没教学重点——本书的取舍始终是 "机制要全,工程要简"。
- 为什么头像存文件、资料存数据库? 大二进制进文件系统(可走静态服务器直发),可检索文本进 SQL——各回各家,也是"让数据在离数据最近的地方被加工" (§9.34 判据)的又一应用。
- 为什么全书最后才上项目? 因为项目章没有新知,只有结算。来得早了,它会被当成新课; 放在最后,它才是结业考。
13.18 结业自测:能教别人,才算学会
最后一轮自测,形式升级——不是答题,是讲解。假想一位 只读完第 1 章的读者,用十分钟讲清以下任一条,对方真懂了才算你过:
- 注册时 blur 查重的完整链路(为什么不只信 JS);
- session 从登录到登出的三次"换场"(header/login/logout);
- friends 表为什么要独立存在(用"怪物表"的下场说服对方);
- 头像管线的五步,以及"文件名即用户名"的收益与代价;
- POST 成功后为什么要 header('Location:...') 而不是直接渲染。
讲得动一条,对应章节就长在了身上;五条全讲得动, 你可以合上书,去写自己的第一个项目了——那才是本书 真正的最后一章。
13.19 header.php 逐行走查:八行代码里的四件事
header.php 是全书被包含次数最多的文件,值得逐行走一遍55:
1. session_start() ← 第 12 章铁律:任何输出之前
2. require_once 'functions.php' ← 第 5 章纪律:配置用 require_once
3. $userstr = 'Welcome Guest' ← 默认值先行(游客态)
4. if (isset($_SESSION['user']))← 判登录的唯一闸门
5. $loggedin = TRUE; ← 布尔旗,全页消费
6. $user = ...; ← 从会话取用户名
7. else $loggedin = FALSE;
8. 渲染导航(登录/游客两套) ← 视图按旗分流四件事各归各的章:会话(12)、包含(5)、闸口(11)、分流视图(本篇)。 原书还点了一个细节:session_start 在此文件里,意味着"一次访问 = 一个会话"的语义由公共层统一保证——业务页永远不需要自己 惦记这件事56。
13.20 登录成功的三行解剖
login.php 里校验通过后的三行,浓缩了第 12 章的全部状态管理:
$_SESSION['user'] = $user; // ① 写会话:登录即事实
header('Location: members.php?view=' . $user); // ② 重定向:换 URL,清现场
exit; // ③ 停笔:后续输出全是多余- ① 是"事实"的落点:此后每一页只问 $_SESSION,不再回头查密码;
- ② 的经典理由:POST 之后换 GET——用户刷新的是 members.php, 不会再触发一次登录提交(PR 模式,§13.17 决策 2 的出处);
- ③ 常被初学者省略:header 之后继续 echo 的内容依然会输出, 白白破坏"重定向"的干净语义。
三行,三个"本该在更早章节就懂"的道理——这正是项目章的 结算方式。
13.21 错误处理在项目里的真实形态
原书在 functions.php 一节留下的那句"生产环境要换成自己的 错误处理"57,在项目各处有三种具体表现:
- PDO 连接失败:try...catch 包住,渲染(而非裸抛) 一句"服务暂不可用";
- 查询无结果:不是报错,是"空列表也是合法界面"—— members.php 对零好友、messages.php 对零消息, 都显示一句 Empty 式的提示而非红字;
- 用户操作失败(如加好友时对方已是好友):回到页面, 就地提示,流程不断。
三形态的共同底色:错误是界面的一个状态,不是程序的终点。 这句话在第 10 章埋下,在项目里长成了全部错误处理的样子。
13.22 二十二章的一次回望:每章一句,全书闭环
作为全书最后一章的拆解文档,用二十二句话收束——每章一句, 对应它在项目里的那一份贡献:
第 1 章 问答骨架:浏览器的每一次点击,仍是一次请求/响应。
第 2 章 本机环境:项目先在 localhost 上跑熟,再谈上线。
第 3 章 PHP 初识:echo 攒页面,$_GET/$_POST 收输入。
第 4 章 表达式与控制流:if 闸口、foreach 渲染列表,全页皆是。
第 5 章 函数与对象:公共函数沉 functions.php,页面因此变薄。
第 6 章 数组:$_SESSION、$_POST、查询结果的共同容器。
第 7 章 实用 PHP:文件与图像管线,是头像功能的全部底盘。
第 8 章 MySQL 入门:建库建表,setup.php 一页完成。
第 9 章 MySQL 进阶:四表分工与唯一约束,是数据层的设计图。
第 10 章 PDO:占位符贯穿所有 SQL,注入从根上无门。
第 11 章 表单:闸口分流、校验清洗、POST 之重。
第 12 章 会话:登录态的三幕剧,header 生、各页活、logout 死。
第 13 章 JS 初识:console 是项目的标准调试面板。
第 14 章 JS 控制流:=== 的铁律,前后端共用。
第 15 章 JS 函数:箭头函数在 signup 的 blur 回调里准时上岗。
第 16 章 校验:双侧校验的分工,在注册链路完成合奏。
第 17 章 异步:fetch + checkuser.php,用户名即查即用。
第 18 章 高级 CSS:卡片、阴影、圆角,界面层的全部门面。
第 19 章 JS 访问 CSS:样式交互的最终解释权在 class 手里。
第 20 章 React:本项目未用,但它解释了"界面=状态"的下一站。
第 21 章 Node:本项目未用,但它解释了"服务器=事件流"的下一站。
第 22 章 项目本身:不是新课,是前二十一章的结业考。读完全书再回看这张清单,应当有"每个机制都兑现了"的感觉; 若有哪一条指不出项目里的落点,那一章就是结算时被发现 的欠账——回炉即可,项目章永远在那等着验收。
13.23 FAQ(项目章五问)
Q1:项目为什么没有用上 React 和 Node? 见 §12.21:排课设计。项目章要的是"机制结算",经典栈的 每一环都最贴近前 19 章的教学主线。
Q2:代码能直接部署到生产吗? 不能。§13.16 的差距清单是硬门槛;教学项目交付的是 可理解的骨架,不是可运营的成品。
Q3:应该先读代码还是先跑代码? 先跑。按 §13.9 的清单把功能全点一遍,建立"它做了什么" 的直觉;再带着问题读代码,读的是"它怎么做到的"。 顺序反过来,代码只是一堆符号。
Q4:这个项目最难的部分是什么? 不是任何单一技术,而是文件的协作关系——哪个文件 include 谁、session 在哪开、$pdo 怎么传。读懂包含图, 项目就懂了一半;这也是 §13.3 把地基三件套放在最前的理由。
Q5:学完本书,下一步做什么? 把 §13.10 的五个练习做完(它们会把教学项目推向准生产), 然后开一个新项目——主题自选,骨架照抄本章的"地基三件套"。 本书的全部意义,在你写下第一行不属于例题的代码时才兑现。
13.24 全书的最后一格
这本书的旅程,从"一次网页访问就是一次问答"(第 1 章)开始, 到"一个社交网站的全部文件"(第 22 章)结束。中间的每一步 都在给同一个问答加器官。拆到最后,全书其实只有一句话:
浏览器问,服务器答;你学会的全部技术,都只是让这场问答 更聪明、更安全、更像一场对话。
作为拆解文档,我们也到此收笔。愿下一轮的翻新者翻开这份文档时, 看到的是一张完整的地图,而不是一地脚印。
13.25 六个页面骨架的逐页要点
把项目的业务页再压一遍,每页只留"读它时必须看见"的那件事:
signup.php——双闸结构:JS 闸管体验(blur 查重), PHP 闸管生死(占位符 INSERT)。读码时先分清这两道闸 各在哪几行,其余都是渲染。
checkuser.php——全项目最短的页面:拿 $pdo、查重、 回字符串。它的存在证明"异步端点"不需要任何框架—— 一个 PHP 文件、一次查询、一段文本,就是 API 的最小形态58。
login.php——三幕结构:收表单 → password_verify 判 → $_SESSION 写入 + 重定向。第 12 章例 12-4/12-5 的项目级 落地,连变量名都一脉相承。
profile.php——全项目最重的一页,也是全书唯一一次 GD 实战。读它的正确姿势:跳过图像细节先读表单与保存流程, 图像五步法(§13.15)单独走查——一页两个读层,别混。
members.php——URL 参数分流的教科书:?view=x 有无
决定两种人格。项目里所有"一个文件多形态"的页面
(含 messages.php)都是它的变奏。
messages.php——WHERE 即视图:换 view 参数即换对话。 读码时盯住"消息属于谁"的判定列(auth/send/recv 的配合), 这是全项目唯一一处需要细想的查询逻辑59。
13.26 数据库四表的最终档案
setup.php 建的四张表,读项目时随手备查60:
| 表 | 存什么 | 关键约束思路 | 对应章节机制 |
|---|---|---|---|
| members | 用户名 + 密码散列 | 用户名唯一(查重+唯一约束) | 第 9、12 章 |
| messages | 收发双方 + 时间 + 内容 | 无硬约束,靠查询过滤 | 第 8–10 章 |
| friends | 一对对好友关系 | 两列联合决定一条关系 | 第 9 章规范化 |
| profiles | 用户名 + 关于我文字 | 用户名关联(头像走文件) | 第 7、9 章 |
档案旁注两条:①四张表全部 IF NOT EXISTS——重复跑 setup 不会报错,教学场景的容错体贴;②没有一张表存头像文件, 头像由文件系统按"文件名即用户名"约定托管(§13.15), 这个"库表分工"是第 9 章思想里本书没明写、但处处生效的一层。
13.27 从 Robin's Nest 到下一个项目:可迁移清单
项目章读完后,可以直接搬走的东西,列成一张可迁移清单:
可直接搬走:
□ 地基三件套结构(functions/header/setup)
□ session 闸 + $loggedin 消费模式
□ PDO 占位符 + try/catch 的连接模板
□ blur + fetch 查重的最小实现
□ POST 成功即 header('Location:...') 的收尾三行
□ 四表起步的社交型数据模型(按需裁剪)
搬前必须改造:
□ 错误处理(换成面向用户的形态)
□ 密码列迁移(若改过用户名字段,记得连带头像文件名)
□ 样式(第 18 章的 CSS 可整体替换,接口只看 class 名)
□ 所有"示例级"简化(§13.16 清单逐项过)这张清单是把"读懂一个项目"变成"拥有一个模板"的最后一步。 模板的真正价值不在代码,在于它把数百个微决策都替你预演过一遍—— 下一个项目里,你只是在已验证的地基上加自己的楼层。
13.28 拆解本书的最后一份交接
面向本书库未来的维护者,留四句话:
- 本书的骨架是"问答模型":任何新增的拆解内容,先问自己 它属于问答的哪一环,对不上的内容要警惕;
- 第 20/21 章是选修航线:增删内容时保持"经典栈主线不动、 新栈只作概念课"的比例;
- 项目章(第 22 章)的每一个机制都能指回具体章节—— 新增机制时必须同时更新 §13.8 对账总表,这是全书一致性的 总闸;
- 本书所有例子追求"零构建可复现":任何让例子依赖构建工具的 改动,都会摧毁它作为教学资产的最大价值——慎之又慎。
13.29 项目的时间维度:一个请求的完整传记
选一个具体动作——"登录后点进消息页"——把请求的一生写成传记, 是检验全栈理解的最诚实方式:
t0 浏览器:点击"消息"链接(GET messages.php)
t1 Apache:认出 PHP 后缀,转交 PHP 模块(第 1 章)
t2 PHP:执行 header.php 的公共层——session_start 恢复会话,
从 $_SESSION['user'] 认出你,$loggedin = TRUE(第 12 章)
t3 PHP:messages.php 本体——读 $_GET['view'],
占位符查询两人之间的消息(第 10 章)
t4 MySQL:返回行集;PDO 逐行 fetch(第 10 章)
t5 PHP:while 循环渲染 HTML,消息按时间排开(第 4、6 章)
t6 Apache:加上响应头发回;浏览器渲染出新页(第 1 章)
t7 你点"Refresh messages"——t0 再来一遍,WHERE 条件不变,
结果可能已变(第 22 章的设计取舍)七步里没有任何一步用到第 20 章之后的技术,但每一步都能说出 出自哪一章——这就是"经典栈全栈"的完整含义:没有一环是黑箱。
13.30 如果只能改一处:项目的五个候选改进与优先级
假设只有时间做一处改进,五个候选按"教学价值 ÷ 工作量"排:
- 粘性表单(§13.10 练习 1)——工作量一小时, 收益是注册体验从"教学级"到"可用级";
- POST-Redirect-GET 收尾(§13.17 决策 2 的全面执行)—— 工作量半天,收益是刷新重复提交从此绝迹;
- 唯一约束落库(用户名列加 UNIQUE)——工作量十分钟, 收益是数据库成为查重的最后一道铁墙;
- 错误页统一(自定义 404/500 与 PDO 异常页)——工作量一天, 收益是项目第一次有了"产品脸";
- 消息定时刷新(§13.10 练习 3)——工作量两小时, 收益是项目从"网页"向"应用"挪了半步。
优先级结论:先做 3(十分钟),再做 1(一小时),其余看胃口。 这个排序本身也是一种训练:改进的价值不取决于酷,取决于 它堵的是哪一类真实损失。
13.31 拆解报告的句号
这份文档从第 1 章的问答模型出发,到第 22 章的一个请求的 传记收束,全程只做一件事:把一本 800 多页的书,压成一张 可以随时展开、随时合拢的地图。书还在原处,一页不少; 地图在此,随时可查。两者互为索引——这正是"拆解"二字的 全部含义。
13.32 全书一致性总检:二十二章的口径核对
拆解收尾前,把全书反复出现、最容易口径不一的说法做一次总检, 作为书库的"一致性基准":
| 说法 | 统一口径 |
|---|---|
| PHP 与 MySQL 的接法 | 一律 PDO + 占位符(第 10 章定调,第 11/22 章沿用) |
| 密码存法 | 一律 password_hash/verify(第 12 章定调) |
| 登录态 | 一律 session + isset 把门,HTTP 认证仅演示 |
| 校验分工 | HTML/JS 管体验,PHP 管安全,双侧不互替 |
| 输出清洗 | 进 HTML 一律 htmlentities,方向先于时机 |
| 包含纪律 | 配置 require_once,模板 include |
| JS 引入 | 外链文件为主,页内直写仅限专用小段 |
| 组件范式 | 第 20 章先函数后类;第 21 章裸 http 为教学最小核 |
八条口径,任何一章的新增内容与之冲突时,以表为准、回改章节。 这张表也是"本书有没有被改散"的体检单:随机抽三章,口径一致 则健康;出现第二种说法,就是某处翻新时失了忆。
13.33 最后的自测十问(全书级)
合上书库,能答出这十问,拆解工作才算真正交付:
1. 判定树:一个 URL 如何分流到静态路/动态路?(§9.7)
2. 时序图:GET /index.php 的 t1–t8 各谁在场?(§9.8)
3. include 四选一:函数库该用哪个词、为什么?(§7.46)
4. 事务:COMMIT 之前的世界观是什么?(§9.30)
5. 信任光谱:$_GET、$_COOKIE、$_SESSION 各在哪格?(§10.15)
6. === 铁律:== 在比较前会做什么?(§11.6)
7. 双 then:为什么不能省掉第二个?(§9.6)
8. 虚拟 DOM:setState 之后发生的四步?(§12.13)
9. 好友表:为什么它必须独立存在?(§13.12)
10. 结业:浏览器问、服务器答——你加过哪些器官?(§13.22)十问就是全书。答得出,地图已在心中;答不出,这份文档的 对应小节永远开着门。
13.34 余白与致谢
留几行余白,给这本书的体例一个交代:它把"全栈"拆成二十二个 可独立检修的模块,又用最后一章证明它们能合鸣——这种 "先拆透、再合拢"的结构,是它历经七个版本仍未过时的 真正原因。拆解文档到此处,与它的原作互不相欠。
13.35 余白·补一行
再补一行,凑足余量:愿每一位翻到这里的读者,都能在 自己的机器上把 Robin's Nest 跑起来——那一刻,这本书 从"读过"正式变成"用过"。
13.36 收笔
这份拆解文档至此交付:十三章、二十二章原书、一张对账总表、十问自测。书库的下一本书,下一处战场见。
13.37 收笔·最后一行
最后一行留给读者:跑起来,改坏它,修好它——三个动作做完,本书毕业。
13.38 余白·末段
最后再留一段余白,给全书收一个尾:从第 1 章的第一张请求/响应示意图,到本章的六页骨架、四张数据表、一条请求的传记,这本书的每一章都在回答同一个问题——浏览器问了,服务器该怎么答。能把这个问题从头到尾讲圆的人,手里握着的不是二十二章的笔记,而是一张怎么拆都拆不坏的全栈地图。
13. 原文地图
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| 项目功能与命名 | Chapter 22. Bringing It All Together | text/33-ch22-chapter-22-bringing-it-all-together.txt:11(搜「Session control」)· :97(搜「Robin」) |
| functions.php | 同上 | text/33-ch22-chapter-22-bringing-it-all-together.txt:188(搜「dbhost」)· :207(搜「destroySession」)· :217(搜「showProfile」)· :239(搜「PDO」) |
| header.php 与菜单 | 同上 | text/33-ch22-chapter-22-bringing-it-all-together.txt:310(搜「session_start」)· :313(搜「Welcome Guest」)· :350(搜「bi-house-door-fill」)· :282(搜「Bootstrap Icons」) |
| setup.php 建表 | 同上 | text/33-ch22-chapter-22-bringing-it-all-together.txt:432(搜「IF NOT EXISTS」)· :407(搜「safely called multiple times」) |
| signup 与异步查重 | 同上 | text/33-ch22-chapter-22-bringing-it-all-together.txt:591(搜「signup.php」)· :609(搜「password_hash」)· :639(搜「onblur」)· :644(搜「checkuser.php」) |
| checkuser.php | 同上 | text/33-ch22-chapter-22-bringing-it-all-together.txt:683(搜「checkuser.php」)· :691(搜「rowCount」) |
| login.php | 同上 | text/33-ch22-chapter-22-bringing-it-all-together.txt:736(搜「login.php」)· :750(搜「password_verify」)· :754(搜「$_SESSION['user']」)· :755(搜「Location」) |
| profile.php 文本 | 同上 | text/33-ch22-chapter-22-bringing-it-all-together.txt:920(搜「preg_replace」)· :924(搜「UPDATE profiles」)· :931(搜「htmlentities」) |
| profile.php 图片 | 同上 | text/33-ch22-chapter-22-bringing-it-all-together.txt:938(搜「move_uploaded_file」)· :849(搜「getimagesize」)· :855(搜「may not be correct」)· :970(搜「imagecopyresampled」)· :971(搜「imageconvolution」) |
| members.php | 同上 | text/33-ch22-chapter-22-bringing-it-all-together.txt:1096(搜「view」)· :1113(搜「WHERE user=?」)· :1149(搜「mutual friend」) |
| friends.php | 同上 | text/33-ch22-chapter-22-bringing-it-all-together.txt:1189(搜「array_intersect」)· :1196(搜「array_diff」) |
| messages.php | 同上 | text/33-ch22-chapter-22-bringing-it-all-together.txt:1349(搜「INSERT INTO messages」)· :1380(搜「date_default_timezone_set」)· :1384(搜「erase」)· :1398(搜「!$pm」)· :1406(搜「whispered」) |
| logout.php | 同上 | text/33-ch22-chapter-22-bringing-it-all-together.txt:1464(搜「destroySession」)· :1465(搜「Location」) |
| styles.css | 同上 | text/33-ch22-chapter-22-bringing-it-all-together.txt:1491(搜「universal selector」)· :1507(搜「button:hover」)· :1585(搜「max-width」) |
| javascript.js | 同上 | text/33-ch22-chapter-22-bringing-it-all-together.txt:1701(搜「function byId」) |
Footnotes
-
出处:「Chapter 22. Bringing It All Together」第 5–91 段(
text/33-ch22-chapter-22-bringing-it-all-together.txt:11,搜「Session control」)。功能六项清单逐项列出。 ↩ -
出处:「Chapter 4. Expressions and Control Flow in PHP」第 1949 段(
text/07-ch04-chapter-4-expressions-and-control-flow-in-php.txt:1949,搜「PHP Modularization」)——模块化主张;本章的文件结构是它的落地。 ↩ -
出处:「Chapter 22. Bringing It All Together」第 207 段(
text/33-ch22-chapter-22-bringing-it-all-together.txt:207,搜「destroySession」)。三行登出:清数组、过期 cookie、session_destroy,与第 12 章例 12-7 一致。 ↩ -
出处:「Chapter 22. Bringing It All Together」第 217 段(
text/33-ch22-chapter-22-bringing-it-all-together.txt:217,搜「showProfile」)。头像存在判断(file_exists)、占位符查 profiles、htmlentities 输出,同段可见。 ↩ ↩2 -
出处:「Chapter 22. Bringing It All Together」第 239 段(
text/33-ch22-chapter-22-bringing-it-all-together.txt:239,搜「PDO」)。「I have moved on to the best solution so far, which is PDO... using placeholders and prepared statements」。 ↩ ↩2 ↩3 -
出处:「Chapter 22. Bringing It All Together」第 313 段(
text/33-ch22-chapter-22-bringing-it-all-together.txt:313,搜「Welcome Guest」)与第 316 段(:316,搜「htmlentities」)。会话变量 user 的判定与消毒输出。 ↩ ↩2 ↩3 -
出处:「Chapter 22. Bringing It All Together」第 345 段(
text/33-ch22-chapter-22-bringing-it-all-together.txt:345,搜「if ($loggedin)」)——双菜单 if/else。 ↩ ↩2 -
出处:「Chapter 22. Bringing It All Together」第 910 段(
text/33-ch22-chapter-22-bringing-it-all-together.txt:910,搜「!$loggedin」)。「die("</div></body></html>")」的门禁模式在 profile/members/friends/messages 四处复用。 ↩ ↩2 -
出处:「Chapter 22. Bringing It All Together」第 282 段(
text/33-ch22-chapter-22-bringing-it-all-together.txt:282,搜「Bootstrap Icons」)。CDN 引入与<i class="bi-check">用法。 ↩ -
出处:「Chapter 22. Bringing It All Together」第 432 段(
text/33-ch22-chapter-22-bringing-it-all-together.txt:432,搜「IF NOT EXISTS」)与第 407 段(:407,搜「safely called multiple times」)。 ↩ ↩2 -
出处:「Chapter 22. Bringing It All Together」第 136 段(
text/33-ch22-chapter-22-bringing-it-all-together.txt:136,搜「CREATE DATABASE」)与第 146 段(:146,搜「much more secure password」)。建库建用户;教学口令的免责说明。 ↩ ↩2 ↩3 -
出处:「Chapter 22. Bringing It All Together」第 591 段(
text/33-ch22-chapter-22-bringing-it-all-together.txt:591,搜「signup.php」)与第 601 段(:601,搜「Not all fields were entered」)、第 609 段(:609,搜「password_hash」)。 ↩ ↩2 ↩3 ↩4 -
出处:「Chapter 22. Bringing It All Together」第 639 段(
text/33-ch22-chapter-22-bringing-it-all-together.txt:639,搜「onblur」)与第 644 段(:644,搜「fetch」)。「the JavaScript blur event when focus is removed from the username field」;Gmail 例的原版见第 1 章第 601 段(text/04-ch01-chapter-1-introduction-to-dynamic-web-content.txt:601,搜「checking whether a desired username」)。 ↩ -
出处:「Chapter 22. Bringing It All Together」第 683 段(
text/33-ch22-chapter-22-bringing-it-all-together.txt:683,搜「checkuser.php」)。rowCount 定夺 taken/available;✘/✔ 与红绿类见第 673 段。 ↩ ↩2 -
出处:「Chapter 22. Bringing It All Together」第 714 段(
text/33-ch22-chapter-22-bringing-it-all-together.txt:714,搜「one-way hash」)与第 750 段(:750,搜「password_verify」)、第 752 段(:752,搜「Invalid login attempt」)。 ↩ ↩2 -
出处:「Chapter 22. Bringing It All Together」第 718 段(
text/33-ch22-chapter-22-bringing-it-all-together.txt:718,搜「Storing the password」)。「not needed and would be a security risk as it would be stored in the clear in the session data」;header 重定向见第 722 段。 ↩ ↩2 ↩3 ↩4 -
出处:「Chapter 22. Bringing It All Together」第 920 段(
text/33-ch22-chapter-22-bringing-it-all-together.txt:920,搜「preg_replace」)与第 924 段(:924,搜「UPDATE profiles」)、第 931 段(:931,搜「htmlentities」)。压空白、UPDATE/INSERT 二选一、textarea 预填。 ↩ -
出处:「Chapter 22. Bringing It All Together」第 938 段(
text/33-ch22-chapter-22-bringing-it-all-together.txt:938,搜「move_uploaded_file」)。「$saveto = "$user.jpg"」——文件名由用户名定死。 ↩ -
出处:「Chapter 22. Bringing It All Together」第 840 段(
text/33-ch22-chapter-22-bringing-it-all-together.txt:840,搜「getimagesize」)与第 855 段(:855,搜「may not be correct」)。「Don't use the $_FILES array to get the image type... could... be modified by the attacker」。 ↩ ↩2 ↩3 -
出处:「Chapter 22. Bringing It All Together」第 874 段(
text/33-ch22-chapter-22-bringing-it-all-together.txt:874,搜「$max」)与第 878 段(:878,搜「imagecopyresampled」)、第 882 段(:882,搜「imagedestroy」)。等比缩放、卷积锐化、释放画布。 ↩ ↩2 -
出处:「Chapter 22. Bringing It All Together」第 979 段(
text/33-ch22-chapter-22-bringing-it-all-together.txt:979,搜「showProfile」)。保存后即时回显。 ↩ -
出处:「Chapter 22. Bringing It All Together」第 1096 段(
text/33-ch22-chapter-22-bringing-it-all-together.txt:1096,搜「view」)与第 1113 段(:1113,搜「WHERE user=?」)、第 1119 段(:1119,搜「remove」)。view/add/remove 三模式;防重复插入见第 1115 段。 ↩ ↩2 ↩3 -
出处:「Chapter 22. Bringing It All Together」第 1132 段(
text/33-ch22-chapter-22-bringing-it-all-together.txt:1132,搜「while ($row」)与第 1143 段(:1143,搜「t1」)、第 1149 段(:1149,搜「mutual friend」)。两次查询两个方向,t1/t2 组合判定关系。 ↩ -
出处:「Chapter 22. Bringing It All Together」第 1189 段(
text/33-ch22-chapter-22-bringing-it-all-together.txt:1189,搜「array_intersect」)与第 1196 段(:1196,搜「array_diff」)。「array_intersect function extracts all members common to both arrays」。 ↩ ↩2 -
出处:「Chapter 22. Bringing It All Together」第 1349 段(
text/33-ch22-chapter-22-bringing-it-all-together.txt:1349,搜「INSERT INTO messages」)与第 1383 段(:1384,搜「erase」)。pm 存 0/1 与「只删收件人是我的」限定。 ↩ ↩2 ↩3 -
出处:「Chapter 22. Bringing It All Together」第 1388 段(
text/33-ch22-chapter-22-bringing-it-all-together.txt:1388,搜「ORDER BY time DESC」)与第 1398 段(:1398,搜「!$pm」)、第 1406 段(:1406,搜「whispered」)、第 1380 段(:1380,搜「date_default_timezone_set」)。 ↩ ↩2 -
出处:「Chapter 22. Bringing It All Together」第 1464 段(
text/33-ch22-chapter-22-bringing-it-all-together.txt:1464,搜「destroySession」)与第 1465 段(:1465,搜「Location」)。 ↩ ↩2 -
出处:「Chapter 22. Bringing It All Together」第 1491 段(
text/33-ch22-chapter-22-bringing-it-all-together.txt:1491,搜「universal selector」)与第 1507 段(:1507,搜「button:hover」)、第 1585 段(:1585,搜「max-width」)。「unlike setting width, allows the content to be smaller if the device screen is smaller」见第 1497 段。 ↩ ↩2 ↩3 -
出处:「Chapter 22. Bringing It All Together」第 1701 段(
text/33-ch22-chapter-22-bringing-it-all-together.txt:1701,搜「function byId」)。「contains the byId, style, and by functions used throughout this book」见第 1691 段。 ↩ ↩2 -
出处:「Chapter 22. Bringing It All Together」第 1077 段(
text/33-ch22-chapter-22-bringing-it-all-together.txt:1077,搜「thousands or even millions」)。「you would substantially modify this program to include support for searching... paging」。 ↩ ↩2 ↩3 -
出处:「Chapter 22. Bringing It All Together」第 566 段(
text/33-ch22-chapter-22-bringing-it-all-together.txt:566,搜「more fluid response」)。「A more fluid response at this point might be to automatically log in a newly created user, but because I don't want to overly complicate the code」。 ↩ -
出处:「Chapter 22. Bringing It All Together」第 97 段(
text/33-ch22-chapter-22-bringing-it-all-together.txt:97,搜「Robin's Nest」)。 ↩ -
出处:「Chapter 22. Bringing It All Together」第 66 段(
text/33-ch22-chapter-22-bringing-it-all-together.txt:66,搜「User profiles」)。功能清单见同段一带。 ↩ -
出处:「Chapter 22. Bringing It All Together」第 127 段(
text/33-ch22-chapter-22-bringing-it-all-together.txt:127,搜「database login details」)。 ↩ -
出处:「Chapter 22. Bringing It All Together」第 162 段(
text/33-ch22-chapter-22-bringing-it-all-together.txt:162,搜「showProfile」)。 ↩ -
出处:「Chapter 22. Bringing It All Together」第 170 段(
text/33-ch22-chapter-22-bringing-it-all-together.txt:170,搜「error handling is in place」)。 ↩ -
出处:「Chapter 22. Bringing It All Together」第 261 段(
text/33-ch22-chapter-22-bringing-it-all-together.txt:261,搜「session_start」);导航链接见第 349–370 段。 ↩ -
出处:「Chapter 22. Bringing It All Together」第 257 段(
text/33-ch22-chapter-22-bringing-it-all-together.txt:257,搜「single require_once」)。 ↩ -
出处:「Chapter 22. Bringing It All Together」第 432–456 段(
text/33-ch22-chapter-22-bringing-it-all-together.txt:432,搜「CREATE TABLE」)。 ↩ -
出处:「Chapter 22. Bringing It All Together」第 388 段(
text/33-ch22-chapter-22-bringing-it-all-together.txt:388,搜「before calling up any other files」);重申见第 574 段。 ↩ -
出处:「Chapter 22. Bringing It All Together」第 608 段(
text/33-ch22-chapter-22-bringing-it-all-together.txt:608,搜「INSERT INTO members」)。 ↩ -
出处:「Chapter 22. Bringing It All Together」第 551 段(
text/33-ch22-chapter-22-bringing-it-all-together.txt:551,搜「blur event」);fetch 调用见第 644 段。 ↩ -
出处:「Chapter 22. Bringing It All Together」第 665 段(
text/33-ch22-chapter-22-bringing-it-all-together.txt:665,搜「checkuser.php」)。 ↩ -
出处:「Chapter 22. Bringing It All Together」第 722 段(
text/33-ch22-chapter-22-bringing-it-all-together.txt:722,搜「Location」);重定向代码见第 755 段。 ↩ -
出处:「Chapter 22. Bringing It All Together」第 878–882 段(
text/33-ch22-chapter-22-bringing-it-all-together.txt:878,搜「imagecreatetruecolor」)。 ↩ -
出处:「Chapter 22. Bringing It All Together」第 982 段(
text/33-ch22-chapter-22-bringing-it-all-together.txt:982,搜「multipart/form-data」)。 ↩ -
出处:「Chapter 22. Bringing It All Together」第 1012 段(
text/33-ch22-chapter-22-bringing-it-all-together.txt:1012,搜「two modes」)。 ↩ -
出处:「Chapter 22. Bringing It All Together」第 1422 段(
text/33-ch22-chapter-22-bringing-it-all-together.txt:1422,搜「Refresh messages」)。 ↩ -
出处:「Chapter 22. Bringing It All Together」第 388 段(
text/33-ch22-chapter-22-bringing-it-all-together.txt:388,搜「type and load into your browser」)。 ↩ -
出处:「Chapter 22. Bringing It All Together」第 482 段(
text/33-ch22-chapter-22-bringing-it-all-together.txt:482,搜「index.php」)。 ↩ -
出处:「Chapter 22. Bringing It All Together」第 329 段(
text/33-ch22-chapter-22-bringing-it-all-together.txt:329,搜「javascript.js」)。 ↩ -
出处:「Chapter 22. Bringing It All Together」第 449 段(
text/33-ch22-chapter-22-bringing-it-all-together.txt:449,搜「CREATE TABLE IF NOT EXISTS friends」)。 ↩ -
出处:「Chapter 22. Bringing It All Together」第 438 段(
text/33-ch22-chapter-22-bringing-it-all-together.txt:438,搜「CREATE TABLE IF NOT EXISTS messages」)。 ↩ -
出处:「Chapter 22. Bringing It All Together」第 164 段(
text/33-ch22-chapter-22-bringing-it-all-together.txt:164,搜「<user.jpg>」)。 ↩ -
出处:「Chapter 22. Bringing It All Together」第 261–269 段(
text/33-ch22-chapter-22-bringing-it-all-together.txt:261,搜「session_start」)。 ↩ -
出处:「Chapter 22. Bringing It All Together」第 261 段(
text/33-ch22-chapter-22-bringing-it-all-together.txt:261,搜「represents a visit」)。 ↩ -
出处:「Chapter 22. Bringing It All Together」第 170 段(
text/33-ch22-chapter-22-bringing-it-all-together.txt:170,搜「provide your own error-handling」)。 ↩ -
出处:「Chapter 22. Bringing It All Together」第 665 段(
text/33-ch22-chapter-22-bringing-it-all-together.txt:665,搜「checkuser.php」)。 ↩ -
出处:「Chapter 22. Bringing It All Together」第 1312 段(
text/33-ch22-chapter-22-bringing-it-all-together.txt:1312,搜「messages.php」)。 ↩ -
出处:「Chapter 22. Bringing It All Together」第 432–456 段(
text/33-ch22-chapter-22-bringing-it-all-together.txt:432,搜「CREATE TABLE IF NOT EXISTS members」)。 ↩