Files
michaelschiemer/src/Framework/Database/Migration/ValueObjects/MigrationTableConfig.php
Michael Schiemer 5050c7d73a docs: consolidate documentation into organized structure
- 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
2025-10-05 11:05:04 +02:00

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,
]);
}
}
}