WP DEVELOP

EP166-169. “构建动态 SQL 查询:URL 参数拼接 WHERE 条件”

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

📌 说明:EP166 的 transcript 在「Let's start with get args.」这句话戛然而止,EP169 开头是「Now that we have sort of the big picture spelled out, let's just start building these methods one by one. So let's start with get args.」——两者明显是同一段实操内容被切成两个文件,中间又插了 EP167、EP168 两条不含代码操作步骤的「Quick Note」勘误/预告。这四篇合并成一篇笔记,按 166→167→168→169 的顺序把内容整合起来。


/pet-adoption 页面这一讲要能响应 URL 参数(比如 ?species=dog&favcolor=green&minweight=20&maxweight=50),动态拼出对应的 SQL WHERE 条件,而不是像 EP165 那样查询条件写死。把查询相关的所有逻辑从模板文件里搬到新建的 GetPets 类里(inc/GetPets.php),让模板文件只剩纯 HTML;这个类负责:从 $_GET 里提取并清理出关心的几个参数(getArgs())、把这些参数值单独整理成一份配合 prepare() 占位符用的数组(createPlaceholders())、把参数名对应拼成 SQL 的 WHERE 子句文本(createWhereText() + specificQuery(),处理 minweight/maxweight/minyear/maxyear 这几个不能直接对应数据库列名的特殊情况)、以及额外发一次 COUNT(*) 查询取得符合条件的总数(用来显示「共找到 X 条结果」)。

📌 EP167 勘误(PHP 数组):视频里把 args(参数值)和 placeholders(专门给 prepare() 占位符用的值)分成两个独立属性来处理,是作者当时误以为 $wpdb->prepare() 需要「数字索引数组」而不能直接吃「关联数组」——这个假设是错的,prepare() 的第二个参数直接用 $this->args(关联数组)完全没问题,PHP 本身也不区分数字索引数组和关联数组(都是同一种 array 类型)。可以照视频步骤跟着做、不会报错,但 placeholders 这个属性技术上是多余的,完全可以省掉、直接把 $this->args 传给 prepare()

📌 EP168 勘误(PHP Warning):直接用 $_GET['favcolor'] 这种写法,如果 URL 根本没带这个参数,会在较新的 PHP 版本里触发「访问不存在的数组键」警告——建议在 getArgs() 里给每个参数都套一层 isset() 判断,只有 URL 真的带了这个参数才纳入数组,取代视频里手动拼 array_filter() 过滤空值的做法。这版写法可以直接用,能跳过视频里前 3 分钟手把手重新写一遍旧版 getArgs() 的过程。


涉及文件

  • wp-content/plugins/new-database-table/inc/template-pets.php (修改,只保留 HTML 和最终展示用的变量)
  • wp-content/plugins/new-database-table/inc/GetPets.php (新建,查询构建类)

代码实现

inc/GetPets.php(新建,完整文件)

<?php 

class GetPets {
  function __construct() {
    global $wpdb;
    $tablename = $wpdb->prefix . 'pets';

    $this->args = $this->getArgs();
    $this->placeholders = $this->createPlaceholders();

    $query = "SELECT * FROM $tablename ";
    $countQuery = "SELECT COUNT(*) FROM $tablename ";
    $query .= $this->createWhereText();
    $countQuery .= $this->createWhereText();
    $query .= " LIMIT 100";

    $this->count = $wpdb->get_var($wpdb->prepare($countQuery, $this->placeholders));
    $this->pets = $wpdb->get_results($wpdb->prepare($query, $this->placeholders));
  }

  function getArgs() {
    $temp = array(
      'favcolor' => sanitize_text_field($_GET['favcolor']),
      'species' => sanitize_text_field($_GET['species']),
      'minyear' => sanitize_text_field($_GET['minyear']),
      'maxyear' => sanitize_text_field($_GET['maxyear']),
      'minweight' => sanitize_text_field($_GET['minweight']),
      'maxweight' => sanitize_text_field($_GET['maxweight']),
      'favhobby' => sanitize_text_field($_GET['favhobby']),
      'favfood' => sanitize_text_field($_GET['favfood']),
    );

    return array_filter($temp, function($x) {
      return $x;
    });
  }

  function createPlaceholders() {
    return array_map(function($x) {
      return $x;
    }, $this->args);
  }

