WP DEVELOP

EP171-172. “admin-post.php:前台表单增删数据”

首页 WordPress 开发课程 插件开发 CH3:数据库存储 · EP171-172
约 23 分钟· #EP171-172#插件开发 CH3:数据库存储
🔒 登录后可标记已读

📌 说明:EP171 的 transcript 在「there's an extra column here with maybe a red x that you click on to delete that pet.」这句话处结束,EP172 开头是同一句话的重复接续——两者是同一段实操内容被切成两个文件,合并成一篇笔记。

📌 并入 EP170 的提醒:EP170 是 EP168 那条「getArgs() 要用 isset() 避免 PHP Warning」勘误的重复提醒,内容完全一致,已经记在 [[EP166-169_构建动态SQL查询-URL参数拼接WHERE条件]] 里,这里不重复贴代码。


/pet-adoption 页面加上「新增宠物」和「删除宠物」两个只有管理员能操作的表单,学习 WordPress 标准的前台表单提交处理方式:admin-post.php。表单提交到这个 WordPress 系统文件,配合表单里一个名叫 action 的隐藏字段的值,WordPress 会自动触发对应命名规则的 hook(admin_post_{action值} / admin_post_nopriv_{action值}),处理完在 hook 回调函数里用 wp_safe_redirect() 跳回原页面。这也是这门课插件开发第三章(数据库存储)的最后一讲。


涉及文件

  • wp-content/plugins/new-database-table/inc/template-pets.php (修改,新增表单)
  • wp-content/plugins/new-database-table/new-database-table.php (修改,新增处理表单提交的 hook 和方法)

代码实现

inc/template-pets.php:表格里给管理员加「删除」列 + 底部新增宠物表单

<table class="pet-adoption-table">
  <tr>
    <th>Name</th>
    <th>Species</th>
    <th>Weight</th>
    <th>Birth Year</th>
    <th>Hobby</th>
    <th>Favorite Color</th>
    <th>Favorite Food</th>
    <?php if (current_user_can('administrator')) { ?>
      <th>Delete</th>
    <?php } ?>
  </tr>
  <?php
    foreach($getPets->pets as $pet) { ?>
      <tr>
        <td><?php echo $pet->petname; ?></td>
        <td><?php echo $pet->species; ?></td>
        <td><?php echo $pet->petweight; ?></td>
        <td><?php echo $pet->birthyear; ?></td>
        <td><?php echo $pet->favhobby; ?></td>
        <td><?php echo $pet->favcolor; ?></td>
        <td><?php echo $pet->favfood; ?></td>
        <?php if (current_user_can('administrator')) { ?>
          <td style="text-align: center;">
          <form action="<?php echo esc_url(admin_url('admin-post.php')) ?>" method="POST">
            <input type="hidden" name="action" value="deletepet">
            <input type="hidden" name="idtodelete" value="<?php echo $pet->id; ?>">
            <button class="delete-pet-button">X</button>
          </form>
        </td>
        <?php } ?>
      </tr>
    <?php }
  ?>
</table>

<?php 
  if (current_user_can('administrator')) { ?>
    <form action="<?php echo esc_url(admin_url('admin-post.php')) ?>" class="create-pet-form" method="POST">
      <p>Enter just the name for a new pet. Its species, weight, and other details with be randomly generated.</p>
      <input type="hidden" name="action" value="createpet">
      <input type="text" name="incomingpetname" placeholder="name...">
      <button>Add Pet</button>
    </form>
  <?php }
?>

new-database-table.php:注册 hook + 新增/删除的处理方法

function __construct() {
  global $wpdb;
  $this->charset = $wpdb->get_charset_collate();
  $this->tablename = $wpdb->prefix . "pets";

  add_action('activate_new-database-table/new-database-table.php', array($this, 'onActivate'));
  add_action('admin_post_createpet', array($this, 'createPet'));
  add_action('admin_post_nopriv_createpet', array($this, 'createPet'));
  add_action('admin_post_deletepet', array($this, 'deletePet'));
  add_action('admin_post_nopriv_deletepet', array($this, 'deletePet'));
  add_action('wp_enqueue_scripts', array($this, 'loadAssets'));
  add_filter('template_include', array($this, 'loadTemplate'), 99);
}

function deletePet() {
  if (current_user_can('administrator')) {
    $id = sanitize_text_field($_POST['idtodelete']);
    global $wpdb;
    $wpdb->delete($this->tablename, array('id' => $id));
    wp_safe_redirect(site_url('/pet-adoption'));
  } else {
    wp_safe_redirect(site_url());
  }
  exit;
}

function createPet() {
  if (current_user_can('administrator')) {
    $pet = generatePet();
    $pet['petname'] = sanitize_text_field($_POST['incomingpetname']);
    global $wpdb;
    $wpdb->insert($this->tablename, $pet);
    wp_safe_redirect(site_url('/pet-adoption'));
  } else {
    wp_safe_redirect(site_url());
  }
  exit;
}

顺带的小改动:给「找到 X 条结果」的数字加千分位逗号

<p>This page took <strong><?php echo timer_stop();?></strong> seconds to prepare. Found <strong><?php echo number_format($getPets->count); ?></strong> results (showing the first <?php echo count($getPets->pets) ?>).</p>

