编程语言研究书库

综合项目 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 从进门到发出第一条私信」走一遍——全项目一半以上的文件 都会在这条线上出场,每一步旁边有具体状态:

  1. t0 安装:管理员跑 setup.php → 四张表建成;Jill 打开 localhost/robinsnest → index.php 经 header 渲染,「Welcome Guest」+ 三键菜单。
  2. t1 注册:Jill 填用户名 jill、移开光标 → blur → fetch 到 checkuser.php → members 表查 0 行 → 回「✔ available」显示在输入框旁。 填密码点 Sign Up → signup.php 查重再查一次 → 占位符 INSERT 一行 (jill, 密码哈希) → 显示「Account created,Please Log in」。
  3. t2 登录:jill 输密码 → login.php 占位符查出哈希 → password_verify 通过 → $_SESSION['user']='jill' → header('Location: members.php?view=jill') → 浏览器跳到成员页,header now 显示「Logged in as: jill」,菜单变六键。
  4. t3 资料:Jill 在 profile.php 写「Coffee, code, cats.」,传一张 800×600 的猫图 → move 存成 jill.jpg → getimagesize 判 JPEG → 等比缩到 100×75 → 卷积锐化 → imagejpeg 落盘 → showProfile 回显:头像居左 + 那句话。
  5. t4 社交:成员页见朋友 Jack → 点 follow → members.php?add=jack 占位符 INSERT 进 friends(user=jill, friend=jack)。 Jack 也 follow 了 jill → friends.php 里 array_intersect 算出互粉, 两人的好友页都显示「mutual friends: 对方」。
  6. t5 私信:Jill 在 messages.php?view=jack 写「hi」,选 Private → INSERT 一行(auth=jill, recip=jack, pm=1, time=秒数, message=hi)。 Jack 在自己的消息页看到 whispered: hi(斜体);别的用户看不到这行。
  7. 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 走一遍,因为它是全项目「展示他人内容」的唯一通道—— 所有页面显示任何人的资料都走它,消毒因此也只有这一处要守:

  1. 调 file_exists("$user.jpg"):这个用户的头像文件在不在磁盘上? 在 → <img src='$user.jpg'> 浮左显示。文件名来自用户名, 而用户名入库时已被注册流程约束过(非空),此处不再注入风险。
  2. prepare("SELECT * FROM profiles WHERE user=?") + execute([$user]) ——占位符查询该用户的资料行。
  3. fetch() 取行:有 → htmlentities($row['text']) 输出「about me」 (用户写过的任何 HTML 在这里全部转义,防 XSS);无 → 输出 「Nothing to see here, yet」占位。
  4. <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 干四件事:

  1. session_start() —— 开启会话(必须在任何输出前);
  2. include functions.php(所以功能页只 require 一次 header 就够);
  3. 认人:if (isset($_SESSION['user'])) → $loggedin = TRUE, 取出用户名(过 htmlentities)写进页头问候;否则 $loggedin = FALSE、 显示「Welcome Guest」6;
  4. 输出 HTML 头 + 双菜单:未登录只见 Home/Sign Up/Log In, 登录后见 Home/Members/Friends/Messages/Edit Profile/Log out 全套7。

