Add one-time DB setup utility for migration scaffolding and lock

This commit is contained in:
DevSoftTech Server 2026-02-28 04:42:58 +05:30
parent 061d9b666d
commit a10908e227
3 changed files with 295 additions and 0 deletions

View File

@ -223,6 +223,10 @@ $routes->post('Payment_Medical/change_user', 'Payment_Medical::change_user');
service('auth')->routes($routes);
$routes->get('setup/db-tools', 'Setup::dbTools');
$routes->post('setup/db-tools/generate', 'Setup::generateMigrations');
$routes->post('setup/db-tools/complete', 'Setup::complete');
$routes->group('billing', function($routes) {
$routes->get('ipd', 'Billing\\Ipd::index');

219
app/Controllers/Setup.php Normal file
View File

@ -0,0 +1,219 @@
<?php
namespace App\Controllers;
use App\Controllers\BaseController;
class Setup extends BaseController
{
private function lockFilePath(): string
{
return WRITEPATH . 'setup_db_complete.lock';
}
private function isSetupLocked(): bool
{
return is_file($this->lockFilePath());
}
private function ensureSetupAccess()
{
if ($this->isSetupLocked()) {
return $this->response->setStatusCode(404)->setBody('Setup is already completed and locked.');
}
if (!function_exists('auth')) {
return null;
}
$user = auth()->user();
if (!$user) {
return $this->response->setStatusCode(403)->setBody('Please login to access setup tools.');
}
$allowed = false;
if (method_exists($user, 'can') && $user->can('billing.opd.edit')) {
$allowed = true;
}
if (!$allowed && method_exists($user, 'inGroup') && $user->inGroup('admin', 'superadmin', 'developer')) {
$allowed = true;
}
if (!$allowed) {
return $this->response->setStatusCode(403)->setBody('Access denied.');
}
return null;
}
public function dbTools()
{
if ($deny = $this->ensureSetupAccess()) {
return $deny;
}
$allTables = $this->db->listTables();
$scripted = $this->detectScriptedTables();
$tableRows = [];
foreach ($allTables as $tableName) {
$isScripted = isset($scripted[strtolower((string) $tableName)]);
$tableRows[] = [
'name' => (string) $tableName,
'scripted' => $isScripted,
];
}
usort($tableRows, static function (array $a, array $b): int {
if ($a['scripted'] === $b['scripted']) {
return strcmp($a['name'], $b['name']);
}
return $a['scripted'] ? 1 : -1;
});
$flash = session('setup_msg');
return view('setup/db_setup', [
'tables' => $tableRows,
'msg' => $flash,
'lock_file' => $this->lockFilePath(),
]);
}
public function generateMigrations()
{
if ($deny = $this->ensureSetupAccess()) {
return $deny;
}
$selected = $this->request->getPost('tables');
if (!is_array($selected)) {
$selected = [];
}
$selected = array_values(array_unique(array_map(static function ($value): string {
$name = strtolower(trim((string) $value));
return preg_replace('/[^a-z0-9_]/', '', $name) ?? '';
}, $selected)));
$selected = array_values(array_filter($selected));
if (empty($selected)) {
session()->setFlashdata('setup_msg', 'Please select at least one table.');
return redirect()->to(base_url('setup/db-tools'));
}
$migrationDir = APPPATH . 'Database/Migrations';
if (!is_dir($migrationDir) && !mkdir($migrationDir, 0777, true) && !is_dir($migrationDir)) {
session()->setFlashdata('setup_msg', 'Unable to create migration directory.');
return redirect()->to(base_url('setup/db-tools'));
}
$createdFiles = [];
$ts = date('Y-m-d-His');
$counter = 0;
foreach ($selected as $tableName) {
$showQuery = $this->db->query('SHOW CREATE TABLE `' . $tableName . '`');
$showRow = $showQuery->getRowArray();
if (empty($showRow)) {
continue;
}
$createSql = '';
foreach ($showRow as $key => $value) {
if (is_string($key) && stripos($key, 'Create Table') !== false) {
$createSql = (string) $value;
break;
}
}
if ($createSql === '') {
$vals = array_values($showRow);
$createSql = (string) ($vals[1] ?? '');
}
if ($createSql === '') {
continue;
}
$createSql = preg_replace('/^CREATE\s+TABLE\s+/i', 'CREATE TABLE IF NOT EXISTS ', trim($createSql), 1) ?? $createSql;
if (!str_ends_with(trim($createSql), ';')) {
$createSql .= ';';
}
$className = 'CreateExistingTable' . $this->studly($tableName) . date('His') . str_pad((string) $counter, 2, '0', STR_PAD_LEFT);
$fileStamp = $ts . str_pad((string) $counter, 2, '0', STR_PAD_LEFT);
$fileName = $fileStamp . '_' . $className . '.php';
$counter++;
$php = "<?php\n\nnamespace App\\Database\\Migrations;\n\nuse CodeIgniter\\Database\\Migration;\n\nclass {$className} extends Migration\n{\n public function up()\n {\n \$this->db->query(<<<'SQL'\n{$createSql}\nSQL\n );\n }\n\n public function down()\n {\n \$this->forge->dropTable('{$tableName}', true);\n }\n}\n";
$path = $migrationDir . DIRECTORY_SEPARATOR . $fileName;
if (@file_put_contents($path, $php) !== false) {
$createdFiles[] = $fileName;
}
}
if (empty($createdFiles)) {
session()->setFlashdata('setup_msg', 'No migration files generated.');
} else {
session()->setFlashdata('setup_msg', 'Generated ' . count($createdFiles) . ' migration file(s).');
}
return redirect()->to(base_url('setup/db-tools'));
}
public function complete()
{
if ($deny = $this->ensureSetupAccess()) {
return $deny;
}
$lockFile = $this->lockFilePath();
$userName = 'unknown';
if (function_exists('auth') && auth()->user()) {
$user = auth()->user();
$userName = (string) ($user->username ?? $user->email ?? 'unknown');
}
$content = "Setup completed at: " . date('Y-m-d H:i:s') . "\nBy: " . $userName . "\n";
@file_put_contents($lockFile, $content);
return $this->response->setBody('Setup locked successfully. You can now remove setup routes/page if required.');
}
/**
* @return array<string, bool>
*/
private function detectScriptedTables(): array
{
$result = [];
$files = glob(APPPATH . 'Database/Migrations/*.php') ?: [];
foreach ($files as $filePath) {
$content = @file_get_contents($filePath);
if (!is_string($content) || $content === '') {
continue;
}
if (preg_match_all('/createTable\(\s*[\'\"]([a-zA-Z0-9_]+)[\'\"]/i', $content, $m1) === 1 || !empty($m1[1])) {
foreach (($m1[1] ?? []) as $table) {
$result[strtolower((string) $table)] = true;
}
}
if (preg_match_all('/CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?`?([a-zA-Z0-9_]+)`?/i', $content, $m2) === 1 || !empty($m2[1])) {
foreach (($m2[1] ?? []) as $table) {
$result[strtolower((string) $table)] = true;
}
}
}
return $result;
}
private function studly(string $value): string
{
$value = preg_replace('/[^a-zA-Z0-9]+/', ' ', $value) ?? $value;
$value = ucwords(strtolower(trim($value)));
return str_replace(' ', '', $value);
}
}

View File

@ -0,0 +1,72 @@
<section class="content-header">
<h1>One-Time DB Setup</h1>
</section>
<section class="content">
<div class="box box-primary">
<div class="box-header with-border">
<h3 class="box-title">Existing Database Tables</h3>
</div>
<div class="box-body">
<?php if (!empty($msg ?? '')) : ?>
<div class="alert alert-info"><?= esc($msg) ?></div>
<?php endif; ?>
<p class="text-muted">
Select unscripted tables to generate migration files. This does not modify existing data.
Generated files are placed in <strong>app/Database/Migrations</strong>.
</p>
<form method="post" action="<?= base_url('setup/db-tools/generate') ?>">
<?= csrf_field() ?>
<div class="table-responsive" style="max-height:520px; overflow:auto; border:1px solid #eee;">
<table class="table table-striped table-bordered">
<thead>
<tr>
<th style="width:60px;">Pick</th>
<th>Table Name</th>
<th style="width:160px;">Migration Script</th>
</tr>
</thead>
<tbody>
<?php if (empty($tables ?? [])) : ?>
<tr><td colspan="3" class="text-center text-muted">No tables found.</td></tr>
<?php else : ?>
<?php foreach (($tables ?? []) as $row) : ?>
<tr>
<td class="text-center">
<?php if (!$row['scripted']) : ?>
<input type="checkbox" name="tables[]" value="<?= esc($row['name']) ?>">
<?php endif; ?>
</td>
<td><?= esc($row['name']) ?></td>
<td>
<?php if ($row['scripted']) : ?>
<span class="label label-success">Scripted</span>
<?php else : ?>
<span class="label label-warning">Missing</span>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
<div style="margin-top:12px;">
<button type="submit" class="btn btn-primary">Generate Migration For Selected</button>
<a class="btn btn-default" href="<?= base_url('setup/db-tools') ?>">Refresh</a>
</div>
</form>
<hr>
<form method="post" action="<?= base_url('setup/db-tools/complete') ?>" onsubmit="return confirm('This will lock setup page. Continue?');">
<?= csrf_field() ?>
<button type="submit" class="btn btn-danger">Complete Setup and Lock Page</button>
<p class="text-muted" style="margin-top:8px;">Lock file: <?= esc($lock_file ?? '') ?></p>
</form>
</div>
</div>
</section>