From 32995b714b7c461500d336f5f1f0f5e209a5abbb Mon Sep 17 00:00:00 2001 From: DualFroz Date: Sun, 23 Aug 2026 16:22:54 +0200 Subject: [PATCH] feat(core): add PDO database wrapper and migration runner --- src/Core/Database.php | 114 ++++++++++++++++++++++++++++++++++++++++++ src/Core/Migrator.php | 54 ++++++++++++++++++++ 2 files changed, 168 insertions(+) create mode 100644 src/Core/Database.php create mode 100644 src/Core/Migrator.php diff --git a/src/Core/Database.php b/src/Core/Database.php new file mode 100644 index 0000000..d5d7056 --- /dev/null +++ b/src/Core/Database.php @@ -0,0 +1,114 @@ + PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + PDO::ATTR_EMULATE_PREPARES => false, + ]; + + if ($driver === 'sqlite') { + $path = (string) Config::get('DB_DATABASE'); + if ($path !== ':memory:' && !str_starts_with($path, '/')) { + $path = basePath($path); + } + $pdo = new PDO('sqlite:' . $path, null, null, $options); + $pdo->exec('PRAGMA foreign_keys = ON'); + return $pdo; + } + + $dsn = sprintf( + '%s:host=%s;port=%s;dbname=%s;charset=utf8mb4', + $driver, + Config::get('DB_HOST'), + Config::get('DB_PORT'), + Config::get('DB_DATABASE') + ); + return new PDO($dsn, (string) Config::get('DB_USERNAME'), (string) Config::get('DB_PASSWORD'), $options); + } + + public function pdo(): PDO + { + return $this->pdo; + } + + private function run(string $sql, array $bindings): PDOStatement + { + $statement = $this->pdo->prepare($sql); + $statement->execute($bindings); + return $statement; + } + + public function select(string $sql, array $bindings = []): array + { + return $this->run($sql, $bindings)->fetchAll(); + } + + public function selectOne(string $sql, array $bindings = []): ?array + { + $row = $this->run($sql, $bindings)->fetch(); + return $row === false ? null : $row; + } + + public function scalar(string $sql, array $bindings = []): mixed + { + return $this->run($sql, $bindings)->fetchColumn(); + } + + public function statement(string $sql, array $bindings = []): int + { + return $this->run($sql, $bindings)->rowCount(); + } + + public function insert(string $sql, array $bindings = []): int + { + $this->run($sql, $bindings); + return (int) $this->pdo->lastInsertId(); + } + + public function transaction(callable $work): mixed + { + $this->pdo->beginTransaction(); + try { + $outcome = $work($this); + $this->pdo->commit(); + return $outcome; + } catch (\Throwable $error) { + $this->pdo->rollBack(); + throw $error; + } + } +} diff --git a/src/Core/Migrator.php b/src/Core/Migrator.php new file mode 100644 index 0000000..4577251 --- /dev/null +++ b/src/Core/Migrator.php @@ -0,0 +1,54 @@ +db->statement( + 'CREATE TABLE IF NOT EXISTS migrations ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL UNIQUE, + appliedAt TEXT NOT NULL + )' + ); + + $applied = array_column( + $this->db->select('SELECT name FROM migrations'), + 'name' + ); + + $ran = []; + foreach ($this->files() as $name => $path) { + if (in_array($name, $applied, true)) { + continue; + } + $this->db->pdo()->exec((string) file_get_contents($path)); + $this->db->insert( + 'INSERT INTO migrations (name, appliedAt) VALUES (?, ?)', + [$name, gmdate('c')] + ); + $ran[] = $name; + } + + return $ran; + } + + private function files(): array + { + $files = glob($this->migrationsPath . '/*.sql') ?: []; + sort($files); + $indexed = []; + foreach ($files as $file) { + $indexed[basename($file)] = $file; + } + return $indexed; + } +}