关键改动点:

  • current_user_can('administrator'):整个「新增」表单、以及表格里的「删除」列/按钮,都只对管理员显示——不是管理员的访客完全看不到这些控件,也无法提交表单
  • 表单提交目标统一是 admin_url('admin-post.php')admin-post.php 是 WordPress 内建的系统文件,专门设计给前台表单提交后处理用——很多人第一次看到这个文件名会本能地觉得「前台不该提交到 admin 相关的文件」,但这其实是 WordPress 官方推荐、完全预期会被这样使用的标准做法;用它的好处是处理逻辑可以统一写在插件的主 PHP 文件里,不需要在模板文件顶部堆砌一堆「检查某个变量是否已提交」的判断代码
  • esc_url(admin_url('admin-post.php'))admin_url() 生成后台某个文件的完整 URL,esc_url() 是标准的 URL 转义函数,输出到 HTML 属性里之前做一层安全处理
  • <input type="hidden" name="action" value="..."> 是整套机制的关键:表单里必须有一个名字精确为 action 的隐藏字段,WordPress 会读取这个字段的值,自动触发名为 admin_post_{这个值} 的 hook——这里新增用的是 createpet,删除用的是 deletepet(都是全小写连写,没有下划线/驼峰,跟 hook 名字符串精确对应)
  • admin_post_{action}admin_post_nopriv_{action} 要成对注册admin_post_createpet 只会在「用户已登录」(不管是不是管理员)时触发;如果访客完全没有登录,WordPress 改为触发 admin_post_nopriv_createpet——这一对 hook 要同时注册、且指向同一个处理函数,因为函数体内部本来就会再检查一次 current_user_can('administrator'),两种触发路径最终执行的还是同一套权限判断逻辑
  • createPet():用之前就有的 generatePet() 辅助函数随机生成一整只假宠物的数据,再用表单提交上来的 $_POST['incomingpetname'](经过 sanitize_text_field() 清理)覆盖掉里面的 petname 字段,最后 $wpdb->insert($this->tablename, $pet) 整个数组一次性插入
  • deletePet():从 $_POST['idtodelete'](同样先 sanitize_text_field() 清理)拿到要删除的宠物 ID,调用 $wpdb->delete($表名, array('id' => $id))——$wpdb->delete() 的第二个参数是一个「列名 => 值」的条件数组,效果等同于 DELETE FROM 表 WHERE id = 值
  • 成功/失败都要重定向、并且用 wp_safe_redirect() 而不是 wp_redirect()wp_safe_redirect() 会额外检查目标 URL 是否指向本站允许的域名,比普通的 wp_redirect() 多一层安全保障,官方建议只要是根据某些条件动态决定跳转目标(哪怕只是简单的 if/else 两个分支)都优先用这个安全版本
  • exit 放在 if/else 判断结构外面、只写一次:不管是「有权限、操作完重定向」还是「没权限、重定向回首页」,两个分支最后都要执行重定向后紧跟 exit(阻止 PHP 继续执行后面根本不需要的代码)——与其在每个分支各写一次 exit,不如把它提到 if/else 结构外面统一收尾,减少重复
  • 删除按钮的表单里额外带一个隐藏字段存 idtodelete:因为要删除的是「当前这一行」的宠物,这个 ID 只有在渲染这一行的时候才知道,所以每一行都单独生成一个迷你表单、把这一行对应的 ID 存进隐藏字段里
  • number_format($getPets->count):PHP 内建函数,给大数字每三位加一个千分位逗号(比如 100000 显示成 100,000),纯粹的展示细节优化

Hook / Function 速查

名称类型用途
admin-post.phpWP 系统文件官方推荐的前台表单提交统一处理入口
admin_post_{action值}WP 动态 hook已登录用户提交带有 action={值} 隐藏字段的表单时触发
admin_post_nopriv_{action值}WP 动态 hook未登录访客提交同样表单时触发,需跟上面那个成对注册
current_user_can($角色)WP 内建 function判断当前用户是否具备指定角色/权限
$wpdb->delete($表名, $条件数组)$wpdb 方法按条件删除表中的记录
wp_safe_redirect($url)WP 内建 function安全跳转,额外校验目标 URL 是否指向本站允许的域名
number_format($数字)PHP 内建 function给数字加千分位分隔符,用于展示

常见坑

  • 表单的隐藏字段名字拼错(不是精确的 action)——WordPress 不会识别,不会触发任何自定义 hook
  • 只注册 admin_post_{action},漏掉 admin_post_nopriv_{action}——未登录的访客提交表单时会遇到 WordPress 默认报错,因为没有对应的 hook 处理这种情况
  • wp_redirect() 而不是 wp_safe_redirect()——少了一层「跳转目标是否为本站域名」的安全校验
  • 重定向之后忘记调用 exit——PHP 会继续执行重定向语句之后的代码,可能导致重复输出或者不可预期的行为
  • 处理函数内部不再次检查 current_user_can(),只依赖前端表单本身是否显示——只在模板里判断「是否管理员」来决定显不显示表单和按钮是不够的,真正的权限校验必须在服务器端的处理函数里再做一次,否则任何人都能绕过前端直接构造请求提交

[截图:管理员登录状态下的 /pet-adoption 页面,表格每行末尾多出的红色 X 删除按钮,以及页面底部的 Add Pet 新增宠物表单]


Sources

Udemy:

  • Become a WordPress Developer: Unlocking Power With Code — Section 27, EP170, EP171, EP172