WP DEVELOP

EP041. “建立 Professor 文章类型并与 Program 建立关联”

首页 WordPress 开发课程 PROFESSOR POST TYPE · EP041
约 20 分钟· #EP041#PROFESSOR POST TYPE
🔒 登录后可标记已读

📌 并入说明(EP040. Quick Note About The Next Lesson): EP040 是一篇很短的文字提醒,不是视频讲座:下一讲(EP041)注册 professor 文章类型时, 记得加上 'show_in_rest' => true 这一行。原文还提醒,每一讲附带的 zip 代码包永远是最新版, 可以放心信任。这个提醒已经体现在下面 EP041 的代码里。


两个目标:新建 professor 自定义文章类型,并且在 professor 与 program 之间建立双向关联—— professor 详情页要能显示这位教授教什么科目,program 详情页也要能反过来列出教这个科目的所有教授。 professor 不需要归档页(访问者不会通过导航栏进 professor 列表,而是通过 program 页面、校区页面或 以后的搜索功能找到教授)。这一讲顺带借这个双向查询的场景,讲清楚了 wp_reset_postdata() 到底在干什么、 为什么必须在同一个模板里连续跑两个自定义查询时调用它。

涉及文件

  • wp-content/mu-plugins/university-post-types.php (修改,在 university_post_types() 函数里新增 professor 的 register_post_type() 调用)
  • wp-content/themes/fictional-university-theme/single-professor.php (新建,以 single-event.php 为起点复制修改)
  • wp-content/themes/fictional-university-theme/single-program.php (修改,新增 related professors 查询)
  • ACF 后台设置(不是代码文件):把已有的「Related Programs」字段组的 Location 规则从「Post Type == event」改成「Post Type == event OR Post Type == professor」

代码实现


university-post-types.php(修改)

复制 program 那段 register_post_type() 改名而来。因为不需要归档页,所以没有 has_archiverewrite 这两行(has_archive 默认就是 false,不需要显式写 false):

// 加在 university_post_types() 函数里,Program Post Type 那段代码后面

// Professor Post Type
register_post_type('professor', array(
  'show_in_rest' => true,
  'supports' => array('title', 'editor'),
  'public' => true,
  'labels' => array(
    'name' => 'Professors',
    'add_new_item' => 'Add New Professor',
    'edit_item' => 'Edit Professor',
    'all_items' => 'All Professors',
    'singular_name' => 'Professor'
  ),
  'menu_icon' => 'dashicons-welcome-learn-more'
));

建完之后要记得去 WordPress 后台 Settings → Permalinks 点一次 Save Changes,强制刷新 permalink 结构, 不然访问 professor 详情页会显示 Page not found。


single-professor.php(新建)

single-event.php 整份复制过来当起点,删掉 event 详情页才需要的 meta box(Events Home 那个 div), 标题从「Related Program(s)」改成「Subject(s) Taught」(这是 professor 页面专属的措辞,不影响 event/program 共用的 related_programs 字段本身):

<?php
  
  get_header();

  while(have_posts()) {
    the_post(); ?>
    <div class="page-banner">
      <div class="page-banner__bg-image" style="background-image: url(<?php echo get_theme_file_uri('/images/ocean.jpg') ?>);"></div>
      <div class="page-banner__content container container--narrow">
        <h1 class="page-banner__title"><?php the_title(); ?></h1>
        <div class="page-banner__intro">
          <p>DONT FORGET TO REPLACE ME LATER</p>
        </div>
      </div>  
    </div>

    <div class="container container--narrow page-section">

      <div class="generic-content"><?php the_content(); ?></div>

      <?php

        $relatedPrograms = get_field('related_programs');

        if ($relatedPrograms) {
          echo '<hr class="section-break">';
          echo '<h2 class="headline headline--medium">Subject(s) Taught</h2>';
          echo '<ul class="link-list min-list">';
          foreach($relatedPrograms as $program) { ?>
            <li><a href="<?php echo get_the_permalink($program); ?>"><?php echo get_the_title($program); ?></a></li>
          <?php }
          echo '</ul>';
        }

      ?>

    </div>

  <?php }

  get_footer();

?>

这段代码之所以「一新建就能用」,是因为它是从 single-event.php 抄过来的,而 Section 8 已经把 related_programs 这个 ACF 字段组挂在 event 上了。要让它同样出现在 professor 编辑页,还需要去 ACF 后台把这个字段组的 Location 规则加一条 OR:Post Type == event OR Post Type == professor