  function createWhereText() {
    $whereQuery = "";

    if (count($this->args)) {
      $whereQuery = "WHERE ";
    }

    $currentPosition = 0;
    foreach($this->args as $index => $item) {
      $whereQuery .= $this->specificQuery($index);
      if ($currentPosition != count($this->args) - 1) {
        $whereQuery .= " AND ";
      }
      $currentPosition++;
    }

    return $whereQuery;
  }

  function specificQuery($index) {
    switch ($index) {
      case "minweight":
        return "petweight >= %d";
      case "maxweight":
        return "petweight <= %d";
      case "minyear":
        return "birthyear >= %d";
      case "maxyear":
        return "birthyear <= %d";
      default:
        return $index . " = %s";
    }
  }

}

推荐做法(EP168 勘误版本):getArgs() 改用 isset() 逐个判断,避免 PHP Warning

function getArgs() {
  $temp = [];

  if (isset($_GET['favcolor'])) $temp['favcolor'] = sanitize_text_field($_GET['favcolor']);
  if (isset($_GET['species'])) $temp['species'] = sanitize_text_field($_GET['species']);
  if (isset($_GET['minyear'])) $temp['minyear'] = sanitize_text_field($_GET['minyear']);
  if (isset($_GET['maxyear'])) $temp['maxyear'] = sanitize_text_field($_GET['maxyear']);
  if (isset($_GET['minweight'])) $temp['minweight'] = sanitize_text_field($_GET['minweight']);
  if (isset($_GET['maxweight'])) $temp['maxweight'] = sanitize_text_field($_GET['maxweight']);
  if (isset($_GET['favhobby'])) $temp['favhobby'] = sanitize_text_field($_GET['favhobby']);
  if (isset($_GET['favfood'])) $temp['favfood'] = sanitize_text_field($_GET['favfood']);

  return $temp;
}

inc/template-pets.php:模板只剩纯展示逻辑

<?php

require_once plugin_dir_path(__FILE__) . 'GetPets.php';
$getPets = new GetPets();

get_header(); ?>

<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">Pet Adoption</h1>
    <div class="page-banner__intro">
      <p>Providing forever homes one search at a time.</p>
    </div>
  </div>  
</div>

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

  <p>This page took <strong><?php echo timer_stop();?></strong> seconds to prepare. Found <strong><?php echo $getPets->count; ?></strong> results (showing the first <?php echo count($getPets->pets) ?>).</p>
  
  <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>
    </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>
        </tr>
      <?php }
    ?>
  </table>
  
</div>

<?php get_footer(); ?>

