feat(core): add PDO database wrapper and migration runner

This commit is contained in:
dualfroz committed 2026-08-23 16:22:54 +02:00
1 parent e4f8897a65
commit 32995b714b
2 files changed
+168

No files matched your search

+114
View File
@@ -0,0 +1,114 @@
<?php
namespace Bazaar\Core;
use PDO;
use PDOStatement;
final class Database
{
private static ?Database $instance = null;
private function __construct(private readonly PDO $pdo)
{
}
public static function instance(): Database
{
if (self::$instance === null) {
self::$instance = new self(self::connect());
}
return self::$instance;
}
public static function swap(Database $database): void
{
self::$instance = $database;
}
public static function fromPdo(PDO $pdo): Database
{
return new self($pdo);
}
private static function connect(): PDO
{
$driver = (string) Config::get('DB_DRIVER', 'sqlite');
$options = [
PDO::ATTR_ERRMODE => 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;
}
}
}
+54
View File
@@ -0,0 +1,54 @@
<?php
namespace Bazaar\Core;
final class Migrator
{
public function __construct(
private readonly Database $db,
private readonly string $migrationsPath
) {
}
public function run(): array
{
$this->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;
}
}