[截图:前台某位教授详情页底部"Subject(s) Taught"区块,列出该教授关联科目链接的画面]


single-program.php(修改)

在原本「upcoming events」自定义查询的前面,新增一个「related professors」查询,query 名沿用 首页那段查询复制过来改名。posts_per_page 从 2 改成 -1(拉出全部关联教授),orderby 改成 title 按教授姓名字母排序,post_type 改成 professormeta_query 只保留「关联到当前这个 program」这一个 filter(删掉了 event 查询特有的日期 filter):

<?php 
  $relatedProfessors = new WP_Query(array(
    'posts_per_page' => -1,
    'post_type' => 'professor',
    'orderby' => 'title',
    'order' => 'ASC',
    'meta_query' => array(
      array(
        'key' => 'related_programs',
        'compare' => 'LIKE',
        'value' => '"' . get_the_ID() . '"'
      )
    )
  ));

  if ($relatedProfessors->have_posts()) {
    echo '<hr class="section-break">';
    echo '<h2 class="headline headline--medium">' . get_the_title() . ' Professors</h2>';

    while($relatedProfessors->have_posts()) {
      $relatedProfessors->the_post(); ?>
      <li><a href="<?php the_permalink(); ?>"><?php the_title(); ?></a></li>
    <?php }
  }

  wp_reset_postdata();

  $today = date('Ymd');
  // ↓ 原本就有的 upcoming events 查询,紧接在 wp_reset_postdata() 后面 ↓

wp_reset_postdata() 加在 related professors 查询和 upcoming events 查询中间,这是这一讲的重点。 原文用了一个小实验来解释原因:如果在文件最上面(program 循环开头)echo the_ID();,会看到当前 program(比如 biology)的 ID;但如果 related professors 的 while 循环跑过 $relatedProfessors->the_post() 之后,再在任何地方调用 the_ID() / get_the_ID(),拿到的会是最后一个教授的 ID,不是当前 program 的 ID—— 因为 the_post() 会把 WordPress 全局 post 对象「劫持」成这个自定义查询里当前这一项。而下面 upcoming events 的 meta_query 里用 get_the_ID() 取当前 program 的 ID 来筛选关联事件,如果这个 ID 已经被 professor 查询污染了,upcoming events 查询就会查不到任何东西(原文实测出现「upcoming events 整个消失」的现象)。 wp_reset_postdata() 就是把全局 post 数据重置回「当前这个模板本来对应的那篇内容」。 结论:一个模板里如果要连续跑多个自定义查询,两个查询之间基本都要调用一次这个函数。

Hook / Function 速查

名称类型用途
register_post_type()WP 内建 function这一讲用来注册 professor 这个新的自定义文章类型
WP_QueryWP 内建 class自定义查询,这一讲用来查出「关联到当前 program 的所有 professor」
wp_reset_postdata()WP 内建 function自定义查询循环结束后重置全局 post 数据;这一讲详细解释了不调用会出什么问题
get_the_ID() / the_ID()WP 内建 function返回/输出当前全局 post 的 ID,这一讲用来演示 wp_reset_postdata() 的作用
get_field()ACF function(返回值)取出 related_programs 字段的值(数组),单篇 professor 页面借用了 event 页面已有的字段组

常见坑

  • 新建自定义文章类型后忘记去 Settings → Permalinks 点 Save Changes,访问详情页会显示 Page not found。
  • ACF 字段组的 Location 规则要用「OR」按钮加条件,不能用默认的「AND」——AND 要求两个条件同时成立,
    永远不会在 professor 编辑页显示出来。
  • 在同一个模板文件里连续跑两个自定义查询,如果中间不调用 wp_reset_postdata(),后面那个查询里凡是
    依赖全局 post 数据的函数(get_the_ID()the_title() 等)都会被前一个查询的最后一项「污染」,
    导致查询条件出错、内容整个消失或显示错乱。

延伸 / 后续讲座会用到

下一讲 EP042 会给 professor 加上 featured image(特色图片)支持,让每位教授能上传自己的照片, 并在 single-professor.php 和 single-program.php 里都会用到刚建立的这套 related professors 循环。

Sources

Udemy:

  • Become a WordPress Developer: Unlocking Power With Code — Section 9, EP040(并入), EP041