在早期 PHP 開發中,常需快速將任意表的查詢結果轉為下拉選單、列表頁等場景。本文提供一個無需 ORM、零依賴的通用查詢函數,支援動態表名、字段、條件、排序,並深度解析 SQL 注入防護、資源管理、現代 PDO 重寫方案,適合維護舊項目或理解底層原理。
<?php
/**
* 通用查詢生成下拉選單 (舊版 mysql_* 函數,已棄用)
* @param string $table 表名
* @param string $name select name 屬性
* @param string $field 顯示字段
* @param string $where WHERE 條件 (含 WHERE 關鍵字)
* @param string $textID select id 屬性
*/
function listMyTableRows($table, $name, $field, $where, $textID) {
// 1. 連接數據庫 (應提取為獨立函數/配置)
$conn = mysql_connect('localhost', 'user', 'pass');
mysql_select_db('dbname', $conn);
mysql_query("SET NAMES utf8");
// 2. 構建 SQL (極高風險:直接拼接 $table, $field, $where)
$sql = "SELECT * FROM `$table` $where ORDER BY `$field`";
$res = mysql_query($sql);
// 3. 輸出 HTML (邏輯與視圖耦合)
echo "<select name='$name' id='$textID'>";
echo "<option value=''>請選擇</option>";
while ($row = mysql_fetch_assoc($res)) {
echo "<option value='{$row['id']}'>{$row[$field]}</option>";
}
echo "</select>";
mysql_free_result($res);
mysql_close($conn);
}
| 問題 | 風險 | 修正方向 |
|---|---|---|
| 使用 `mysql_*` 函數 | PHP 7.0 移除,不安全,無預處理 | 遷移至 PDO / mysqli |
| SQL 直接拼接變量 | 極高 SQL 注入風險 ($table, $field, $where 均可控) | 白名單驗證表名/字段;條件參數化 |
| 連接資源重複創建/銷毀 | 性能極差,無連接池 | 單例/連接池/依賴注入 |
| HTML 直接輸出 | 難以複用、測試、維護 | 返回數據結構,視圖層渲染 |
| 無異常處理 | 靜默失敗,難以排查 | 拋出異常/返回 Result 對象 |
<?php
class Database {
private static $pdo = null;
public static function getPDO(): PDO {
if (self::$pdo === null) {
$dsn = "mysql:host=localhost;dbname=test;charset=utf8mb4";
self::$pdo = new PDO($dsn, 'user', 'pass', [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false, // 真正預處理
]);
}
return self::$pdo;
}
}
/**
* 通用查詢構建器 (簡易版)
*/
class QueryBuilder {
private $pdo;
private $table;
private $columns = ['*'];
private $where = [];
private $params = [];
private $orderBy = '';
private $limit = null;
private $offset = null;
public function __construct(PDO $pdo = null) {
$this->pdo = $pdo ?? Database::getPDO();
}
public function table(string $table): self {
// 表名白名單驗證 (防注入)
$allowed = ['users', 'categories', 'products', 'articles']; // 根據實際項目配置
if (!in_array($table, $allowed, true)) {
throw new InvalidArgumentException("Table '$table' not allowed");
}
$this->table = $table;
return $this;
}
public function select(array $columns): self {
// 字段白名單驗證 (簡化:僅允許字母數字下劃線)
foreach ($columns as $col) {
if (!preg_match('/^[a-zA-Z0-9_]+$/', $col)) {
throw new InvalidArgumentException("Invalid column: $col");
}
}
$this->columns = $columns;
return $this;
}
public function where(string $column, $operator, $value): self {
// 簡化:支持 =, !=, >, <, >=, <=, LIKE, IN, NOT IN
$allowedOps = ['=', '!=', '>', '<', '>=', '<=', 'LIKE', 'IN', 'NOT IN'];
if (!in_array(strtoupper($operator), $allowedOps, true)) {
throw new InvalidArgumentException("Operator '$operator' not allowed");
}
if (!preg_match('/^[a-zA-Z0-9_]+$/', $column)) {
throw new InvalidArgumentException("Invalid column: $column");
}
$this->where[] = "`$column` $operator ?";
$this->params[] = $value;
return $this;
}
public function orderBy(string $column, string $direction = 'ASC'): self {
if (!preg_match('/^[a-zA-Z0-9_]+$/', $column)) throw new InvalidArgumentException("Invalid column");
$direction = strtoupper($direction) === 'DESC' ? 'DESC' : 'ASC';
$this->orderBy = "ORDER BY `$column` $direction";
return $this;
}
public function limit(int $limit, int $offset = 0): self {
$this->limit = $limit;
$this->offset = $offset;
return $this;
}
public function get(): array {
$sql = "SELECT " . implode(', ', array_map(fn($c) => "`$c`", $this->columns)) .
" FROM `{$this->table}`";
if ($this->where) $sql .= " WHERE " . implode(' AND ', $this->where);
if ($this->orderBy) $sql .= " " . $this->orderBy;
if ($this->limit) $sql .= " LIMIT {$this->limit}" . ($this->offset ? " OFFSET {$this->offset}" : '');
$stmt = $this->pdo->prepare($sql);
$stmt->execute($this->params);
return $stmt->fetchAll();
}
public function getKeyValue(string $keyColumn, string $valueColumn): array {
$rows = $this->select([$keyColumn, $valueColumn])->get();
return array_column($rows, $valueColumn, $keyColumn);
}
}
// 使用示例
$options = (new QueryBuilder())
->table('categories')
->select(['id', 'name'])
->where('parent_id', '=', 0)
->where('status', '=', 1)
->orderBy('sort_order', 'ASC')
->getKeyValue('id', 'name');
// 視圖層渲染 (分離關注點)
echo "<select name='category_id' id='category_id'>";
echo "<option value=''>請選擇分類</option>";
foreach ($options as $id => $name) {
echo "<option value='$id'>" . htmlspecialchars($name) . "</option>";
}
echo "</select>";
若項目已使用框架,切勿重複造輪子:
function listMyTableRows($table, $name, $field, $where, $textID) {
// 解析舊 $where 字符串 (如 "WHERE status=1 AND type=2") -> 結構化條件
// 為簡化:要求調用方改用新 API,此函數僅作過渡拋出異常
trigger_error('listMyTableRows deprecated, use QueryBuilder instead', E_USER_DEPRECATED);
// ... 臨時實現或拋出異常
}
總結: 「通用查詢函數」看似便利,實則是「SQL 注入溫床」「邏輯視圖耦合」「難以維護」的三重陷阱。現代 PHP 開發應採用「查詢構建器 + 參數化預處理 + 白名單驗證 + 視圖分離」的分層架構。對於舊項目,制定遷移計劃、建立白名單、編寫適配器、逐步替換,是消除技術債務的唯一正道。