feat(core): add PDO database wrapper and migration runner
This commit is contained in:
1 parent
e4f8897a65
commit
32995b714b
2 files changed
+168
No files matched your search
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user