把 header.php 的一次执行走到底(它每个页面都跑一遍,值得看清):

  1. 某功能页被请求 → 第一行 require header.php → PHP 转入 header。
  2. header 第一行 session_start()——PHP 检查请求里的会话 cookie: 有,就按 ID 打开服务器上的会话数据(第 12 章的「凭号领物」); 没有,就新建一个(本页会写入,响应头带新 cookie)。
  3. require functions.php——$pdo 此刻建成,全页可用; destroySession 与 showProfile 两个函数就绪。
  4. 判定 $_SESSION['user']:有值 → 会话里躺着用户名 → $loggedin=TRUE, 用户名字符串过 htmlentities 存进 $user_html_entities(与它的脏版隔离); 无值 → $loggedin=FALSE。
  5. 输出 <!DOCTYPE html>…<body> 的开头 HTML:标题里嵌问候语 (<title>Robin's Nest: Logged in as: jill</title>),链上 styles.css、 javascript.js、Bootstrap Icons CDN。
  6. 按 $loggedin 二选一输出菜单块;随后 </div></div><div class="content"> 开内容容器——收尾留给功能页(各功能页最后自己补 </div></body></html>)。
  7. 控制回到功能页,从自己的业务代码继续。

这个「骨架 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。

把四张表的「将来怎么用」摆在一起看,设计意图就清楚了:

表列(简)回答的问题
membersuser(索引)、pass(255)谁是注册用户、他的密码哈希
friendsuser、friend(都索引)谁关注了谁(单向一行)
profilesuser(索引)、text(4096)谁的「about me」写了什么
messagesid(主键,自增)、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。

把「注册一次」从头到尾走一遍(这一步覆盖全项目一半的手艺):

  1. 访客填 Robin,光标移开用户名框( blur )。
  2. JS 端 fetch 异步发到 checkuser.php;PHP 占位符查 members → 0 行 → 回传绿勾「available」→ span 里出现提示。页面没有刷新。
  3. 访客填密码点 Sign Up → POST 到 signup.php 自身(第 11 章模式)。
  4. signup.php 经 header 拿到会话;先 destroySession() 清掉任何旧登录态 (防半登录残态);再查重、插库(密码哈希入库)。
  5. 成功:die 输出「Account created,Please Log in」。
  6. 访客去 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 章会话机制的完整实战):

  1. login.php 验证通过 → $_SESSION['user'] = $user ——此刻 PHP 把 这个键值写进服务器端的会话文件;响应头里,会话 cookie(若本轮新建) 被浏览器收下。
  2. 浏览器带着 cookie 请求 members.php → header.php 的 session_start() 按 cookie 里的 ID 打开同一份会话文件 → $_SESSION['user'] 有值 → $loggedin=TRUE、菜单变六键。
  3. members.php 要用用户名:从 $_SESSION['user'] 取——不碰表单、不碰 URL, 身份只从会话读(view 参数另算,那是「看谁的资料」,不是「我是谁」)。
  4. 点 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 章的骨架),处理后是一条五站流水线:

  1. move_uploaded_file($_FILES['image']['tmp_name'], "$user.jpg")—— 文件名用用户名定死(Jill → Jill.jpg),不给用户输入文件名的机会 (第 7 章安全纪律的实战版)18;
  2. getimagesize() 验明正身——Warning 原话:别信 $_FILES 里的 type 字段,那是浏览器报的,可伪造;getimagesize 是读文件内容判型, 只放行 GIF/JPEG/PNG 三种 IMAGETYPE19;
  3. 按类型用 imagecreatefromgif/jpeg/png 载入成 PHP 可处理的图像;
  4. 算缩放:最长边压到 $max=100,短边等比;imagecreatetruecolor 开画布, imagecopyresampled 重采样,imageconvolution 锐化(重采样会轻微糊, 用卷积拉回来),imagejpeg 存回 $saveto;
  5. 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 模式值得单独走一遍——它是「看别人的资料」的完整链路,也是参数消毒 的又一个样板:

  1. isset($_GET['view']) 为真 → $view = $_GET['view']——URL 里的参数 原样收下,但立刻分流:一个脏版($view,用于查库、比较), 一个干净版($view_html_entities = htmlentities($view),用于输出和拼 URL)。
  2. 判断 $view === $user:看的是自己 → 标题用「Your Profile」; 看的是别人 → 「jill's Profile」(用干净版拼)。
  3. 调 showProfile($view, $pdo) —— 用脏版查库(占位符会处理好它), 输出的消毒在 showProfile 内部完成。
  4. 附一个「View jill's messages」按钮,链接的 view 参数也用干净版。
  5. 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(对方→我)显示文字给出的链接
00(无关系标注)follow
10you are followingdrop
01is following yourecip
11is a mutual frienddrop

循环体的骨架因此只有四层:取一个成员 → 两次占位符查询算 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']):

  1. array_intersect 对两个数组取交——只有 'jack' 两边都在 → $mutual = 'jack';
  2. array_diff($followers, $mutual)——在 followers 里但不在 mutual 里的: 'anna'(我的单向粉丝);
  3. array_diff($following, $mutual)——'bob'(我在单向关注的人)。
  4. 三个数组各 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 章通用选择器
.contentmax-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 从教学版到生产版的路线

原书在多处自陈这是教学实现。把「要补哪些课才能上线」按优先级排成一张路线表 (每条都对应本书某章的边界条目):

