WordPress 里的 PHP 实战
这一课你将能做到
- 在模板里正确驱动主循环(The Loop),并说清它与主查询($wp_query)的关系
- 用 pre_get_posts 改主查询、用 WP_Query 开次级查询,且知道何时必须 wp_reset_postdata()
- 写出 WP 风格的 PHP 8.2 代码:Yoda 条件、命名空间、late escaping(输出时转义)
为什么要学
WP 本质是一个 PHP 应用。你学的 PHP 8.2 特性(只读属性、枚举、命名空间)在 WP 里全部可用,但 WP 有自己的「方言」:全局变量、The Loop、条件标签、转义函数家族。摸清这套方言,模板和插件才不会互相污染数据。
人话:The Loop 相当于一个游标。WP 根据 URL 先生成一次「主查询」存进全局 $wp_query,模板里 have_posts()/the_post() 就是推动游标逐条输出。你中途另开一个 WP_Query 查询会把「当前文章」这个全局指针挪走——不归位,后面的循环全部错乱,这是新手最高频的事故。
先建立图像
URL: /category/java/
↓ rewrite 解析 → 主查询 WP_Query( category_name=java ) 存入 $wp_query
↓ 模板层级选中 category.php
category.php:
have_posts()? ──yes→ the_post() → $post 指向当前行 → the_title()/the_content()
↑ ↓
└────────── while 循环直到 false ←──────┘
↓
wp_reset_postdata()(次级查询后必须调)
跟着做
步骤 1:标准主循环
在子主题 single.php 中:
<?php
get_header();
if ( have_posts() ) :
while ( have_posts() ) :
the_post();
?>
<article>
<h1><a href="<?php the_permalink(); ?>"><?php the_title(); ?></a></h1>
<time><?php echo esc_html( get_the_date( 'Y-m-d' ) ); ?></time>
<div class="content"><?php the_content(); ?></div>
</article>
<?php
endwhile;
else :
echo '<p>暂无内容</p>';
endif;
get_footer();
the_*() 系列直接输出(内部 echo),get_the_*() 返回值——需要转义、拼接时用后者。
步骤 2:条件标签分流
if ( is_front_page() ) {
get_template_part( 'template-parts/hero', 'home' );
} elseif ( is_single() ) {
get_template_part( 'template-parts/card', get_post_type() );
} elseif ( is_category( 'java' ) ) {
echo '<div class="badge">Java 专栏</div>';
} elseif ( is_page( 'about' ) ) {
get_template_part( 'template-parts/team' );
}
常用族:is_single/is_page/is_archive/is_category/is_tag/is_search/is_404/is_admin/is_paged。
步骤 3:改主查询的正确入口
需求:分类页每页显示 6 篇并排除置顶。不要在模板里写 query_posts(),挂 pre_get_posts:
add_action( 'pre_get_posts', function ( WP_Query $query ): void {
if ( is_admin() || ! $query->is_main_query() ) {
return; // 只动前台主查询
}
if ( $query->is_category() ) {
$query->set( 'posts_per_page', 6 );
$query->set( 'ignore_sticky_posts', true );
}
} );
这是 WP 面试的经典考点:改主查询用 pre_get_posts(查询发生前改 SQL),不要查询后再换内容。
步骤 4:次级查询与归位
侧边栏「最新课程」:
$courses = new WP_Query( array(
'post_type' => 'course',
'posts_per_page' => 5,
'no_found_rows' => true, // 不算总页数,更快
) );
if ( $courses->have_posts() ) :
while ( $courses->have_posts() ) :
$courses->the_post(); // 注意:把全局 $post 挪走了
the_title();
endwhile;
wp_reset_postdata(); // 归位主查询
endif;
步骤 5:现代 PHP 落地到插件
<?php
namespace Qrocmq\Course;
enum Level: string {
case Beginner = 'beginner';
case Advanced = 'advanced';
public function label(): string {
return match ( $this ) {
self::Beginner => '入门',
self::Advanced => '进阶',
};
}
}
final class CourseMeta {
public function __construct(
public readonly string $title,
public readonly Level $level,
public readonly int $durationHours,
) {}
}
枚举、只读属性、match、命名空间与自动加载(Composer)在 WP 6.x + PHP 8.2 中可放心使用——类文件照常 require 或经 autoload 引入即可。
步骤 6:late escaping 输出
// 差:入库时净化就以为安全了(数据来源可能变化)
// 好:输出点转义,语义明确
echo esc_html( $post->post_title ); // 纯文本
echo esc_url( home_url( '/courses' ) ); // URL
echo esc_attr( $meta['level'] ); // 属性值
echo wp_kses_post( get_the_content() ); // 允许富文本白名单
讲清楚要点
- 主查询一生只有一次:由 URL 决定、before 模板渲染执行;改它用 pre_get_posts,不要在模板里再跑一遍同条件的 WP_Query 造成双倍 SQL。
- the_ 与 get_the_ 的分工**:前者输出(多数内部已转义),后者返回值交你处理——处理就别忘了 esc_*。
- 全局变量污染链:WP_Query→the_post() 改写全局 $post;任何「嵌套循环输出错位」的幽灵问题,先查 reset。
- WP 方言:函数名下划线小写、双引号、Yoda 条件(
'publish' === $status防止误赋值)、_e()/__()包裹可翻译字符串。 - PHP 8.2 的使用边界:枚举/readonly 随便用;动态属性已废弃(8.2 弃用警告),别再依赖 WP 老代码里 \stdClass 随手加属性的习惯。
你可能会懵的地方
-
have_posts() 循环里调用 the_post() 到底干了什么? 把游标后移一位并重置全局 $post 为当前文章——之后所有模板函数(the_title 等)都在操作这个 $post。
-
the_content() 和 get_the_content() 的实际区别? the_content() 应用了完整过滤器链(wpautop 段落化、短代码渲染、区块序列化转换),get_the_content() 是原始存储内容,直接输出会少掉全部加工。
-
get_posts() 和 new WP_Query 选哪个? 两者底层一样,get_posts 默认 no_found_rows=true 更轻,适合「拿几条就完事」;需要分页/计算总页数用 WP_Query。
-
为什么我的页面出现两套循环内容重复? 主查询已渲染过一次,你在模板里又用相同参数跑了一遍次级查询。改主查询(pre_get_posts)而不是再查。
-
PHP 8.2 报 Deprecated: Creation of dynamic property? 老插件/老代码在运行时给对象动态加属性。定位到哪个插件(Query Monitor/日志),升级或替换;自己新代码用类属性或 #[\AllowDynamicProperties]。
错法对照
| 错法 | 问题 | 正解 |
|---|---|---|
| query_posts( ... ) 改列表 | 抛弃主查询、双份 SQL、破坏分页 | pre_get_posts + is_main_query |
| 次级查询后不 wp_reset_postdata() | 全局 $post 污染,后续循环错乱 | 每次次级查询结束立即归位 |
| echo $post->post_title | XSS(标题可含 HTML 实体注入) | echo esc_html(...) |
| 用 == 比较 'publish' 和 0 | 松散比较 PHP8 下触发意外 | Yoda 严格比较 === |
| 在 Loop 外调 the_title() | 没有当前文章,输出异常 | get_the_title( $post_id ) 带 ID |
卡点急救
- 列表页输出重复文章:主查询被二次执行或循环没 break——检查是否误用 WP_Query 替代主查询。
- 循环内容全部错位:找最近一次未 reset 的次级查询,补 wp_reset_postdata()。
- 升级 PHP 8.2 后页面报 Deprecated 刷屏:先看日志定位插件,临时在开发环境保留警告逐个修,生产关闭 DISPLAY。
面试常问
-
pre_get_posts 和 WP_Query 分别在什么场景用? pre_get_posts 修改主查询(归档页数量、首页排除分类),发生在 SQL 生成前;WP_Query 用于模板内全新的次级查询(侧栏、相关文章),两者不可混用。
-
the_content() 的过滤器链说明什么? 输出经过 wpautop、do_shortcode、区块转换等多层 filter,这就是 filter 体系的价值:内容加工被拆成可插拔环节,插件可以在任意环节介入。
-
如何在 WP 中安全输出一段用户提交的富文本? 输出时用 wp_kses_post 白名单过滤;纯文本用 esc_html;属性用 esc_attr;URL 用 esc_url——late escaping 保证无论数据源头如何变化,输出点是最后一道闸。
自测三问
- 主查询和次级查询的本质区别是什么?各自正确的修改/使用入口?
- wp_reset_postdata() 归位的是哪个变量?不归位的典型症状?
- 写出 four 个 esc_* 函数及其适用场景。
小结一句话
The Loop 是主查询的迭代器,pre_get_posts 是改主查询的唯一正门,次级查询用完必须归位;配合 WP 方言与 late escaping,你就能写出与核心逻辑和平共处的现代 PHP。
延伸
- The Loop 官方文档
- WP_Query 类参考
- 下一课:数据库——$wpdb、表结构与 meta 查询的性能真相。