Database.php 2.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384
  1. <?php
  2. declare(strict_types=1);
  3. namespace Glacier\Core;
  4. use PDO;
  5. /**
  6. * PDO 数据访问封装(单库多表,架构约定见 docs/architecture.md §3.5)。
  7. * - 一律 prepared statement(防注入)
  8. * - utf8mb4 + 会话时区 UTC+8(三层统一约定)
  9. * - 异常模式(ERRMODE_EXCEPTION)
  10. */
  11. final class Database
  12. {
  13. private static ?PDO $pdo = null;
  14. public static function pdo(): PDO
  15. {
  16. if (self::$pdo === null) {
  17. $host = (string) Config::get('db.host', '127.0.0.1');
  18. $port = (int) Config::get('db.port', 3306);
  19. $name = (string) Config::get('db.name', 'glacier');
  20. $charset = (string) Config::get('db.charset', 'utf8mb4');
  21. $user = (string) Config::get('db.user', 'root');
  22. $pass = (string) Config::get('db.pass', '');
  23. $dsn = "mysql:host={$host};port={$port};dbname={$name};charset={$charset}";
  24. self::$pdo = new PDO($dsn, $user, $pass, [
  25. PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
  26. PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
  27. PDO::ATTR_EMULATE_PREPARES => false,
  28. PDO::ATTR_TIMEOUT => 5,
  29. ]);
  30. self::$pdo->exec("SET time_zone = '+08:00'");
  31. }
  32. return self::$pdo;
  33. }
  34. /** 执行 prepared statement(唯一写库/查询路径)。 */
  35. public static function run(string $sql, array $params = []): \PDOStatement
  36. {
  37. $stmt = self::pdo()->prepare($sql);
  38. $stmt->execute($params);
  39. return $stmt;
  40. }
  41. public static function fetchAll(string $sql, array $params = []): array
  42. {
  43. return self::run($sql, $params)->fetchAll();
  44. }
  45. public static function fetchOne(string $sql, array $params = []): ?array
  46. {
  47. $row = self::run($sql, $params)->fetch();
  48. return $row === false ? null : $row;
  49. }
  50. public static function fetchValue(string $sql, array $params = []): mixed
  51. {
  52. $value = self::run($sql, $params)->fetchColumn();
  53. return $value === false ? null : $value;
  54. }
  55. /** 插入一行(表名白名单校验),返回自增 ID。 */
  56. public static function insert(string $table, array $data): int
  57. {
  58. if (!preg_match('/^[a-z][a-z0-9_]*$/', $table)) {
  59. throw new \InvalidArgumentException("非法表名: {$table}");
  60. }
  61. $q = chr(96); // 反引号字符,MySQL 标识符引用
  62. $columns = array_keys($data);
  63. $quoted = array_map(static fn(string $c) => $q . $c . $q, $columns);
  64. $sql = sprintf(
  65. 'INSERT INTO %s (%s) VALUES (%s)',
  66. $table,
  67. implode(', ', $quoted),
  68. implode(', ', array_map(static fn(string $c) => ':' . $c, $columns))
  69. );
  70. self::run($sql, $data);
  71. return (int) self::pdo()->lastInsertId();
  72. }
  73. }