优先级要补的课出处
P0CSRF 令牌:所有 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.1 的 t0 建库建表,在浏览器里把 t1–t6 走一遍;
  2. 再带着问题读文件——「登录态是怎么传到 members.php 的」读 login/header, 「头像为什么缩到 100 像素」读 profile,「互粉怎么算」读 friends;
  3. 最后做一件破坏——把某一处的 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 类、成功用绿 .availableUI 惯例(语义色),无机制依据,但与行业惯例一致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. 可带走的

  1. 项目结构:公共层(functions/header/js/css)+ 一次性的 setup + 一功能一文件。
  2. header.php 的 $loggedin 门禁:每个功能页第一句就是认人与踢人。
  3. 注册 = 双端校验 + blur 异步查重:第 1、16、17 章在此合龙; 注册前先 destroySession 清残态,是容易被忽略的好卫生。
  4. 登录三行:password_verify 通过 → $_SESSION['user'] → header('Location:…')。
  5. 会话只存用户名;密码哈希永远只出现在 verify 的一行里——连会话文件都不许见它。
  6. 上传安全三连:文件名程序定死、内容判型用 getimagesize、类型白名单三选。
  7. 图片流水线:move → getimagesize → imagecreatefrom* → 等比缩到 $max → 重采样 + 卷积锐化 → imagejpeg → 释放画布。
  8. 加友就是 friends 表插一行;互粉 = 两行方向相反;求交用 array_intersect。
  9. 私信 = 一列 pm + 一条显示判定(公开全可见,私密收发双方可见;界面不给删别人的按钮, SQL 里再拦一道)。
  10. 纪律清单(第 9 节那张表)比功能清单更值钱——项目能跑靠功能,项目敢上线靠纪律。

13.1 全书收束:一次「三件套合奏」的复盘

这一章是全书唯一一次让三件套同台。回看 §1.1 那条时间线,每一站用了哪些章节:

时刻功能主要用到的章节
t1 注册表单、查重、哈希、异步11、12、16、17
t2 登录占位符、verify、会话、重定向10、12
t3 资料文件上传、GD 图像、showProfile7、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 练习场(项目改造)

  1. 粘性注册:signup 失败时回显已填字段(注意 htmlentities)。
  2. remember me:登录页加"记住我",用长效 cookie 存用户名, 下次自动填(第 12 章练习 2 的项目版)。
  3. 消息即达:messages.php 加每 10 秒自动 fetch 刷新的 定时器——从"手动刷新"到"准实时"的一步之遥。
  4. 头像防御:给上传加扩展名白名单(第 7 章安全版的复用)。
  5. 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 式的怪物表就是这么来的。 项目选择了拆开,现场理由有三:

  1. 好友关系是多对多:一个用户多个好友,一个好友属于多个用户—— 宽表放不下,只能行长存(违反一阶)或重复存(违反二阶)。
  2. 关系会变化:加、删好友只动 friends 表的一两行; 宽表里这是大手术,还随时漏改(第 9 章"只更新了一行重复数据" 的翻车现场)。
  3. 查询对称:"谁是我的好友"与"谁是某人的好友"是同一条 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 章里"没讲为什么、但值得问为什么"的决策挖出来:

  1. 为什么公共逻辑只有两文件(functions/header)? 第 5 章 include 纪律的最小执行;文件越少,包含图越浅, 初学者越不会迷路。
  2. 为什么登录跳转用 header('Location:...')? POST 成功后重定向,防刷新重复提交(PR 模式的雏形); 第 11 章流程图"通过→结果页"的标准实现。
  3. 为什么消息要手动刷新? 实时推送的复杂度会淹没教学重点——本书的取舍始终是 "机制要全,工程要简"。
  4. 为什么头像存文件、资料存数据库? 大二进制进文件系统(可走静态服务器直发),可检索文本进 SQL——各回各家,也是"让数据在离数据最近的地方被加工" (§9.34 判据)的又一应用。
  5. 为什么全书最后才上项目? 因为项目章没有新知,只有结算。来得早了,它会被当成新课; 放在最后,它才是结业考。

13.18 结业自测:能教别人,才算学会