关键改动点:

  • 把查询逻辑从模板文件搬到独立的 GetPets:模板文件从此只关心「怎么展示数据」,GetPets 类只关心「怎么查数据」——延续这门课一贯的组织习惯(大段逻辑拆到独立文件);用类而不是一堆散落的函数,是为了让类内部可以随便取简单的方法名(getArgs/createWhereText 等),不用担心跟别的插件/主题的全局函数重名
  • GetPets 的构造函数就是整个类的「调度中心」:一进来就依次调用 getArgs()(拿到清理过的参数)、createPlaceholders()(整理出 prepare() 要用的值数组)、拼出完整的主查询和计数查询字符串、最后各自跑一次 prepare() + get_var()/get_results(),构造函数跑完,pets(结果数组)和 count(总条数)两个属性就都准备好了
  • getArgs()——只提取「认识」的几个参数并清理:不是把 $_GET 里的所有内容照单全收(访客可能在 URL 里塞入任何乱七八糟的参数名,那些不关心),而是明确列出这 8 个关心的参数名,逐一用 sanitize_text_field() 清理过再收进数组——sanitize_text_field() 是 WordPress 提供的通用文本清理函数,哪怕 $wpdb->prepare() 已经做了 SQL 层面的转义,多一层通用的输入清理是更保守安全的习惯
  • array_filter($temp, function($x) { return $x; }):过滤掉值为空(''/null/0/false 等「假值」)的项,只保留真正有值的参数——如果 URL 只带了 species 一个参数,其余 7 个经过这个过滤后就不会出现在最终数组里
  • createPlaceholders():目前只是把 args 数组透过 array_map 原样映射一遍、只保留值本身——如前面 📌 提到的,这一步理论上是多余的(prepare() 可以直接吃关联数组),保留下来只是跟着视频原始步骤走
  • createWhereText()——动态拼 WHERE 子句:如果 args 一个参数都没有(完全没有筛选条件),干脆不拼 WHERE 这个词;否则遍历 args 数组的每个键(用 foreach ($this->args as $index => $item),这里只关心键名 $index,不关心值,因为值已经交给 placeholders 处理),对每个键调用 specificQuery($index) 拿到这个键对应的 SQL 片段,用 $currentPosition 手动计数,只有「不是最后一项」时才在片段之间加上 AND 连接词
  • specificQuery($index)——用 switch 处理「参数名跟数据库列名对不上」的特殊情况minweight/maxweight 都要映射到数据库的 petweight 列(用 >=/<= 比较),minyear/maxyear 都要映射到 birthyear 列;剩下的参数名(species/favcolor/favhobby/favfood)本身就跟数据库列名一致,走 switchdefault 分支,直接拼成「列名 = 占位符」
  • 占位符类型的选择species = %s(字符串),petweight >= %d(数字)——跟 EP165 学到的 %s/%d 用法一致,prepare() 会根据占位符类型对相应的值做适当处理
  • 额外发一次 COUNT(*) 查询:不是想办法在一次查询里既拿结果又拿总数(作者提到看过 Stack Overflow 上的讨论,认为分成两次独立查询反而性能更好),而是单独拼一条 SELECT COUNT(*) FROM 表名 WHERE ...(复用同一个 createWhereText(),但不加 LIMIT),用 $wpdb->get_var() 执行——这个方法专门用来获取「只有一行一列」的单一结果值(这里就是符合条件的总条数),不需要像 get_results() 那样处理一整个结果集
  • 主查询才加 LIMIT 100:计数查询不能加这个限制,否则永远最多只能数到 100,没法反映真实的总条数
  • 模板文件里 found X results (showing the first Y)X 直接输出 $getPets->count(总条数),Ycount($getPets->pets)(当前这一页真正返回了几条,避免总数不足 100 时依然显示写死的「100」)

Hook / Function 速查

名称类型用途
sanitize_text_field($字符串)WP 内建 function清理用户输入的文本字段,去除多余空白、非法字符等
isset($变量)PHP 内建语法判断变量/数组键是否存在且不为 null,避免访问不存在的键触发警告
array_filter($数组, $回调)PHP 内建 function按回调函数的返回值过滤数组,只保留返回真值的项
array_map($回调, $数组)PHP 内建 function对数组每一项应用回调函数,返回处理后的新数组
switch / case / defaultPHP 内建语法按一个值匹配多个分支,逐一处理不同情况
$wpdb->get_var($SQL)$wpdb 方法执行查询并只取返回结果的第一行第一列(单一值),常用于 COUNT(*) 这类查询

常见坑

  • 以为 $wpdb->prepare() 的第二个参数必须是数字索引数组,特意为此多做一层转换——PHP 不区分索引数组和关联数组,直接传关联数组也完全没问题(EP167 勘误)
  • 直接用 $_GET['参数名'] 而不先判断 isset()——如果 URL 没带这个参数,较新版本的 PHP 会抛出访问不存在数组键的警告(EP168 勘误)
  • WHERE 子句时忘记处理「最后一项后面不该有 AND」——会生成语法错误的 SQL(WHERE a=1 AND b=2 AND 这种悬空的 AND 结尾)
  • 计数查询也加上了 LIMIT 100——会导致不管实际有多少条符合条件的记录,「找到 X 条结果」永远最多显示 100
  • 忘记给 minweight/maxweight/minyear/maxyear 这几个参数名做特殊映射,直接当成数据库列名去拼 SQL——数据库根本没有这几个列,查询会报错

[截图:访问带 URL 参数(如 ?species=dog&favcolor=green)的 /pet-adoption 页面,表格只显示符合筛选条件的宠物,顶部"Found X results"数字跟着变化]


延伸 / 后续讲座会用到

下一讲要在页面上加一个只有管理员能看到的表单,实现「填写宠物信息 → 提交 → 真正写入数据库」和「删除某一行」的功能。


Sources

Udemy:

  • Become a WordPress Developer: Unlocking Power With Code — Section 27, EP166, EP167, EP168, EP169