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
This commit is contained in:
2025-10-05 11:05:04 +02:00
parent 887847dde6
commit 5050c7d73a
36686 changed files with 196456 additions and 12398919 deletions

View File

@@ -5,252 +5,415 @@ declare(strict_types=1);
namespace App\Framework\Database\Migration;
use App\Framework\Database\ConnectionInterface;
use App\Framework\Database\Exception\DatabaseException;
use App\Framework\Database\Transaction;
use App\Framework\Database\Migration\Services\MigrationDatabaseManager;
use App\Framework\Database\Migration\Services\MigrationErrorAnalyzer;
use App\Framework\Database\Migration\Services\MigrationLogger;
use App\Framework\Database\Migration\Services\MigrationPerformanceTracker;
use App\Framework\Database\Migration\Services\MigrationValidator;
use App\Framework\Database\Migration\ValueObjects\MemoryThresholds;
use App\Framework\Database\Migration\ValueObjects\MigrationTableConfig;
use App\Framework\Database\Platform\DatabasePlatform;
use App\Framework\DateTime\Clock;
use App\Framework\Exception\ErrorCode;
use App\Framework\Exception\ExceptionContext;
use App\Framework\Exception\FrameworkException;
use App\Framework\Logging\Logger;
use App\Framework\Performance\MemoryMonitor;
use App\Framework\Performance\OperationTracker;
use App\Framework\Performance\PerformanceReporter;
use App\Framework\Performance\Repository\PerformanceMetricsRepository;
final readonly class MigrationRunner
{
/**
* @var MigrationDependencyGraph Dependency graph for migrations
*/
private MigrationDependencyGraph $dependencyGraph;
private MigrationDatabaseManager $databaseManager;
private MigrationPerformanceTracker $performanceTracker;
private MigrationLogger $migrationLogger;
private MigrationValidator $validator;
private MigrationErrorAnalyzer $errorAnalyzer;
public function __construct(
private ConnectionInterface $connection,
private string $migrationsTable = 'migrations'
private DatabasePlatform $platform,
private Clock $clock,
?MigrationTableConfig $tableConfig = null,
?Logger $logger = null,
?OperationTracker $operationTracker = null,
?MemoryMonitor $memoryMonitor = null,
?PerformanceReporter $performanceReporter = null,
?MemoryThresholds $memoryThresholds = null,
?PerformanceMetricsRepository $performanceMetricsRepository = null
) {
$this->ensureMigrationsTable();
$this->dependencyGraph = new MigrationDependencyGraph();
$effectiveTableConfig = $tableConfig ?? MigrationTableConfig::default();
// Initialize service classes
$this->databaseManager = new MigrationDatabaseManager(
$this->connection,
$this->platform,
$this->clock,
$effectiveTableConfig
);
$this->performanceTracker = new MigrationPerformanceTracker(
$operationTracker,
$memoryMonitor,
$performanceMetricsRepository,
$logger,
$memoryThresholds
);
$this->migrationLogger = new MigrationLogger($logger);
$this->validator = new MigrationValidator(
$this->connection,
$this->platform
);
$this->errorAnalyzer = new MigrationErrorAnalyzer();
}
/**
* Run migrations
*
* @param MigrationCollection $migrations The migrations to run
* @return array<string> List of executed migration versions
*/
public function migrate(MigrationCollection $migrations): array
public function migrate(MigrationCollection $migrations, bool $skipPreflightChecks = false): array
{
$this->databaseManager->ensureMigrationsTable();
$executedMigrations = [];
$appliedVersions = $this->getAppliedVersions();
$appliedVersions = MigrationVersionCollection::fromStrings($this->databaseManager->getAppliedVersions());
// Build the dependency graph
$this->dependencyGraph->buildGraph($migrations);
// Pre-flight checks
if (! $skipPreflightChecks) {
$this->runPreFlightChecks($migrations, $appliedVersions);
}
// Get migrations in the correct execution order based on dependencies
// Filter and order migrations
$pendingMigrations = $migrations->filterByNotApplied($appliedVersions);
if ($pendingMigrations->isEmpty()) {
$this->migrationLogger->logMigration('none', 'No pending migrations', 'Skipped');
return [];
}
$this->dependencyGraph->buildGraph($pendingMigrations);
$orderedMigrations = $this->dependencyGraph->getExecutionOrder();
$totalMigrations = $orderedMigrations->count();
// Start batch tracking
$batchOperationId = 'migration_batch_' . uniqid();
$this->performanceTracker->startBatchOperation($batchOperationId, $totalMigrations);
$currentPosition = 0;
foreach ($orderedMigrations as $migration) {
$version = $migration->getVersion()->toString();
if ($appliedVersions->containsString($version)) {
$currentPosition++;
if ($appliedVersions->contains($migration->getVersion())) {
continue;
}
// Check if all dependencies are applied
if ($migration instanceof DependentMigration) {
$missingDependencies = [];
foreach ($migration->getDependencies() as $dependencyVersion) {
$dependencyVersionString = $dependencyVersion->toString();
if (! $appliedVersions->containsString($dependencyVersionString) &&
! in_array($dependencyVersionString, $executedMigrations)) {
$missingDependencies[] = $dependencyVersionString;
}
}
if (! empty($missingDependencies)) {
throw new DatabaseException(
"Cannot apply migration {$version} because it depends on migrations that have not been applied: " .
implode(', ', $missingDependencies)
);
}
}
try {
Transaction::run($this->connection, function () use ($migration, $version) {
echo "Migrating: {$version} - {$migration->getDescription()}\n";
// Start individual migration tracking
$migrationOperationId = "migration_{$version}";
$this->performanceTracker->startMigrationOperation(
$migrationOperationId,
$version,
$migration->getDescription(),
$currentPosition,
$totalMigrations,
$batchOperationId
);
// Check memory thresholds before migration
$this->performanceTracker->checkMemoryThresholds($version, $currentPosition - 1, $totalMigrations);
// Execute migration in transaction
$this->connection->beginTransaction();
try {
$migration->up($this->connection);
$this->databaseManager->recordMigrationExecution($migration, $version);
$this->connection->execute(
"INSERT INTO {$this->migrationsTable} (version, description, executed_at) VALUES (?, ?, ?)",
[$version, $migration->getDescription(), date('Y-m-d H:i:s')]
);
});
if ($this->connection->inTransaction()) {
$this->connection->commit();
}
// Update operation with successful execution
$this->performanceTracker->updateBatchOperation($batchOperationId, [
'items_processed' => $currentPosition,
]);
} catch (\Throwable $e) {
if ($this->connection->inTransaction()) {
$this->connection->rollback();
}
throw $e;
}
$executedMigrations[] = $version;
echo "Migrated: {$version}\n";
// Complete individual migration tracking
$migrationSnapshot = $this->performanceTracker->completeMigrationOperation(
$migrationOperationId,
$version,
$migration->getDescription(),
$currentPosition,
$totalMigrations,
$batchOperationId
);
$this->migrationLogger->logMigration($version, $migration->getDescription(), 'Migrated');
$this->migrationLogger->logMigrationProgress($version, $currentPosition, $totalMigrations, 'completed', [
'execution_time' => $migrationSnapshot?->duration?->toSeconds(),
'memory_used' => $migrationSnapshot?->memoryDelta?->toHumanReadable(),
]);
// Post-migration memory check
$this->performanceTracker->logPostMigrationMemory($version, $migrationSnapshot);
} catch (\Throwable $e) {
throw new DatabaseException(
"Migration {$version} failed: {$e->getMessage()}",
0,
// Track failed individual migration
$failedSnapshot = $this->performanceTracker->failMigrationOperation(
$migrationOperationId,
$version,
$migration->getDescription(),
$currentPosition,
$totalMigrations,
$batchOperationId,
$e
);
// Track failed batch
$this->performanceTracker->failBatchOperation($batchOperationId, $e);
// Enhanced error reporting with recovery hints
$recoveryHints = $this->errorAnalyzer->generateMigrationRecoveryHints($migration, $e, $executedMigrations);
$migrationContext = $this->errorAnalyzer->analyzeMigrationContext($migration, $version, $e);
throw FrameworkException::create(
ErrorCode::DB_MIGRATION_FAILED,
"Migration {$version} failed: {$e->getMessage()}"
)->withContext(
ExceptionContext::forOperation('migration.execute', 'MigrationRunner')
->withData([
'migration_version' => $version,
'migration_description' => $migration->getDescription(),
'executed_migrations' => $executedMigrations,
'total_migrations' => $totalMigrations,
'current_position' => $currentPosition,
])
->withDebug($migrationContext)
->withMetadata([
'recovery_hints' => $recoveryHints,
'can_retry' => $this->errorAnalyzer->canRetryMigration($e),
'rollback_recommended' => $this->errorAnalyzer->isRollbackRecommended($e, $executedMigrations),
])
);
}
}
// Complete batch tracking
$batchSnapshot = $this->performanceTracker->completeBatchOperation($batchOperationId);
$this->migrationLogger->logMigrationBatchSummary($executedMigrations, $totalMigrations, $batchSnapshot);
return $executedMigrations;
}
/**
* Rollback migrations
*
* @param MigrationCollection $migrations The migrations to rollback from
* @param int $steps Number of migrations to rollback
* @return MigrationCollection List of rolled back migration versions
*/
public function rollback(MigrationCollection $migrations, int $steps = 1): MigrationCollection
public function rollback(MigrationCollection $migrations, int $steps = 1): array
{
$appliedVersions = MigrationVersionCollection::fromStrings($this->databaseManager->getAppliedVersions());
if ($appliedVersions->isEmpty()) {
$this->migrationLogger->logMigration('none', 'No migrations to rollback', 'Skipped');
return [];
}
$versionsToRollback = array_slice(array_reverse($appliedVersions->toArray()), 0, $steps);
$rolledBackMigrations = [];
$appliedVersions = $this->getAppliedVersions();
$totalRollbacks = count($versionsToRollback);
// Build the dependency graph
$this->dependencyGraph->buildGraph($migrations);
// Start rollback batch tracking
$rollbackBatchId = 'rollback_batch_' . uniqid();
$this->performanceTracker->startBatchOperation($rollbackBatchId, $totalRollbacks);
// Use the collection's sortedDescending method to get migrations in reverse order
$sortedMigrations = $migrations->sortedDescending();
// Create a map of version strings to migrations for easy lookup
$migrationMap = [];
foreach ($migrations as $migration) {
$migrationMap[$migration->getVersion()->toString()] = $migration;
}
// Create a list of versions that can be rolled back
$versionsToRollback = [];
$count = 0;
foreach ($sortedMigrations as $migration) {
if ($count >= $steps) {
break;
}
$version = $migration->getVersion()->toString();
if (! $appliedVersions->containsString($version)) {
continue;
}
// Check if any applied migrations depend on this one
$dependants = $this->dependencyGraph->getDependants($version);
$appliedDependants = [];
foreach ($dependants as $dependant) {
if ($appliedVersions->containsString($dependant) && ! in_array($dependant, $versionsToRollback)) {
$appliedDependants[] = $dependant;
}
}
// If there are applied dependants, we can't roll back this migration
if (! empty($appliedDependants)) {
echo "Cannot roll back {$version} because the following migrations depend on it: " . implode(', ', $appliedDependants) . "\n";
continue;
}
$versionsToRollback[] = $version;
$count++;
}
// Roll back the migrations in the correct order
$currentPosition = 0;
foreach ($versionsToRollback as $version) {
$migration = $migrationMap[$version];
$currentPosition++;
$migration = $migrations->findByVersion(MigrationVersion::fromTimestamp($version));
if (! $migration) {
$this->migrationLogger->logRollbackSkipped($version, ['reason' => 'Migration class not found']);
continue;
}
try {
Transaction::run($this->connection, function () use ($migration, $version) {
echo "Rolling back: {$version} - {$migration->getDescription()}\n";
// Validate rollback safety
$this->validator->validateRollbackSafety($migration, $version);
// Real-time rollback progress tracking
$this->migrationLogger->logRollbackProgress($version, $currentPosition, $totalRollbacks, 'starting');
// Start individual rollback tracking
$rollbackOperationId = "rollback_{$version}";
$this->performanceTracker->startRollbackOperation(
$rollbackOperationId,
$version,
$migration->getDescription(),
$currentPosition,
$totalRollbacks,
$rollbackBatchId,
$steps
);
// Execute rollback in transaction
$this->connection->beginTransaction();
try {
$migration->down($this->connection);
$this->databaseManager->recordMigrationRollback($version);
$this->connection->execute(
"DELETE FROM {$this->migrationsTable} WHERE version = ?",
[$version]
);
});
if ($this->connection->inTransaction()) {
$this->connection->commit();
}
} catch (\Throwable $e) {
if ($this->connection->inTransaction()) {
$this->connection->rollback();
}
throw $e;
}
$rolledBackMigrations[] = $migration;
echo "Rolled back: {$version}\n";
// Complete individual rollback tracking
$rollbackSnapshot = $this->performanceTracker->completeRollbackOperation(
$rollbackOperationId,
$version,
$migration->getDescription(),
$currentPosition,
$totalRollbacks,
$rollbackBatchId,
$steps
);
$this->migrationLogger->logMigration($version, $migration->getDescription(), 'Rolled back');
$this->migrationLogger->logRollbackProgress($version, $currentPosition, $totalRollbacks, 'completed', [
'execution_time' => $rollbackSnapshot?->duration?->toSeconds(),
'memory_used' => $rollbackSnapshot?->memoryDelta?->toHumanReadable(),
]);
// Post-rollback memory check
$this->performanceTracker->logPostRollbackMemory($version, $rollbackSnapshot);
} catch (\Throwable $e) {
throw new DatabaseException(
"Rollback {$version} failed: {$e->getMessage()}",
0,
$e
// Generate rollback recovery hints
$remainingRollbacks = array_slice($versionsToRollback, $currentPosition);
$recoveryHints = $this->errorAnalyzer->generateRollbackRecoveryHints($migration, $e, $remainingRollbacks);
throw FrameworkException::create(
ErrorCode::DB_MIGRATION_ROLLBACK_FAILED,
"Rollback failed for migration {$version}: {$e->getMessage()}"
)->withContext(
ExceptionContext::forOperation('migration.rollback', 'MigrationRunner')
->withData([
'migration_version' => $version,
'rollback_steps' => $steps,
'completed_rollbacks' => count($rolledBackMigrations),
'failed_migration' => $version,
])
->withMetadata([
'recovery_hints' => $recoveryHints,
'can_continue' => $this->errorAnalyzer->canContinueRollback($remainingRollbacks),
])
);
}
}
return new MigrationCollection(...$rolledBackMigrations);
// Complete rollback batch tracking
$rollbackSnapshot = $this->performanceTracker->completeBatchOperation($rollbackBatchId);
$this->migrationLogger->logRollbackBatchSummary($rolledBackMigrations, $totalRollbacks, $rollbackSnapshot);
return $rolledBackMigrations;
}
/**
* Get status of all migrations
*
* @param MigrationCollection $migrations The migrations to check status for
* @return MigrationStatusCollection Collection of migration statuses
* Get applied migration versions
*/
public function getStatus(MigrationCollection $migrations): MigrationStatusCollection
public function getAppliedVersions(): array
{
$appliedVersions = $this->getAppliedVersions();
return $this->databaseManager->getAppliedVersions();
}
/**
* Get status information for all migrations
*
* @return array<MigrationStatus>
*/
public function getStatus(MigrationCollection $migrations): array
{
$appliedVersions = MigrationVersionCollection::fromStrings($this->databaseManager->getAppliedVersions());
$statuses = [];
foreach ($migrations as $migration) {
$version = $migration->getVersion();
$applied = $appliedVersions->contains($version);
$statuses[] = $applied
? MigrationStatus::applied($version, $migration->getDescription())
: MigrationStatus::pending($version, $migration->getDescription());
$statuses[] = new MigrationStatus(
version: $version,
description: $migration->getDescription(),
applied: $applied
);
}
return new MigrationStatusCollection($statuses);
return $statuses;
}
public function getAppliedVersions(): MigrationVersionCollection
private function runPreFlightChecks(MigrationCollection $migrations, MigrationVersionCollection $appliedVersions): void
{
$versionStrings = $this->connection->queryColumn(
"SELECT version FROM {$this->migrationsTable} ORDER BY executed_at"
$results = $this->validator->runPreFlightChecks($migrations, $appliedVersions);
$dependencyResults = $this->validator->validateMigrationDependencies($migrations, $appliedVersions);
// Extract critical issues and warnings
$criticalIssues = [];
$warnings = [];
foreach ($results as $check => $result) {
if ($result['status'] === 'fail' && ($result['severity'] ?? 'info') === 'critical') {
$criticalIssues[] = $check . ': ' . $result['message'];
} elseif ($result['status'] === 'warning' || ($result['severity'] ?? 'info') === 'warning') {
$warnings[] = $check . ': ' . $result['message'];
}
}
$this->migrationLogger->logPreFlightResults($results, $criticalIssues, $warnings);
$this->migrationLogger->logDependencyValidationIssues(
$dependencyResults['orphaned_dependencies'],
$dependencyResults['partial_chains'],
$dependencyResults['ordering_issues']
);
return MigrationVersionCollection::fromStrings($versionStrings);
}
private function ensureMigrationsTable(): void
{
// Use database-agnostic approach
$driver = $this->connection->getPdo()->getAttribute(\PDO::ATTR_DRIVER_NAME);
$sql = match($driver) {
'mysql' => "CREATE TABLE IF NOT EXISTS {$this->migrationsTable} (
id INT PRIMARY KEY AUTO_INCREMENT,
version VARCHAR(20) NOT NULL UNIQUE COMMENT 'Format: YYYY_MM_DD_HHMMSS',
description TEXT,
executed_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX idx_version (version),
INDEX idx_executed_at (executed_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4",
'pgsql' => "CREATE TABLE IF NOT EXISTS {$this->migrationsTable} (
id SERIAL PRIMARY KEY,
version VARCHAR(20) NOT NULL UNIQUE,
description TEXT,
executed_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
)",
'sqlite' => "CREATE TABLE IF NOT EXISTS {$this->migrationsTable} (
id INTEGER PRIMARY KEY AUTOINCREMENT,
version TEXT NOT NULL UNIQUE,
description TEXT,
executed_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
)",
default => throw new \RuntimeException("Unsupported database driver: {$driver}")
};
$this->connection->execute($sql);
// Create indexes for PostgreSQL separately
if ($driver === 'pgsql') {
$this->connection->execute("CREATE INDEX IF NOT EXISTS idx_{$this->migrationsTable}_version ON {$this->migrationsTable} (version)");
$this->connection->execute("CREATE INDEX IF NOT EXISTS idx_{$this->migrationsTable}_executed_at ON {$this->migrationsTable} (executed_at)");
// Throw exception if critical issues found
if (! empty($criticalIssues)) {
throw FrameworkException::create(
ErrorCode::DB_MIGRATION_PREFLIGHT_FAILED,
'Pre-flight checks failed with critical issues'
)->withData([
'critical_issues' => $criticalIssues,
'warnings' => $warnings,
'full_results' => $results,
]);
}
}
}