最后一轮自测,形式升级——不是答题,是讲解。假想一位 只读完第 1 章的读者,用十分钟讲清以下任一条,对方真懂了才算你过:

  1. 注册时 blur 查重的完整链路(为什么不只信 JS);
  2. session 从登录到登出的三次"换场"(header/login/logout);
  3. friends 表为什么要独立存在(用"怪物表"的下场说服对方);
  4. 头像管线的五步,以及"文件名即用户名"的收益与代价;
  5. 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,在项目各处有三种具体表现:

  1. PDO 连接失败:try...catch 包住,渲染(而非裸抛) 一句"服务暂不可用";
  2. 查询无结果:不是报错,是"空列表也是合法界面"—— members.php 对零好友、messages.php 对零消息, 都显示一句 Empty 式的提示而非红字;
  3. 用户操作失败(如加好友时对方已是好友):回到页面, 就地提示,流程不断。

三形态的共同底色:错误是界面的一个状态,不是程序的终点。 这句话在第 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 拆解本书的最后一份交接

面向本书库未来的维护者,留四句话:

  1. 本书的骨架是"问答模型":任何新增的拆解内容,先问自己 它属于问答的哪一环,对不上的内容要警惕;
  2. 第 20/21 章是选修航线:增删内容时保持"经典栈主线不动、 新栈只作概念课"的比例;
  3. 项目章(第 22 章)的每一个机制都能指回具体章节—— 新增机制时必须同时更新 §13.8 对账总表,这是全书一致性的 总闸;
  4. 本书所有例子追求"零构建可复现":任何让例子依赖构建工具的 改动,都会摧毁它作为教学资产的最大价值——慎之又慎。

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 如果只能改一处:项目的五个候选改进与优先级

