首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 相關知識

PHP 通用數據庫查詢封裝:從任意表讀取數據到關聯數組的輕量級方案

     編輯:PHP 相關知識

在早期 PHP 開發中,常需快速將任意表的查詢結果轉為下拉選單、列表頁等場景。本文提供一個無需 ORM、零依賴的通用查詢函數,支援動態表名、字段、條件、排序,並深度解析 SQL 注入防護、資源管理、現代 PDO 重寫方案,適合維護舊項目或理解底層原理。

原始 mysql 版本 (僅供理解遺留代碼,嚴禁新項目使用)

<?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 對象

現代 PDO 版本:安全、可複用、可測試

<?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>";

進階:結合 Laravel Eloquent / Doctrine DBAL

若項目已使用框架,切勿重複造輪子:

  • Laravel: `Category::where('parent_id', 0)->pluck('name', 'id')->toArray()`
  • Symfony/Doctrine: `$repo->createQueryBuilder('c')->select('c.id, c.name')->where('c.parent = 0')->getQuery()->getResult()`
  • ThinkPHP: `Db::name('categories')->where('parent_id',0)->column('name,id')`

舊項目遷移策略

  1. 識別所有 `listMyTableRows` 調用點,統計參數模式
  2. 建立表名/字段白名單映射表,覆蓋所有調用場景
  3. 編寫 `QueryBuilder` 兼容舊函數簽名的適配器:
    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);
        // ... 臨時實現或拋出異常
    }
    
  4. 逐個替換調用點,測試驗證
  5. 移除舊函數、mysql_* 相關代碼

總結: 「通用查詢函數」看似便利,實則是「SQL 注入溫床」「邏輯視圖耦合」「難以維護」的三重陷阱。現代 PHP 開發應採用「查詢構建器 + 參數化預處理 + 白名單驗證 + 視圖分離」的分層架構。對於舊項目,制定遷移計劃、建立白名單、編寫適配器、逐步替換,是消除技術債務的唯一正道。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved