function list_hit($fields, $where, $order, $direction, $limit, $conn) {
if ($fields == "") {$fields = "hit_id,banner_id,hit_ip,hit_time";}
if ($order == "") {$direction = "";}
$sql = "select $fields from hit $where $order $direction $limit";
$res = mysql_query($sql, $conn);
$i = 0;
$list_hit[$i]["rows"] = mysql_num_rows($res);
$i++;
while ($rs = mysql_fetch_array($res)) {
$list_hit[$i]["hit_id"] = $rs["hit_id"];
$list_hit[$i]["banner_id"] = $rs["banner_id"];
$list_hit[$i]["hit_ip"] = $rs["hit_ip"];
$list_hit[$i]["hit_time"] = $rs["hit_time"];
$i++;
}
return $list_hit;
}
$where, $order, $direction, $limit 直接拼接,若來自使用者輸入極度危險。mysql_* 於 PHP 7.0 移除,需遷移至 PDO 或 MySQLi。["rows" => count],其餘為資料列,結構不統一。mysql_free_result()。hit 表特定欄位,缺乏通用性。<?php
declare(strict_types=1);
class SqlBuilder {
public static function buildSelect(
string $table,
array $columns = ['*'],
array $where = [],
array $orderBy = [],
?int $limit = null,
?int $offset = null
): array { // 回傳 [sql, params]
$sql = "SELECT " . implode(', ', array_map('self::quoteIdent', $columns)) . " FROM " . self::quoteIdent($table);
$params = [];
if ($where) {
$conditions = [];
foreach ($where as $col => $val) {
if (is_array($val)) { // IN 查詢
$placeholders = implode(',', array_fill(0, count($val), '?'));
$conditions[] = self::quoteIdent($col) . " IN ($placeholders)";
$params = array_merge($params, $val);
} elseif ($val === null) {
$conditions[] = self::quoteIdent($col) . " IS NULL";
} else {
$conditions[] = self::quoteIdent($col) . " = ?";
$params[] = $val;
}
}
$sql .= " WHERE " . implode(" AND ", $conditions);
}
if ($orderBy) {
$orders = [];
foreach ($orderBy as $col => $dir) {
$dir = strtoupper($dir) === 'DESC' ? 'DESC' : 'ASC';
$orders[] = self::quoteIdent($col) . " $dir";
}
$sql .= " ORDER BY " . implode(", ", $orders);
}
if ($limit !== null) {
$sql .= " LIMIT ?";
$params[] = $limit;
if ($offset !== null) {
$sql .= " OFFSET ?";
$params[] = $offset;
}
}
return [$sql, $params];
}
private static function quoteIdent(string $ident): string {
// 簡易識別符引用,實際建議用 PDO::quote 或資料庫特定函數
return '`' . str_replace('`', '``', $ident) . '`';
}
}
<?php
abstract class BaseRepository {
protected PDO $pdo;
protected string $table;
protected string $primaryKey = 'id';
public function __construct(PDO $pdo) {
$this->pdo = $pdo;
$this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$this->pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
}
public function findAll(array $criteria = [], array $orderBy = [], ?int $limit = null, ?int $offset = null): array {
[$sql, $params] = SqlBuilder::buildSelect($this->table, ['*'], $criteria, $orderBy, $limit, $offset);
$stmt = $this->pdo->prepare($sql);
$stmt->execute($params);
return $stmt->fetchAll();
}
public function count(array $criteria = []): int {
[$sql, $params] = SqlBuilder::buildSelect($this->table, ['COUNT(*) as cnt'], $criteria);
$stmt = $this->pdo->prepare($sql);
$stmt->execute($params);
return (int)$stmt->fetchColumn();
}
public function findById(mixed $id): ?array {
$stmt = $this->pdo->prepare("SELECT * FROM " . SqlBuilder::quoteIdent($this->table) . " WHERE " . SqlBuilder::quoteIdent($this->primaryKey) . " = ?");
$stmt->execute([$id]);
return $stmt->fetch() ?: null;
}
}
<?php
class HitRepository extends BaseRepository {
protected string $table = 'hit';
protected string $primaryKey = 'hit_id';
public function findByBannerId(int $bannerId, int $limit = 100, int $offset = 0): array {
return $this->findAll(['banner_id' => $bannerId], ['hit_time' => 'DESC'], $limit, $offset);
}
public function getStatsByDate(string $date): array {
$sql = "SELECT COUNT(*) as total, COUNT(DISTINCT hit_ip) as unique_ips FROM hit WHERE DATE(hit_time) = ?";
$stmt = $this->pdo->prepare($sql);
$stmt->execute([$date]);
return $stmt->fetch() ?: ['total' => 0, 'unique_ips' => 0];
}
}
<?php
$pdo = new PDO('mysql:host=localhost;dbname=test;charset=utf8mb4', 'user', 'pass');
$hitRepo = new HitRepository($pdo);
// 取得第 1 頁,每頁 15 筆,按時間倒序
$hits = $hitRepo->findAll([], ['hit_time' => 'DESC'], 15, 0);
$total = $hitRepo->count();
foreach ($hits as $hit) {
echo "ID: {$hit['hit_id']}, IP: {$hit['hit_ip']}, Time: {$hit['hit_time']}\n";
}
// 類型安全、可測試、防注入
// $hitRepo->findAll(["hit_ip = '1' OR 1=1" => null]) // 不會注入,因為值會被參數化
| 舊寫法 | 現代 PDO 寫法 | 優勢 |
|---|---|---|
mysql_query($sql) | $pdo->prepare($sql)->execute($params) | Prepared Statement 防注入、效能 (解析一次、多次執行) |
mysql_fetch_array() | $stmt->fetch(PDO::FETCH_ASSOC) | 可控制回傳格式 (關聯/數值/物件) |
mysql_num_rows() | 另行 COUNT(*) 查詢 | 避免緩衝全結果集,大資料集效能佳 |
| 字串拼接 SQL | SqlBuilder / Query Builder | 識別符引用、參數化、可組合 |
全域 $conn | DI 容器注入 PDO | 可測試、多資料庫支援、生命週期管理 |
$pdoRead / $pdoWrite。PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => false。mysql_* 時代已終結。擁抱 PDO + Prepared Statements + Repository Pattern,不僅修補安全漏洞,更奠定可測試、可維護、可擴展的資料存取架構基石。