1select * from posts;在文章列表页通过执行以上 SQL 语句获取全部文章数据:
xxxxxxxxxx21// 查询全部文章数据2$posts = xiu_query('select * from posts');在表格中将多余的 <tr> 标记移除,通过 foreach 遍历查询到的 $posts 变量,绑定数据:
名词解释: 数据绑定是指将一个有结构的数据输出到特定结构的 HTML 上。
xxxxxxxxxx141<?php foreach ($posts as $item) { ?>2<tr>3 <td class="text-center"><input type="checkbox"></td>4 <td><?php echo $item['title']; ?></td>5 <td><?php echo $item['user_id']; ?></td>6 <td><?php echo $item['category_id']; ?></td>7 <td><?php echo $item['created']; ?></td>8 <td><?php echo $item['status']; ?></td>9 <td class="text-center">10 <a href="post-add.php" class="btn btn-default btn-xs">编辑</a>11 <a href="javascript:;" class="btn btn-danger btn-xs">删除</a>12 </td>13</tr>14<?php } ?>🚩 源代码: step-19
Sometimes, 我们希望在界面上看到的数据并不是数据库中原始的数据,而是经过一个特定的转换逻辑转换过后的结果。这种情况经常发生在输出时间、状态和关联数据时。
一般情况下,我们在数据库存储标识都用英文或数字方式存储,但是在界面上应该展示成中文方式,所以我们需要在输出的时候做一次转换,转换方式就是定义一个转换函数:
xxxxxxxxxx171/**2 * 将英文状态描述转换为中文3 * @param string $status 英文状态4 * @return string 中文状态5 */6function convert_status ($status) {7 switch ($status) {8 case 'drafted':9 return '草稿';10 case 'published':11 return '已发布';12 case 'trashed':13 return '回收站';14 default:15 return '未知';16 }17}在输出时调用这个函数:
xxxxxxxxxx11<td class="text-center"><?php echo convert_status($item['status']); ?></td>如果需要自定义发布时间的展示格式,可以通过 date() 函数完成,而 date() 函数所需的参数除了控制输出格式的 format 以外,还需要一个整数类型的 timestamp。
我们已有的是一个类似于 2012-12-12 12:00:00 具有标准时间格式的字符串,必须转换成整数类型的 timestamp,才能调用 date() 函数。我们可以借助 strtotime() 函数完成时间字符串到时间戳(timestamp)的转换:
http://php.net/manual/zh/function.strtotime.php
- 在使用
strtotime()之前,确保通过date_default_timezone_set()设置默认时区
于是乎,定义一个格式化日期的转换函数:
x1/**2 * 格式化日期3 * @param string $created 时间字符串4 * @return string 格式化后的时间字符串5 */6function format_date ($created) {7 // 设置默认时区!!!8 date_default_timezone_set('UTC');910 // 转换为时间戳11 $timestamp = strtotime($created);1213 // 格式化并返回 由于 r 是特殊字符,所以需要 \r 转义一下14 return date('Y年m月d日 <b\r> H:i:s', $timestamp);15}在输出的位置调用此函数:
xxxxxxxxxx11<td class="text-center"><?php echo format_date($item['created']); ?></td>🚩 源代码: step-20
我们在文章表中存储的分类信息就只是分类的 ID,直接输出到表格中,并不友好,所以我们需要在输出分类信息时再次根据分类 ID 查询对应文章的分类信息,将查询到的结果输出到 HTML 中。
定义一个根据分类 ID 获取分类信息的函数:
xxxxxxxxxx91/**2 * 根据 ID 获取分类信息3 * @param integer $id 分类 ID4 * @return array 分类信息关联数组5 */6function get_category ($id) {7 $sql = sprintf('select * from categories where id = %d', $id);8 return xiu_query($sql)[0];9}在输出分类名称的位置通过调用该函数输出分类名称:
xxxxxxxxxx11<td><?php echo get_category($item['category_id'])['name']; ?></td>同上所述,按照相同的方式查询文章的作者信息并输出:
xxxxxxxxxx91/**2 * 根据 ID 获取用户信息3 * @param integer $id 用户 ID4 * @return array 用户信息关联数组5 */6function get_author ($id) {7 $sql = sprintf('select * from users where id = %d', $id);8 return xiu_query($sql)[0];9}在输出作者的位置通过调用该函数输出作者昵称:
xxxxxxxxxx11<td><?php echo get_author($item['user_id'])['nickname']; ?></td>🚩 源代码: step-21
按照以上的方式,可以正常输出分类和作者信息,但是过程中需要有大量的数据库连接和查询操作。
在实际开发过程中,一般不这么做,通常我们会使用联合查询的方式,同时把我们需要的信息查询出来:
xxxxxxxxxx41select *2from posts3inner join users on posts.user_id = users.id4inner join categories on posts.category_id = categories.id;以上这条语句可以把 posts、users、categories 三张表同时查询出来(查询到一个结果集中)。
所以我们可以移除 get_category 和 get_author 两个函数,将查询语句改为上面定义的内容,完成一次性查询。
🚩 源代码: step-22
这样查询也有一些小问题:如果这几个表中有相同名称的字段,在查询过后转换为关联数组就会有问题(关联数组的键是不能重复的),所以我们需要指定需要查询的字段,同时还可以给每一个字段起一个别名,避免冲突:
xxxxxxxxxx101select2 posts.id,3 posts.title,4 posts.created,5 posts.status,6 categories.name as category_name,7 users.nickname as author_name8from posts9inner join users on posts.user_id = users.id10inner join categories on posts.category_id = categories.id;从另外一个角度来说:指定了具体的查询字段,也会提高数据库检索的速度。
🚩 源代码: step-23
导入更多的测试数据
当数据过多过后,如果还是按照以上操作每次查询全部数据,页面就显得十分臃肿,加载起来也非常慢,所以必须要通过分页加载的方式改善(每次只显示一部分数据)。
操作方式也非常简单,就是在原有 SQL 语句的基础之上加上 limit 和 order by 子句:
xxxxxxxxxx121select2 posts.id,3 posts.title,4 posts.created,5 posts.status,6 categories.name as category_name,7 users.nickname as author_name8from posts9inner join users on posts.user_id = users.id10inner join categories on posts.category_id = categories.id11order by posts.created desc12limit 0, 10limit 用法:limit [offset, ]rows
limit 10-- 只取前 10 条数据limit 5, 10-- 从第 5 条之后,第 6 条开始,向后取 10 条数据
limit 子句中的 0 和 10 不是一成不变的,应该跟着页码的变化而变化,具体的规则就是:
limit 0, 10limit 10, 10limit 20, 10limit 30, 10根据以上规则得出公式:offset = (page - 1) * size
xxxxxxxxxx221// 处理分页2// ========================================34$size = 10;5$page = 2;67// 查询数据8// ========================================910// 查询全部文章数据11$posts = xiu_query(sprintf('select12 posts.id,13 posts.title,14 posts.created,15 posts.status,16 categories.name as category_name,17 users.nickname as author_name18from posts19inner join users on posts.user_id = users.id20inner join categories on posts.category_id = categories.id21order by posts.created desc22limit %d, %d', ($page - 1) * $size, $size));🚩 源代码: step-24
一般分页都是通过 URL 传递一个页码参数(通常使用 querystring)
也就是说,我们应该在页面开始执行的时候获取这个 URL 参数:
xxxxxxxxxx141// 处理分页2// ========================================34// 定义每页显示数据量(一般把这一项定义到配置文件中)5$size = 10;67// 获取分页参数 没有或传过来的不是数字的话默认为 18$page = isset($_GET['p']) && is_numeric($_GET['p']) ? intval($_GET['p']) : 1;910if ($page <= 0) {11 // 页码小于 1 没有任何意义,则跳转到第一页12 header('Location: /admin/posts.php?p=1');13 exit;14}🚩 源代码: step-25
用户在使用分页功能时不可能通过地址栏改变要访问的页码,必须通过可视化的链接点击访问,所以我们需要根据数据的情况在界面上显示分页链接。

组合一个分页跳转链接的必要条件:
以上必要条件中只有第一条需要额外处理,其余的目前都可以拿到,所以重点攻克第一条:
一共有多少页面取决于一共有多少条数据和每一页显示多少条,计算公式为:$total_pages = ceil($total_count / $size)。
通过查询数据库可以得到总条数:
xxxxxxxxxx81// 查询总条数2$total_count = intval(xiu_query('select count(1)3from posts4inner join users on posts.user_id = users.id5inner join categories on posts.category_id = categories.id')[0][0]);67// 计算总页数8$total_pages = ceil($total_count / $size);思考:为什么要在查询条数的时候也用联合查询
知道了总页数,就可以对 URL 中传递过来的分页参数做范围校验了($page <= $totel_pages)
xxxxxxxxxx51if ($page > $total_pages) {2 // 超出范围,则跳转到最后一页3 header('Location: /admin/posts.php?p=' . $total_pages);4 exit;5}🚩 源代码: step-26
xxxxxxxxxx111<ul class="pagination pagination-sm pull-right">2 <?php if ($page - 1 > 0) : ?>3 <li><a href="?p=<?php echo $page - 1; ?>">上一页</a></li>4 <?php endif; ?>5 <?php for ($i = 1; $i <= $total_pages; $i++) : ?>6 <li<?php echo $i === $page ? ' class="active"' : '' ?>><a href="?p=<?php echo $i; ?>"><?php echo $i; ?></a></li>7 <?php endfor; ?>8 <?php if ($page + 1 <= $total_pages) : ?>9 <li><a href="?p=<?php echo $page + 1; ?>">下一页</a></li>10 <?php endif; ?>11</ul>🚩 源代码: step-27
由于分页功能在不同页面都会被使用到,所以也提取到 functions.php 文件中,封装成一个 xiu_pagination 函数:
xxxxxxxxxx251/**2 * 输出分页链接3 * @param integer $page 当前页码4 * @param integer $total 总页数5 * @param string $format 链接模板,%d 会被替换为具体页数6 * @example7 * <?php xiu_pagination(2, 10, '/list.php?page=%d'); ?>8 */9function xiu_pagination ($page, $total, $format) {10 // 上一页11 if ($page - 1 > 0) {12 printf('<li><a href="%s">上一页</a></li>', sprintf($format, $page - 1));13 }1415 // 数字页码16 for ($i = 1; $i <= $total; $i++) {17 $activeClass = $i === $page ? ' class="active"' : '';18 printf('<li%s><a href="%s">%d</a></li>', $activeClass, sprintf($format, $i), $i);19 }2021 // 下一页22 if ($page + 1 <= $total) {23 printf('<li><a href="%s">下一页</a></li>', sprintf($format, $page + 1));24 }25}🚩 源代码: step-28
按照目前的实现情况,已经可以正常使用分页链接了,但是当总页数过多,显示起来也会有问题,所以需要控制显示页码的个数,一般情况下,我们是根据当前页码在中间,左边和右边各留几位。
实现以上需求的思路:主要就是想办法根据当前页码知道应该从第几页开始显示,到第几页结束,另外需要注意不能超出范围。
以下是具体实现:
xxxxxxxxxx531/**2 * 输出分页链接3 * @param integer $page 当前页码4 * @param integer $total 总页数5 * @param string $format 链接模板,%d 会被替换为具体页数6 * @param integer $visible 可见页码数量(可选参数,默认为 5)7 * @example8 * <?php xiu_pagination(2, 10, '/list.php?page=%d', 5); ?>9 */10function xiu_pagination ($page, $total, $format, $visible = 5) {11 // 计算起始页码12 // 当前页左侧应有几个页码数,如果一共是 5 个,则左边是 2 个,右边是两个13 $left = floor($visible / 2);14 // 开始页码15 $begin = $page - $left;16 // 确保开始不能小于 117 $begin = $begin < 1 ? 1 : $begin;18 // 结束页码19 $end = $begin + $visible - 1;20 // 确保结束不能大于最大值 $total21 $end = $end > $total ? $total : $end;22 // 如果 $end 变了,$begin 也要跟着一起变23 $begin = $end - $visible + 1;24 // 确保开始不能小于 125 $begin = $begin < 1 ? 1 : $begin;2627 // 上一页28 if ($page - 1 > 0) {29 printf('<li><a href="%s">«</a></li>', sprintf($format, $page - 1));30 }3132 // 省略号33 if ($begin > 1) {34 print('<li class="disabled"><span>···</span></li>');35 }3637 // 数字页码38 for ($i = $begin; $i <= $end; $i++) {39 // 经过以上的计算 $i 的类型可能是 float 类型,所以此处用 == 比较合适40 $activeClass = $i == $page ? ' class="active"' : '';41 printf('<li%s><a href="%s">%d</a></li>', $activeClass, sprintf($format, $i), $i);42 }4344 // 省略号45 if ($end < $total) {46 print('<li class="disabled"><span>···</span></li>');47 }4849 // 下一页50 if ($page + 1 <= $total) {51 printf('<li><a href="%s">»</a></li>', sprintf($format, $page + 1));52 }53}🚩 源代码: step-29
注意:不要立即联想 AJAX 的问题,AJAX 是后来诞生的,换而言之不是必须的,我们这里讨论的只是传统的方式(历史使人明智)
实现思路:用户只有在未筛选的情况下知道一共有哪些状态,当用户选择其中某个状态过后,必须让服务端知道用户选择的状态是什么,从而返回指定的状态下的数据。
好有一比:去商店买钻石,你不可能直接说来颗 5 斤的,正常情况,你都是先问有没有钻石,售货员拿出一排,顺便告诉你有哪几种重量的,你再告诉售货员你选其中哪一种,售货员再拿出指定种类的让你挑。
所以我们先在页面上添加一个表单(用于接收用户后续的意愿),然后提供一个 <select> 列出全部状态,让用户选择。用户选择完了再把表单提交回来,此时服务端就知道你需要什么状态的数据。
注意:永远都不要说历史上的事 low,历史永远是伟大的。
不设置表单的 method 默认就是 get,此处就应该使用 get,原因有二:
xxxxxxxxxx91<form class="form-inline" action="/admin/posts.php">2 <select name="s" class="form-control input-sm">3 <option value="all">所有状态</option>4 <option value="drafted">草稿</option>5 <option value="published">已发布</option>6 <option value="trashed">回收站</option>7 </select>8 <button class="btn btn-default btn-sm">筛选</button>9</form>在查询数据之前,接受参数,组织查询参数:
xxxxxxxxxx121// 处理筛选逻辑2// ========================================34// 数据库查询筛选条件(默认为 1 = 1,相当于没有条件)5$where = '1 = 1';67// 状态筛选8if (isset($_GET['s']) && $_GET['s'] != 'all') {9 $where .= sprintf(" and posts.status = '%s'", $_GET['s']);10}1112// $where => " and posts.status = 'drafted'"然后在进行查询时添加 where 子句:
xxxxxxxxxx231// 查询总条数2$total_count = intval(xiu_query('select count(1)3from posts4inner join users on posts.user_id = users.id5inner join categories on posts.category_id = categories.id6where ' . $where)[0][0]);78...910// 查询全部文章数据11$posts = xiu_query(sprintf('select12 posts.id,13 posts.title,14 posts.created,15 posts.status,16 categories.name as category_name,17 users.nickname as author_name18from posts19inner join users on posts.user_id = users.id20inner join categories on posts.category_id = categories.id21where %s22order by posts.created desc23limit %d, %d', $where, ($page - 1) * $size, $size));🚩 源代码: step-30
筛选后,<select> 中被选中的 <option> 应该在展示的时候默认选中:
xxxxxxxxxx61<select name="s" class="form-control input-sm">2 <option value="all">所有状态</option>3 <option value="drafted"<?php echo isset($_GET['s']) && $_GET['s'] == 'drafted' ? ' selected' : ''; ?>>草稿</option>4 <option value="published"<?php echo isset($_GET['s']) && $_GET['s'] == 'published' ? ' selected' : ''; ?>>已发布</option>5 <option value="trashed"<?php echo isset($_GET['s']) && $_GET['s'] == 'trashed' ? ' selected' : ''; ?>>回收站</option>6</select>🚩 源代码: step-31
✏️ 作业: 仿照状态筛选功能的实现过程,实现分类筛选功能
🚩 源代码: step-32
目前来说,单独看筛选功能和分页功能都是没有问题,但是同时使用会有问题:
筛选过后,页数不对(没有遇到,但是常见)。
筛选过后,点分页链接访问其他页,筛选状态丢失。
只要在涉及到分页链接的地方加上当前的筛选参数即可解决问题,所以我们在接收状态筛选参数时将其记录下来:
xxxxxxxxxx161// 记录本次请求的查询参数2$query = '';34// 状态筛选5if (isset($_GET['s']) && $_GET['s'] != 'all') {6 $where .= sprintf(" and posts.status = '%s'", $_GET['s']);7 $query .= '&s=' . $_GET['s'];8}910// 分类筛选11if (isset($_GET['c']) && $_GET['c'] != 'all') {12 $where .= sprintf(" and posts.category_id = %d", $_GET['c']);13 $query .= '&c=' . $_GET['c'];14}1516// $query => "&s=drafted&c=2"🚩 源代码: step-33