- Move 12 markdown files from root to docs/ subdirectories - Organize documentation by category: • docs/troubleshooting/ (1 file) - Technical troubleshooting guides • docs/deployment/ (4 files) - Deployment and security documentation • docs/guides/ (3 files) - Feature-specific guides • docs/planning/ (4 files) - Planning and improvement proposals Root directory cleanup: - Reduced from 16 to 4 markdown files in root - Only essential project files remain: • CLAUDE.md (AI instructions) • README.md (Main project readme) • CLEANUP_PLAN.md (Current cleanup plan) • SRC_STRUCTURE_IMPROVEMENTS.md (Structure improvements) This improves: ✅ Documentation discoverability ✅ Logical organization by purpose ✅ Clean root directory ✅ Better maintainability
140 lines
4.9 KiB
PHP
140 lines
4.9 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Framework\Database\Migration\ValueObjects;
|
|
|
|
use App\Framework\Exception\ErrorCode;
|
|
use App\Framework\Exception\FrameworkException;
|
|
|
|
/**
|
|
* Configuration for migration table structure and naming
|
|
*/
|
|
final readonly class MigrationTableConfig
|
|
{
|
|
public function __construct(
|
|
public string $tableName,
|
|
public string $versionColumn = 'version',
|
|
public string $descriptionColumn = 'description',
|
|
public string $executedAtColumn = 'executed_at',
|
|
public string $idColumn = 'id'
|
|
) {
|
|
if (empty($tableName)) {
|
|
throw FrameworkException::create(
|
|
ErrorCode::VAL_BUSINESS_RULE_VIOLATION,
|
|
'Migration table name cannot be empty'
|
|
)->withData(['table_name' => $tableName]);
|
|
}
|
|
|
|
$this->validateColumnName($versionColumn, 'version');
|
|
$this->validateColumnName($descriptionColumn, 'description');
|
|
$this->validateColumnName($executedAtColumn, 'executed_at');
|
|
$this->validateColumnName($idColumn, 'id');
|
|
}
|
|
|
|
public static function default(): self
|
|
{
|
|
return new self('migrations');
|
|
}
|
|
|
|
public static function withCustomTable(string $tableName): self
|
|
{
|
|
return new self($tableName);
|
|
}
|
|
|
|
/**
|
|
* Get the full INSERT SQL for recording a migration
|
|
*/
|
|
public function getInsertSql(): string
|
|
{
|
|
return "INSERT INTO {$this->tableName} ({$this->versionColumn}, {$this->descriptionColumn}, {$this->executedAtColumn}) VALUES (?, ?, ?)";
|
|
}
|
|
|
|
/**
|
|
* Get the SELECT SQL for fetching applied versions
|
|
*/
|
|
public function getVersionSelectSql(): string
|
|
{
|
|
return "SELECT {$this->versionColumn} FROM {$this->tableName} ORDER BY {$this->executedAtColumn}";
|
|
}
|
|
|
|
/**
|
|
* Get the DELETE SQL for removing a migration record
|
|
*/
|
|
public function getDeleteSql(): string
|
|
{
|
|
return "DELETE FROM {$this->tableName} WHERE {$this->versionColumn} = ?";
|
|
}
|
|
|
|
/**
|
|
* Get the CREATE TABLE SQL for the given database driver
|
|
*/
|
|
public function getCreateTableSql(string $driver): string
|
|
{
|
|
return match($driver) {
|
|
'mysql' => "CREATE TABLE IF NOT EXISTS {$this->tableName} (
|
|
{$this->idColumn} INT PRIMARY KEY AUTO_INCREMENT,
|
|
{$this->versionColumn} VARCHAR(20) NOT NULL UNIQUE COMMENT 'Format: YYYY_MM_DD_HHMMSS',
|
|
{$this->descriptionColumn} TEXT,
|
|
{$this->executedAtColumn} TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
INDEX idx_{$this->versionColumn} ({$this->versionColumn}),
|
|
INDEX idx_{$this->executedAtColumn} ({$this->executedAtColumn})
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4",
|
|
|
|
'pgsql' => "CREATE TABLE IF NOT EXISTS {$this->tableName} (
|
|
{$this->idColumn} SERIAL PRIMARY KEY,
|
|
{$this->versionColumn} VARCHAR(20) NOT NULL UNIQUE,
|
|
{$this->descriptionColumn} TEXT,
|
|
{$this->executedAtColumn} TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
)",
|
|
|
|
'sqlite' => "CREATE TABLE IF NOT EXISTS {$this->tableName} (
|
|
{$this->idColumn} INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
{$this->versionColumn} TEXT NOT NULL UNIQUE,
|
|
{$this->descriptionColumn} TEXT,
|
|
{$this->executedAtColumn} TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
)",
|
|
|
|
default => throw FrameworkException::create(
|
|
ErrorCode::VAL_UNSUPPORTED_OPERATION,
|
|
"Unsupported database driver for migrations"
|
|
)->withData(['driver' => $driver])
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Get PostgreSQL index creation SQL (separate from table creation)
|
|
*/
|
|
public function getPostgreSqlIndexSql(): array
|
|
{
|
|
return [
|
|
"CREATE INDEX IF NOT EXISTS idx_{$this->tableName}_{$this->versionColumn} ON {$this->tableName} ({$this->versionColumn})",
|
|
"CREATE INDEX IF NOT EXISTS idx_{$this->tableName}_{$this->executedAtColumn} ON {$this->tableName} ({$this->executedAtColumn})",
|
|
];
|
|
}
|
|
|
|
private function validateColumnName(string $columnName, string $context): void
|
|
{
|
|
if (empty($columnName)) {
|
|
throw FrameworkException::create(
|
|
ErrorCode::VAL_BUSINESS_RULE_VIOLATION,
|
|
"Migration {$context} column name cannot be empty"
|
|
)->withData([
|
|
'column_name' => $columnName,
|
|
'context' => $context,
|
|
]);
|
|
}
|
|
|
|
// Basic SQL injection prevention
|
|
if (! preg_match('/^[a-zA-Z_][a-zA-Z0-9_]*$/', $columnName)) {
|
|
throw FrameworkException::create(
|
|
ErrorCode::VAL_BUSINESS_RULE_VIOLATION,
|
|
"Invalid column name format for migration {$context}"
|
|
)->withData([
|
|
'column_name' => $columnName,
|
|
'context' => $context,
|
|
]);
|
|
}
|
|
}
|
|
}
|