- Add comprehensive health check system with multiple endpoints - Add Prometheus metrics endpoint - Add production logging configurations (5 strategies) - Add complete deployment documentation suite: * QUICKSTART.md - 30-minute deployment guide * DEPLOYMENT_CHECKLIST.md - Printable verification checklist * DEPLOYMENT_WORKFLOW.md - Complete deployment lifecycle * PRODUCTION_DEPLOYMENT.md - Comprehensive technical reference * production-logging.md - Logging configuration guide * ANSIBLE_DEPLOYMENT.md - Infrastructure as Code automation * README.md - Navigation hub * DEPLOYMENT_SUMMARY.md - Executive summary - Add deployment scripts and automation - Add DEPLOYMENT_PLAN.md - Concrete plan for immediate deployment - Update README with production-ready features All production infrastructure is now complete and ready for deployment.
278 lines
8.4 KiB
PHP
278 lines
8.4 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Framework\Database\Indexing;
|
|
|
|
use App\Framework\Database\Indexing\ValueObjects\IndexRecommendation;
|
|
use App\Framework\Database\PdoConnection;
|
|
|
|
/**
|
|
* Generates database migration files for index optimizations
|
|
*
|
|
* Creates both UP and DOWN migrations for safe index management
|
|
*/
|
|
final readonly class IndexMigrationGenerator
|
|
{
|
|
private const MIGRATION_TEMPLATE = <<<'PHP'
|
|
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Framework\Database\Migration\Migration;
|
|
use App\Framework\Database\Schema\Schema;
|
|
|
|
final class {CLASS_NAME} extends Migration
|
|
{
|
|
public function up(Schema $schema): void
|
|
{
|
|
// {DESCRIPTION}
|
|
$schema->table('{TABLE_NAME}', function ($table) {
|
|
{UP_STATEMENTS}
|
|
});
|
|
}
|
|
|
|
public function down(Schema $schema): void
|
|
{
|
|
$schema->table('{TABLE_NAME}', function ($table) {
|
|
{DOWN_STATEMENTS}
|
|
});
|
|
}
|
|
}
|
|
|
|
PHP;
|
|
|
|
public function __construct(
|
|
private PdoConnection $connection
|
|
) {}
|
|
|
|
/**
|
|
* Generate migration file for adding recommended indexes
|
|
*
|
|
* @param array<IndexRecommendation> $recommendations
|
|
*/
|
|
public function generateAddIndexMigration(array $recommendations, string $tableName): string
|
|
{
|
|
if (empty($recommendations)) {
|
|
throw new \InvalidArgumentException('No index recommendations provided');
|
|
}
|
|
|
|
$className = $this->generateClassName('AddIndexesTo' . ucfirst($tableName));
|
|
$upStatements = [];
|
|
$downStatements = [];
|
|
|
|
foreach ($recommendations as $recommendation) {
|
|
$indexName = $recommendation->getIndexName()->toString();
|
|
$columns = $recommendation->columns;
|
|
|
|
$upStatements[] = $this->generateAddIndexStatement($indexName, $columns, $recommendation->indexType->value);
|
|
$downStatements[] = $this->generateDropIndexStatement($indexName);
|
|
}
|
|
|
|
$description = 'Add optimized indexes based on query analysis';
|
|
|
|
return $this->fillTemplate(
|
|
className: $className,
|
|
tableName: $tableName,
|
|
description: $description,
|
|
upStatements: $upStatements,
|
|
downStatements: $downStatements
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Generate migration file for removing unused indexes
|
|
*
|
|
* @param array<array{index_name: string, columns: array<string>}> $unusedIndexes
|
|
*/
|
|
public function generateRemoveIndexMigration(array $unusedIndexes, string $tableName): string
|
|
{
|
|
if (empty($unusedIndexes)) {
|
|
throw new \InvalidArgumentException('No unused indexes provided');
|
|
}
|
|
|
|
$className = $this->generateClassName('RemoveUnusedIndexesFrom' . ucfirst($tableName));
|
|
$upStatements = [];
|
|
$downStatements = [];
|
|
|
|
foreach ($unusedIndexes as $index) {
|
|
$indexName = $index['index_name'];
|
|
$columns = $index['columns'];
|
|
|
|
$upStatements[] = $this->generateDropIndexStatement($indexName);
|
|
$downStatements[] = $this->generateAddIndexStatement($indexName, $columns, 'BTREE');
|
|
}
|
|
|
|
$description = 'Remove unused indexes identified by analysis';
|
|
|
|
return $this->fillTemplate(
|
|
className: $className,
|
|
tableName: $tableName,
|
|
description: $description,
|
|
upStatements: $upStatements,
|
|
downStatements: $downStatements
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Generate comprehensive optimization migration (add + remove)
|
|
*
|
|
* @param array<IndexRecommendation> $toAdd
|
|
* @param array<array{index_name: string, columns: array<string>}> $toRemove
|
|
*/
|
|
public function generateOptimizationMigration(array $toAdd, array $toRemove, string $tableName): string
|
|
{
|
|
$className = $this->generateClassName('OptimizeIndexesFor' . ucfirst($tableName));
|
|
$upStatements = [];
|
|
$downStatements = [];
|
|
|
|
// First, drop unused indexes
|
|
foreach ($toRemove as $index) {
|
|
$indexName = $index['index_name'];
|
|
$columns = $index['columns'];
|
|
|
|
$upStatements[] = $this->generateDropIndexStatement($indexName);
|
|
$downStatements[] = $this->generateAddIndexStatement($indexName, $columns, 'BTREE');
|
|
}
|
|
|
|
// Then, add recommended indexes
|
|
foreach ($toAdd as $recommendation) {
|
|
$indexName = $recommendation->getIndexName()->toString();
|
|
$columns = $recommendation->columns;
|
|
|
|
$upStatements[] = $this->generateAddIndexStatement($indexName, $columns, $recommendation->indexType->value);
|
|
$downStatements[] = $this->generateDropIndexStatement($indexName);
|
|
}
|
|
|
|
// Reverse down statements to maintain proper order
|
|
$downStatements = array_reverse($downStatements);
|
|
|
|
$description = 'Optimize indexes: remove unused, add recommended';
|
|
|
|
return $this->fillTemplate(
|
|
className: $className,
|
|
tableName: $tableName,
|
|
description: $description,
|
|
upStatements: $upStatements,
|
|
downStatements: $downStatements
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Save migration file to disk
|
|
*/
|
|
public function saveMigration(string $migrationContent, ?string $customPath = null): string
|
|
{
|
|
$timestamp = date('YmdHis');
|
|
$className = $this->extractClassName($migrationContent);
|
|
$snakeCaseName = $this->camelToSnake($className);
|
|
|
|
$filename = "{$timestamp}_{$snakeCaseName}.php";
|
|
$path = $customPath ?? __DIR__ . '/../../../migrations/' . $filename;
|
|
|
|
// Ensure directory exists
|
|
$directory = dirname($path);
|
|
if (!is_dir($directory)) {
|
|
mkdir($directory, 0755, true);
|
|
}
|
|
|
|
file_put_contents($path, $migrationContent);
|
|
|
|
return $path;
|
|
}
|
|
|
|
/**
|
|
* Generate migration class name with timestamp
|
|
*/
|
|
private function generateClassName(string $baseName): string
|
|
{
|
|
return 'Migration' . date('YmdHis') . $baseName;
|
|
}
|
|
|
|
/**
|
|
* Generate ADD INDEX statement
|
|
*/
|
|
private function generateAddIndexStatement(string $indexName, array $columns, string $type = 'BTREE'): string
|
|
{
|
|
$columnsStr = implode(', ', array_map(fn($col) => "'{$col}'", $columns));
|
|
|
|
return "\$table->index([{$columnsStr}], '{$indexName}', '{$type}');";
|
|
}
|
|
|
|
/**
|
|
* Generate DROP INDEX statement
|
|
*/
|
|
private function generateDropIndexStatement(string $indexName): string
|
|
{
|
|
return "\$table->dropIndex('{$indexName}');";
|
|
}
|
|
|
|
/**
|
|
* Fill migration template with actual data
|
|
*/
|
|
private function fillTemplate(
|
|
string $className,
|
|
string $tableName,
|
|
string $description,
|
|
array $upStatements,
|
|
array $downStatements
|
|
): string {
|
|
$upStatementsStr = implode("\n", array_map(
|
|
fn($stmt) => " {$stmt}",
|
|
$upStatements
|
|
));
|
|
|
|
$downStatementsStr = implode("\n", array_map(
|
|
fn($stmt) => " {$stmt}",
|
|
$downStatements
|
|
));
|
|
|
|
return str_replace(
|
|
['{CLASS_NAME}', '{TABLE_NAME}', '{DESCRIPTION}', '{UP_STATEMENTS}', '{DOWN_STATEMENTS}'],
|
|
[$className, $tableName, $description, $upStatementsStr, $downStatementsStr],
|
|
self::MIGRATION_TEMPLATE
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Extract class name from migration content
|
|
*/
|
|
private function extractClassName(string $content): string
|
|
{
|
|
if (preg_match('/final class (\w+) extends Migration/', $content, $matches)) {
|
|
return $matches[1];
|
|
}
|
|
|
|
throw new \RuntimeException('Could not extract class name from migration');
|
|
}
|
|
|
|
/**
|
|
* Convert camelCase to snake_case
|
|
*/
|
|
private function camelToSnake(string $input): string
|
|
{
|
|
return strtolower(preg_replace('/(?<!^)[A-Z]/', '_$0', $input));
|
|
}
|
|
|
|
/**
|
|
* Validate migration content before saving
|
|
*/
|
|
private function validateMigration(string $content): bool
|
|
{
|
|
// Basic validation
|
|
if (!str_contains($content, 'extends Migration')) {
|
|
throw new \RuntimeException('Migration must extend Migration class');
|
|
}
|
|
|
|
if (!str_contains($content, 'public function up(')) {
|
|
throw new \RuntimeException('Migration must have up() method');
|
|
}
|
|
|
|
if (!str_contains($content, 'public function down(')) {
|
|
throw new \RuntimeException('Migration must have down() method');
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|