假设只有时间做一处改进,五个候选按"教学价值 ÷ 工作量"排:

  1. 粘性表单(§13.10 练习 1)——工作量一小时, 收益是注册体验从"教学级"到"可用级";
  2. POST-Redirect-GET 收尾(§13.17 决策 2 的全面执行)—— 工作量半天,收益是刷新重复提交从此绝迹;
  3. 唯一约束落库(用户名列加 UNIQUE)——工作量十分钟, 收益是数据库成为查重的最后一道铁墙;
  4. 错误页统一(自定义 404/500 与 PDO 异常页)——工作量一天, 收益是项目第一次有了"产品脸";
  5. 消息定时刷新(§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 Togethertext/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

  1. 出处:「Chapter 22. Bringing It All Together」第 5–91 段(text/33-ch22-chapter-22-bringing-it-all-together.txt:11,搜「Session control」)。功能六项清单逐项列出。 ↩

  2. 出处:「Chapter 4. Expressions and Control Flow in PHP」第 1949 段(text/07-ch04-chapter-4-expressions-and-control-flow-in-php.txt:1949,搜「PHP Modularization」)——模块化主张;本章的文件结构是它的落地。 ↩

  3. 出处:「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 一致。 ↩

  4. 出处:「Chapter 22. Bringing It All Together」第 217 段(text/33-ch22-chapter-22-bringing-it-all-together.txt:217,搜「showProfile」)。头像存在判断(file_exists)、占位符查 profiles、htmlentities 输出,同段可见。 ↩ ↩2

  5. 出处:「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

  6. 出处:「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

  7. 出处:「Chapter 22. Bringing It All Together」第 345 段(text/33-ch22-chapter-22-bringing-it-all-together.txt:345,搜「if ($loggedin)」)——双菜单 if/else。 ↩ ↩2

  8. 出处:「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

  9. 出处:「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"> 用法。 ↩

  10. 出处:「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

  11. 出处:「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

  12. 出处:「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

  13. 出处:「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」)。 ↩

  14. 出处:「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

  15. 出处:「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

  16. 出处:「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

  17. 出处:「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 预填。 ↩

  18. 出处:「Chapter 22. Bringing It All Together」第 938 段(text/33-ch22-chapter-22-bringing-it-all-together.txt:938,搜「move_uploaded_file」)。「$saveto = "$user.jpg"」——文件名由用户名定死。 ↩

  19. 出处:「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

  20. 出处:「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

  21. 出处:「Chapter 22. Bringing It All Together」第 979 段(text/33-ch22-chapter-22-bringing-it-all-together.txt:979,搜「showProfile」)。保存后即时回显。 ↩

  22. 出处:「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

  23. 出处:「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 组合判定关系。 ↩

  24. 出处:「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

  25. 出处:「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

  26. 出处:「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

  27. 出处:「Chapter 22. Bringing It All Together」第 1464 段(text/33-ch22-chapter-22-bringing-it-all-together.txt:1464,搜「destroySession」)与第 1465 段(:1465,搜「Location」)。 ↩ ↩2

  28. 出处:「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

  29. 出处:「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

  30. 出处:「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

  31. 出处:「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」。 ↩

  32. 出处:「Chapter 22. Bringing It All Together」第 97 段(text/33-ch22-chapter-22-bringing-it-all-together.txt:97,搜「Robin's Nest」)。 ↩

  33. 出处:「Chapter 22. Bringing It All Together」第 66 段(text/33-ch22-chapter-22-bringing-it-all-together.txt:66,搜「User profiles」)。功能清单见同段一带。 ↩

  34. 出处:「Chapter 22. Bringing It All Together」第 127 段(text/33-ch22-chapter-22-bringing-it-all-together.txt:127,搜「database login details」)。 ↩

  35. 出处:「Chapter 22. Bringing It All Together」第 162 段(text/33-ch22-chapter-22-bringing-it-all-together.txt:162,搜「showProfile」)。 ↩

  36. 出处:「Chapter 22. Bringing It All Together」第 170 段(text/33-ch22-chapter-22-bringing-it-all-together.txt:170,搜「error handling is in place」)。 ↩

  37. 出处:「Chapter 22. Bringing It All Together」第 261 段(text/33-ch22-chapter-22-bringing-it-all-together.txt:261,搜「session_start」);导航链接见第 349–370 段。 ↩

  38. 出处:「Chapter 22. Bringing It All Together」第 257 段(text/33-ch22-chapter-22-bringing-it-all-together.txt:257,搜「single require_once」)。 ↩

  39. 出处:「Chapter 22. Bringing It All Together」第 432–456 段(text/33-ch22-chapter-22-bringing-it-all-together.txt:432,搜「CREATE TABLE」)。 ↩

  40. 出处:「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 段。 ↩

  41. 出处:「Chapter 22. Bringing It All Together」第 608 段(text/33-ch22-chapter-22-bringing-it-all-together.txt:608,搜「INSERT INTO members」)。 ↩

  42. 出处:「Chapter 22. Bringing It All Together」第 551 段(text/33-ch22-chapter-22-bringing-it-all-together.txt:551,搜「blur event」);fetch 调用见第 644 段。 ↩

  43. 出处:「Chapter 22. Bringing It All Together」第 665 段(text/33-ch22-chapter-22-bringing-it-all-together.txt:665,搜「checkuser.php」)。 ↩

  44. 出处:「Chapter 22. Bringing It All Together」第 722 段(text/33-ch22-chapter-22-bringing-it-all-together.txt:722,搜「Location」);重定向代码见第 755 段。 ↩

  45. 出处:「Chapter 22. Bringing It All Together」第 878–882 段(text/33-ch22-chapter-22-bringing-it-all-together.txt:878,搜「imagecreatetruecolor」)。 ↩

  46. 出处:「Chapter 22. Bringing It All Together」第 982 段(text/33-ch22-chapter-22-bringing-it-all-together.txt:982,搜「multipart/form-data」)。 ↩

  47. 出处:「Chapter 22. Bringing It All Together」第 1012 段(text/33-ch22-chapter-22-bringing-it-all-together.txt:1012,搜「two modes」)。 ↩

  48. 出处:「Chapter 22. Bringing It All Together」第 1422 段(text/33-ch22-chapter-22-bringing-it-all-together.txt:1422,搜「Refresh messages」)。 ↩

  49. 出处:「Chapter 22. Bringing It All Together」第 388 段(text/33-ch22-chapter-22-bringing-it-all-together.txt:388,搜「type and load into your browser」)。 ↩

  50. 出处:「Chapter 22. Bringing It All Together」第 482 段(text/33-ch22-chapter-22-bringing-it-all-together.txt:482,搜「index.php」)。 ↩

  51. 出处:「Chapter 22. Bringing It All Together」第 329 段(text/33-ch22-chapter-22-bringing-it-all-together.txt:329,搜「javascript.js」)。 ↩

  52. 出处:「Chapter 22. Bringing It All Together」第 449 段(text/33-ch22-chapter-22-bringing-it-all-together.txt:449,搜「CREATE TABLE IF NOT EXISTS friends」)。 ↩

  53. 出处:「Chapter 22. Bringing It All Together」第 438 段(text/33-ch22-chapter-22-bringing-it-all-together.txt:438,搜「CREATE TABLE IF NOT EXISTS messages」)。 ↩

  54. 出处:「Chapter 22. Bringing It All Together」第 164 段(text/33-ch22-chapter-22-bringing-it-all-together.txt:164,搜「<user.jpg>」)。 ↩

  55. 出处:「Chapter 22. Bringing It All Together」第 261–269 段(text/33-ch22-chapter-22-bringing-it-all-together.txt:261,搜「session_start」)。 ↩

  56. 出处:「Chapter 22. Bringing It All Together」第 261 段(text/33-ch22-chapter-22-bringing-it-all-together.txt:261,搜「represents a visit」)。 ↩

  57. 出处:「Chapter 22. Bringing It All Together」第 170 段(text/33-ch22-chapter-22-bringing-it-all-together.txt:170,搜「provide your own error-handling」)。 ↩

  58. 出处:「Chapter 22. Bringing It All Together」第 665 段(text/33-ch22-chapter-22-bringing-it-all-together.txt:665,搜「checkuser.php」)。 ↩

  59. 出处:「Chapter 22. Bringing It All Together」第 1312 段(text/33-ch22-chapter-22-bringing-it-all-together.txt:1312,搜「messages.php」)。 ↩

  60. 出处:「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」)。 ↩

