Resolved multiple critical discovery system issues: ## Discovery System Fixes - Fixed console commands not being discovered on first run - Implemented fallback discovery for empty caches - Added context-aware caching with separate cache keys - Fixed object serialization preventing __PHP_Incomplete_Class ## Cache System Improvements - Smart caching that only caches meaningful results - Separate caches for different execution contexts (console, web, test) - Proper array serialization/deserialization for cache compatibility - Cache hit logging for debugging and monitoring ## Object Serialization Fixes - Fixed DiscoveredAttribute serialization with proper string conversion - Sanitized additional data to prevent object reference issues - Added fallback for corrupted cache entries ## Performance & Reliability - All 69 console commands properly discovered and cached - 534 total discovery items successfully cached and restored - No more __PHP_Incomplete_Class cache corruption - Improved error handling and graceful fallbacks ## Testing & Quality - Fixed code style issues across discovery components - Enhanced logging for better debugging capabilities - Improved cache validation and error recovery Ready for production deployment with stable discovery system. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
380 lines
12 KiB
PHP
380 lines
12 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Framework\Database\Commands;
|
|
|
|
use App\Framework\Console\ConsoleCommand;
|
|
use App\Framework\Console\ExitCode;
|
|
use App\Framework\Database\DatabaseManager;
|
|
use App\Framework\Database\Driver\Optimization\DatabaseOptimizer;
|
|
use App\Framework\Database\Driver\Optimization\MySQLOptimizer;
|
|
use App\Framework\Database\Driver\Optimization\PostgreSQLOptimizer;
|
|
use App\Framework\Database\Driver\Optimization\SQLiteOptimizer;
|
|
|
|
/**
|
|
* Command to optimize database tables and perform maintenance operations
|
|
*/
|
|
final readonly class DatabaseOptimizeCommand
|
|
{
|
|
public function __construct(
|
|
private DatabaseManager $databaseManager
|
|
) {
|
|
}
|
|
|
|
/**
|
|
* Optimize database tables
|
|
*
|
|
* @param string $connection The database connection to use
|
|
* @param string|null $table The table to optimize, or null for all tables
|
|
* @return ExitCode
|
|
*/
|
|
#[ConsoleCommand('db:optimize', 'Optimize database tables')]
|
|
public function optimize(string $connection = 'default', ?string $table = null): ExitCode
|
|
{
|
|
try {
|
|
$optimizer = $this->getOptimizer($connection);
|
|
|
|
echo "Optimizing tables...\n";
|
|
$results = $optimizer->optimizeTables($table);
|
|
|
|
$this->displayResults($results);
|
|
|
|
return ExitCode::SUCCESS;
|
|
} catch (\Throwable $e) {
|
|
echo "Error: {$e->getMessage()}\n";
|
|
|
|
return ExitCode::SOFTWARE_ERROR;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Analyze database tables
|
|
*
|
|
* @param string $connection The database connection to use
|
|
* @param string|null $table The table to analyze, or null for all tables
|
|
* @return ExitCode
|
|
*/
|
|
#[ConsoleCommand('db:analyze', 'Analyze database tables')]
|
|
public function analyze(string $connection = 'default', ?string $table = null): ExitCode
|
|
{
|
|
try {
|
|
$optimizer = $this->getOptimizer($connection);
|
|
|
|
echo "Analyzing tables...\n";
|
|
$results = $optimizer->analyzeTables($table);
|
|
|
|
$this->displayResults($results);
|
|
|
|
return ExitCode::SUCCESS;
|
|
} catch (\Throwable $e) {
|
|
echo "Error: {$e->getMessage()}\n";
|
|
|
|
return ExitCode::SOFTWARE_ERROR;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check database tables
|
|
*
|
|
* @param string $connection The database connection to use
|
|
* @param string|null $table The table to check, or null for all tables
|
|
* @param bool $extended Whether to perform an extended check
|
|
* @return ExitCode
|
|
*/
|
|
#[ConsoleCommand('db:check', 'Check database tables for errors')]
|
|
public function check(string $connection = 'default', ?string $table = null, bool $extended = false): ExitCode
|
|
{
|
|
try {
|
|
$optimizer = $this->getOptimizer($connection);
|
|
|
|
echo "Checking tables...\n";
|
|
$results = $optimizer->checkTables($table, $extended);
|
|
|
|
$this->displayResults($results);
|
|
|
|
return ExitCode::SUCCESS;
|
|
} catch (\Throwable $e) {
|
|
echo "Error: {$e->getMessage()}\n";
|
|
|
|
return ExitCode::SOFTWARE_ERROR;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Show table status
|
|
*
|
|
* @param string $connection The database connection to use
|
|
* @param string|null $table The table to show status for, or null for all tables
|
|
* @return ExitCode
|
|
*/
|
|
#[ConsoleCommand('db:table-status', 'Show database table status')]
|
|
public function status(string $connection = 'default', ?string $table = null): ExitCode
|
|
{
|
|
try {
|
|
$optimizer = $this->getOptimizer($connection);
|
|
|
|
echo "Table status:\n";
|
|
$results = $optimizer->getTableStatus($table);
|
|
|
|
foreach ($results as $tableName => $status) {
|
|
echo "\nTable: {$tableName}\n";
|
|
echo str_repeat('-', 80) . "\n";
|
|
|
|
// Format the status information
|
|
$this->displayTableStatus($status);
|
|
}
|
|
|
|
return ExitCode::SUCCESS;
|
|
} catch (\Throwable $e) {
|
|
echo "Error: {$e->getMessage()}\n";
|
|
|
|
return ExitCode::SOFTWARE_ERROR;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Show index statistics
|
|
*
|
|
* @param string $connection The database connection to use
|
|
* @param string|null $table The table to show index statistics for, or null for all tables
|
|
* @return ExitCode
|
|
*/
|
|
#[ConsoleCommand('db:indexes', 'Show database index statistics')]
|
|
public function indexes(string $connection = 'default', ?string $table = null): ExitCode
|
|
{
|
|
try {
|
|
$optimizer = $this->getOptimizer($connection);
|
|
|
|
echo "Index statistics:\n";
|
|
$results = $optimizer->getIndexStatistics($table);
|
|
|
|
foreach ($results as $tableName => $indexes) {
|
|
echo "\nTable: {$tableName}\n";
|
|
echo str_repeat('-', 80) . "\n";
|
|
|
|
if (empty($indexes)) {
|
|
echo "No indexes found.\n";
|
|
|
|
continue;
|
|
}
|
|
|
|
foreach ($indexes as $indexName => $indexInfo) {
|
|
echo " Index: {$indexName}\n";
|
|
|
|
// Format the index information
|
|
$this->displayIndexInfo($indexInfo);
|
|
|
|
echo "\n";
|
|
}
|
|
}
|
|
|
|
return ExitCode::SUCCESS;
|
|
} catch (\Throwable $e) {
|
|
echo "Error: {$e->getMessage()}\n";
|
|
|
|
return ExitCode::SOFTWARE_ERROR;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the appropriate optimizer for the database connection
|
|
*
|
|
* @param string $connection The database connection name
|
|
* @return DatabaseOptimizer The database optimizer
|
|
* @throws \RuntimeException If the database type is not supported
|
|
*/
|
|
private function getOptimizer(string $connection): DatabaseOptimizer
|
|
{
|
|
$conn = $this->databaseManager->getConnection($connection);
|
|
$driver = $conn->getPdo()->getAttribute(\PDO::ATTR_DRIVER_NAME);
|
|
|
|
return match ($driver) {
|
|
'mysql' => new MySQLOptimizer($conn),
|
|
'pgsql' => new PostgreSQLOptimizer($conn),
|
|
'sqlite' => new SQLiteOptimizer($conn),
|
|
default => throw new \RuntimeException("Unsupported database driver: {$driver}")
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Display the results of an operation
|
|
*
|
|
* @param array<string, string> $results The results to display
|
|
*/
|
|
private function displayResults(array $results): void
|
|
{
|
|
foreach ($results as $name => $result) {
|
|
echo " {$name}: {$result}\n";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Display table status information
|
|
*
|
|
* @param array<string, mixed> $status The table status information
|
|
*/
|
|
private function displayTableStatus(array $status): void
|
|
{
|
|
// Handle error case
|
|
if (isset($status['error'])) {
|
|
echo " Error: {$status['error']}\n";
|
|
|
|
return;
|
|
}
|
|
|
|
// Display basic information
|
|
if (isset($status['row_count'])) {
|
|
echo " Rows: " . number_format($status['row_count']) . "\n";
|
|
}
|
|
|
|
if (isset($status['columns'])) {
|
|
echo " Columns: {$status['columns']}\n";
|
|
}
|
|
|
|
// Display size information
|
|
if (isset($status['total_size'])) {
|
|
echo " Total size: {$status['total_size']}\n";
|
|
} elseif (isset($status['Data_length']) && isset($status['Index_length'])) {
|
|
$totalSize = $status['Data_length'] + $status['Index_length'];
|
|
echo " Total size: " . $this->formatBytes($totalSize) . "\n";
|
|
echo " Data size: " . $this->formatBytes($status['Data_length']) . "\n";
|
|
echo " Index size: " . $this->formatBytes($status['Index_length']) . "\n";
|
|
}
|
|
|
|
// Display engine information for MySQL
|
|
if (isset($status['Engine'])) {
|
|
echo " Engine: {$status['Engine']}\n";
|
|
}
|
|
|
|
// Display row format for MySQL
|
|
if (isset($status['Row_format'])) {
|
|
echo " Row format: {$status['Row_format']}\n";
|
|
}
|
|
|
|
// Display collation for MySQL
|
|
if (isset($status['Collation'])) {
|
|
echo " Collation: {$status['Collation']}\n";
|
|
}
|
|
|
|
// Display index count
|
|
if (isset($status['indexes']) && is_array($status['indexes'])) {
|
|
echo " Indexes: " . count($status['indexes']) . "\n";
|
|
}
|
|
|
|
// Display foreign key count
|
|
if (isset($status['foreign_keys']) && is_array($status['foreign_keys'])) {
|
|
echo " Foreign keys: " . count($status['foreign_keys']) . "\n";
|
|
}
|
|
|
|
// Display last update time for MySQL
|
|
if (isset($status['Update_time']) && $status['Update_time']) {
|
|
echo " Last updated: {$status['Update_time']}\n";
|
|
}
|
|
|
|
// Display auto increment value for MySQL
|
|
if (isset($status['Auto_increment']) && $status['Auto_increment']) {
|
|
echo " Auto increment: " . number_format($status['Auto_increment']) . "\n";
|
|
}
|
|
|
|
// Display PostgreSQL-specific information
|
|
if (isset($status['dead_rows']) && isset($status['row_count']) && $status['row_count'] > 0) {
|
|
$deadRowPercentage = round(($status['dead_rows'] / $status['row_count']) * 100, 2);
|
|
echo " Dead rows: {$status['dead_rows']} ({$deadRowPercentage}%)\n";
|
|
}
|
|
|
|
if (isset($status['last_vacuum'])) {
|
|
echo " Last vacuum: {$status['last_vacuum']}\n";
|
|
}
|
|
|
|
if (isset($status['last_analyze'])) {
|
|
echo " Last analyze: {$status['last_analyze']}\n";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Display index information
|
|
*
|
|
* @param array<string, mixed> $indexInfo The index information
|
|
*/
|
|
private function displayIndexInfo(array $indexInfo): void
|
|
{
|
|
// Handle error case
|
|
if (isset($indexInfo['error'])) {
|
|
echo " Error: {$indexInfo['error']}\n";
|
|
|
|
return;
|
|
}
|
|
|
|
// Display index type
|
|
if (isset($indexInfo['type'])) {
|
|
echo " Type: {$indexInfo['type']}\n";
|
|
} elseif (isset($indexInfo['index_type'])) {
|
|
echo " Type: {$indexInfo['index_type']}\n";
|
|
}
|
|
|
|
// Display uniqueness
|
|
if (isset($indexInfo['unique'])) {
|
|
echo " Unique: " . ($indexInfo['unique'] ? 'Yes' : 'No') . "\n";
|
|
} elseif (isset($indexInfo['is_unique'])) {
|
|
echo " Unique: " . ($indexInfo['is_unique'] ? 'Yes' : 'No') . "\n";
|
|
} elseif (isset($indexInfo['Non_unique'])) {
|
|
echo " Unique: " . ($indexInfo['Non_unique'] == 0 ? 'Yes' : 'No') . "\n";
|
|
}
|
|
|
|
// Display primary key status
|
|
if (isset($indexInfo['is_primary'])) {
|
|
echo " Primary key: " . ($indexInfo['is_primary'] ? 'Yes' : 'No') . "\n";
|
|
}
|
|
|
|
// Display columns
|
|
if (isset($indexInfo['columns']) && is_array($indexInfo['columns'])) {
|
|
echo " Columns: " . implode(', ', $indexInfo['columns']) . "\n";
|
|
} elseif (isset($indexInfo['Column_name'])) {
|
|
echo " Column: {$indexInfo['Column_name']}\n";
|
|
}
|
|
|
|
// Display cardinality
|
|
if (isset($indexInfo['cardinality'])) {
|
|
echo " Cardinality: " . number_format($indexInfo['cardinality']) . "\n";
|
|
} elseif (isset($indexInfo['Cardinality'])) {
|
|
echo " Cardinality: " . number_format($indexInfo['Cardinality']) . "\n";
|
|
}
|
|
|
|
// Display index size
|
|
if (isset($indexInfo['index_size'])) {
|
|
echo " Size: {$indexInfo['index_size']}\n";
|
|
}
|
|
|
|
// Display PostgreSQL-specific information
|
|
if (isset($indexInfo['index_scans'])) {
|
|
echo " Scans: " . number_format($indexInfo['index_scans']) . "\n";
|
|
}
|
|
|
|
if (isset($indexInfo['tuples_read']) && isset($indexInfo['tuples_fetched'])) {
|
|
echo " Tuples read: " . number_format($indexInfo['tuples_read']) . "\n";
|
|
echo " Tuples fetched: " . number_format($indexInfo['tuples_fetched']) . "\n";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Format bytes to a human-readable string
|
|
*
|
|
* @param int $bytes The number of bytes
|
|
* @param int $precision The number of decimal places
|
|
* @return string The formatted string
|
|
*/
|
|
private function formatBytes(int $bytes, int $precision = 2): string
|
|
{
|
|
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
|
|
$bytes = max($bytes, 0);
|
|
$pow = floor(($bytes ? log($bytes) : 0) / log(1024));
|
|
$pow = min($pow, count($units) - 1);
|
|
|
|
$bytes /= (1 << (10 * $pow));
|
|
|
|
return round($bytes, $precision) . ' ' . $units[$pow];
|
|
}
|
|
}
|