|
@@ -0,0 +1,84 @@
|
|
|
|
|
+<?php
|
|
|
|
|
+
|
|
|
|
|
+declare(strict_types=1);
|
|
|
|
|
+
|
|
|
|
|
+namespace Glacier\Core;
|
|
|
|
|
+
|
|
|
|
|
+use PDO;
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * PDO 数据访问封装(单库多表,架构约定见 docs/architecture.md §3.5)。
|
|
|
|
|
+ * - 一律 prepared statement(防注入)
|
|
|
|
|
+ * - utf8mb4 + 会话时区 UTC+8(三层统一约定)
|
|
|
|
|
+ * - 异常模式(ERRMODE_EXCEPTION)
|
|
|
|
|
+ */
|
|
|
|
|
+final class Database
|
|
|
|
|
+{
|
|
|
|
|
+ private static ?PDO $pdo = null;
|
|
|
|
|
+
|
|
|
|
|
+ public static function pdo(): PDO
|
|
|
|
|
+ {
|
|
|
|
|
+ if (self::$pdo === null) {
|
|
|
|
|
+ $host = (string) Config::get('db.host', '127.0.0.1');
|
|
|
|
|
+ $port = (int) Config::get('db.port', 3306);
|
|
|
|
|
+ $name = (string) Config::get('db.name', 'glacier');
|
|
|
|
|
+ $charset = (string) Config::get('db.charset', 'utf8mb4');
|
|
|
|
|
+ $user = (string) Config::get('db.user', 'root');
|
|
|
|
|
+ $pass = (string) Config::get('db.pass', '');
|
|
|
|
|
+
|
|
|
|
|
+ $dsn = "mysql:host={$host};port={$port};dbname={$name};charset={$charset}";
|
|
|
|
|
+ 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,
|
|
|
|
|
+ PDO::ATTR_TIMEOUT => 5,
|
|
|
|
|
+ ]);
|
|
|
|
|
+ self::$pdo->exec("SET time_zone = '+08:00'");
|
|
|
|
|
+ }
|
|
|
|
|
+ return self::$pdo;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** 执行 prepared statement(唯一写库/查询路径)。 */
|
|
|
|
|
+ public static function run(string $sql, array $params = []): \PDOStatement
|
|
|
|
|
+ {
|
|
|
|
|
+ $stmt = self::pdo()->prepare($sql);
|
|
|
|
|
+ $stmt->execute($params);
|
|
|
|
|
+ return $stmt;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public static function fetchAll(string $sql, array $params = []): array
|
|
|
|
|
+ {
|
|
|
|
|
+ return self::run($sql, $params)->fetchAll();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public static function fetchOne(string $sql, array $params = []): ?array
|
|
|
|
|
+ {
|
|
|
|
|
+ $row = self::run($sql, $params)->fetch();
|
|
|
|
|
+ return $row === false ? null : $row;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public static function fetchValue(string $sql, array $params = []): mixed
|
|
|
|
|
+ {
|
|
|
|
|
+ $value = self::run($sql, $params)->fetchColumn();
|
|
|
|
|
+ return $value === false ? null : $value;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** 插入一行(表名白名单校验),返回自增 ID。 */
|
|
|
|
|
+ public static function insert(string $table, array $data): int
|
|
|
|
|
+ {
|
|
|
|
|
+ if (!preg_match('/^[a-z][a-z0-9_]*$/', $table)) {
|
|
|
|
|
+ throw new \InvalidArgumentException("非法表名: {$table}");
|
|
|
|
|
+ }
|
|
|
|
|
+ $q = chr(96); // 反引号字符,MySQL 标识符引用
|
|
|
|
|
+ $columns = array_keys($data);
|
|
|
|
|
+ $quoted = array_map(static fn(string $c) => $q . $c . $q, $columns);
|
|
|
|
|
+ $sql = sprintf(
|
|
|
|
|
+ 'INSERT INTO %s (%s) VALUES (%s)',
|
|
|
|
|
+ $table,
|
|
|
|
|
+ implode(', ', $quoted),
|
|
|
|
|
+ implode(', ', array_map(static fn(string $c) => ':' . $c, $columns))
|
|
|
|
|
+ );
|
|
|
|
|
+ self::run($sql, $data);
|
|
|
|
|
+ return (int) self::pdo()->lastInsertId();
|
|
|
|
|
+ }
|
|
|
|
|
+}
|