On this page

综合项目 Robin's Nest — 用三件套从零造一个社交网站1. 顶层全景:一张文件地图1.1 一条完整的时间线(全书合龙的主线例子)2. 地基三件套(主线例子的起点)2.1 functions.php:连接 + 两个公共函数2.2 header.php:每个页面的「出生证明」2.3 setup.php:四张表3. 注册:异步查重的第一次实战(主线例子)4. 登录:password_verify 与「登录态的诞生」5. profile.php:资料 + 图片流水线6. members.php:目录与「加友/解除」7. friends.php:互粉 = 数组求交8. messages.php:公开与私信9. 收尾三文件与全书纪律的最终盘点9.1 从教学版到生产版的路线9.2 这一章的正确读法10. 作者的判断与证据11.1 一个判断块之外的判断:怎么评价这个项目11. 边界与局限12. 可带走的13.1 全书收束:一次「三件套合奏」的复盘13.2 项目的三个基本盘:读代码前的坐标系13.3 地基三件套:functions.php / header.php / setup.php13.4 注册链路:signup.php 与 checkuser.php 的合奏13.5 登录与登出:session 的完整生命周期13.6 资料页:图片处理的意外主战场13.7 社交图谱:members / friends / messages 三页13.8 全书机制的回收站:一张对账总表13.9 把项目跑起来的验收清单13.10 练习场(项目改造)13.11 index.php 与 javascript.js:被低估的两个小文件13.12 friends 表的独立:一次规范化决策的现场13.13 members.php 双模式的读码走查13.14 messages.php:一次对话的数据结构13.15 头像管线的完整数据流13.16 生产化差距清单(书外补充)13.17 项目的五个设计决策考古13.18 结业自测:能教别人,才算学会13.19 header.php 逐行走查:八行代码里的四件事13.20 登录成功的三行解剖13.21 错误处理在项目里的真实形态13.22 二十二章的一次回望:每章一句,全书闭环13.23 FAQ(项目章五问)13.24 全书的最后一格13.25 六个页面骨架的逐页要点13.26 数据库四表的最终档案13.27 从 Robin's Nest 到下一个项目:可迁移清单13.28 拆解本书的最后一份交接13.29 项目的时间维度:一个请求的完整传记13.30 如果只能改一处:项目的五个候选改进与优先级13.31 拆解报告的句号13.32 全书一致性总检:二十二章的口径核对13.33 最后的自测十问(全书级)13.34 余白与致谢13.35 余白·补一行13.36 收笔13.37 收笔·最后一行13.38 余白·末段13. 原文地图Footnotes