Builds the /api Foundation into the base (per api/.memory/foundation-plan.md):
- app/Services/Installer.php DB test + dump.sql import + crypto keys + /api config + lock
- app/Services/Migrator.php versioned db/migrations/*.sql runner (+ migrations table)
- commands/InstallCommand.php (app:install), commands/MigrateCommand.php (db:migrate)
- app/Controllers/{JsonController,PublicController,ApiController} envelope + two-tier auth
- public/controllers/{health,admin}.php GET /api/health (public), /api/admin/ping (bearer)
- db/migrations/001_*.sql smoke migration
- install/controllers/index.php web wizard now delegates to Installer (path bugs fixed, lock)
- console + composer.json register commands / add commands to classmap
- app/src/pages/api-health-test.astro browser round-trip proof page
Verified (no DB): composer install OK; php console lists app:install + db:migrate;
PSR-4 classes autoload; CLI fails gracefully (validation, bad DB, missing config) with
no artifacts left; app builds 14 pages. Live DB + HTTP round-trip pending a served instance.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SYHWLHihq3v9nxNwoPCKSn
57 lines
1.7 KiB
PHP
57 lines
1.7 KiB
PHP
<?php
|
|
namespace App\Services;
|
|
|
|
use PDO;
|
|
|
|
/** Applies ordered *.sql files from a migrations dir, tracked in a `migrations` table. */
|
|
class Migrator
|
|
{
|
|
private PDO $pdo;
|
|
private string $dir;
|
|
|
|
public function __construct(PDO $pdo, string $dir)
|
|
{
|
|
$this->pdo = $pdo;
|
|
$this->dir = $dir;
|
|
}
|
|
|
|
public function ensureTable(): void
|
|
{
|
|
$this->pdo->exec(
|
|
"CREATE TABLE IF NOT EXISTS `migrations` (
|
|
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
|
`filename` VARCHAR(255) NOT NULL UNIQUE,
|
|
`applied_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"
|
|
);
|
|
}
|
|
|
|
/** @return string[] filenames already applied */
|
|
public function applied(): array
|
|
{
|
|
return $this->pdo->query("SELECT filename FROM `migrations`")->fetchAll(PDO::FETCH_COLUMN) ?: [];
|
|
}
|
|
|
|
/** @return string[] absolute paths of pending migrations, in order */
|
|
public function pending(): array
|
|
{
|
|
$all = glob($this->dir . '/*.sql') ?: [];
|
|
sort($all);
|
|
$applied = $this->applied();
|
|
return array_values(array_filter($all, fn($p) => !in_array(basename($p), $applied, true)));
|
|
}
|
|
|
|
/** @return string[] filenames applied this run */
|
|
public function migrate(): array
|
|
{
|
|
$this->ensureTable();
|
|
$done = [];
|
|
foreach ($this->pending() as $path) {
|
|
$this->pdo->exec((string) file_get_contents($path));
|
|
$stmt = $this->pdo->prepare("INSERT IGNORE INTO `migrations` (filename) VALUES (?)");
|
|
$stmt->execute([basename($path)]);
|
|
$done[] = basename($path);
|
|
}
|
|
return $done;
|
|
}
|
|
}
|