seedproject-web/api/app/Services/Installer.php

133 lines
4.9 KiB
PHP
Raw Permalink Normal View History

<?php
namespace App\Services;
use PDO;
use PDOException;
use RuntimeException;
/**
* Installer provisions config.php + imports the skeleton schema.
* Shared by the CLI (app:install) and the (locked) web wizard.
* Uses filesystem-relative paths, never $_SERVER['DOCUMENT_ROOT'].
*/
class Installer
{
private string $baseDir; // == api/
public function __construct()
{
// app/Services/Installer.php -> up two levels -> api/
$this->baseDir = dirname(__DIR__, 2);
}
public function lockFile(): string
{
return $this->baseDir . '/system/.installed';
}
public function isInstalled(): bool
{
return is_file($this->lockFile());
}
public function testConnection(array $db): bool
{
try {
new PDO(
"mysql:host={$db['host']};dbname={$db['name']};charset=utf8mb4",
$db['user'], $db['pass'],
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
);
return true;
} catch (PDOException $e) {
return false;
}
}
public function importSchema(array $db): void
{
$sqlFile = $this->baseDir . '/install/dump.sql';
if (!is_file($sqlFile)) {
throw new RuntimeException("Schema dump not found: {$sqlFile}");
}
$mysqli = new \mysqli($db['host'], $db['user'], $db['pass'], $db['name']);
if ($mysqli->connect_errno) {
throw new RuntimeException('DB connect failed: ' . $mysqli->connect_error);
}
if (!$mysqli->multi_query((string) file_get_contents($sqlFile))) {
throw new RuntimeException('Schema import failed: ' . $mysqli->error);
}
// Drain all result sets so the connection finishes cleanly.
while ($mysqli->more_results() && $mysqli->next_result()) { /* noop */ }
if ($mysqli->errno) {
throw new RuntimeException('Schema import error: ' . $mysqli->error);
}
$mysqli->close();
}
public function generateKey(int $bytes = 32): string
{
return bin2hex(random_bytes($bytes)); // hex only: safe inside single-quoted PHP
}
public function writeConfig(array $c): void
{
$e = fn($v) => addslashes((string) $v); // escape operator-provided values
$tpl = "<?php\n"
. "// Generated by SeedProject installer. DO NOT COMMIT (gitignored).\n"
. "define('URL', '" . $e($c['url']) . "');\n"
. "define('SITE_BASE', '/api');\n"
. "define('ASSETS', '/api/public/assets/');\n"
. "define('LIBS', 'core/');\n"
. "define('PROJECT_NAME', '" . $e($c['name']) . "');\n"
. "define('PROJECT_LOGO', '/api/public/assets/imgs/SeedProject.png');\n"
. "define('DEBUG', false);\n"
. "define('SECUREAPI', false);\n\n"
. "define('ADMIN_TOKEN', '" . $c['admin_token'] . "');\n"
. "define('ALLOWED_ORIGINS', '" . $e($c['url']) . "');\n\n"
. "define('EMAILUSER', 'ADDEMAILUSER');\n"
. "define('EMAILPASSWORD', 'ADDEMAILPASSWORD');\n"
. "define('EMAILHOST', 'ADDEMAILHOST');\n\n"
. "define('DB_TYPE', 'mysql');\n"
. "define('DB_HOST', '" . $e($c['db']['host']) . "');\n"
. "define('DB_NAME', '" . $e($c['db']['name']) . "');\n"
. "define('DB_USER', '" . $e($c['db']['user']) . "');\n"
. "define('DB_PASS', '" . $e($c['db']['pass']) . "');\n\n"
. "define('HASH_PASSWORD_KEY', '" . $c['hash_password_key'] . "');\n"
. "define('HASH_API_KEY', '" . $c['hash_api_key'] . "');\n"
. "define('TIMESTAMP', date('Y-m-d H:i:s'));\n"
. "date_default_timezone_set('America/New_York');\n\n"
. "\\Db::setConnectionInfo(DB_TYPE, DB_NAME, DB_USER, DB_PASS, DB_HOST);\n";
file_put_contents($this->baseDir . '/config.php', $tpl);
}
public function lock(): void
{
file_put_contents($this->lockFile(), date('c') . "\n");
}
/**
* Full install. $cfg = ['url','name','db'=>['host','name','user','pass']].
*/
public function run(array $cfg): void
{
if (!$this->testConnection($cfg['db'])) {
throw new RuntimeException('Database connection failed. Check credentials.');
}
$this->importSchema($cfg['db']);
$cfg['admin_token'] = $this->generateKey(24);
$cfg['hash_password_key'] = $this->generateKey(32);
$cfg['hash_api_key'] = $this->generateKey(32);
$this->writeConfig($cfg);
$this->lock();
// Apply migrations on top of the freshly imported baseline.
$pdo = new PDO(
"mysql:host={$cfg['db']['host']};dbname={$cfg['db']['name']};charset=utf8mb4",
$cfg['db']['user'], $cfg['db']['pass'],
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
);
(new Migrator($pdo, $this->baseDir . '/db/migrations'))->migrate();
}
}