fix: resolve RedisCache array offset error and improve discovery diagnostics

- Fix RedisCache driver to handle MGET failures gracefully with fallback
- Add comprehensive discovery context comparison debug tools
- Identify root cause: WEB context discovery missing 166 items vs CLI
- WEB context missing RequestFactory class entirely (52 vs 69 commands)
- Improved exception handling with detailed binding diagnostics
This commit is contained in:
2025-09-12 20:05:18 +02:00
parent 8040d3e7a5
commit e30753ba0e
46990 changed files with 10789682 additions and 89639 deletions

View File

@@ -9,6 +9,8 @@ use App\Framework\Database\Config\PoolConfig;
use App\Framework\Database\Driver\DriverConfig;
use App\Framework\Database\Exception\DatabaseException;
use App\Framework\DateTime\Timer;
use App\Framework\Exception\ErrorCode;
use App\Framework\Exception\ExceptionContext;
final class ConnectionPool
{
@@ -67,7 +69,16 @@ final class ConnectionPool
return $this->createNewConnection();
}
throw new DatabaseException('Maximum number of connections reached and no healthy connections available');
throw DatabaseException::create(
ErrorCode::DB_CONNECTION_FAILED,
'Maximum number of connections reached and no healthy connections available',
ExceptionContext::forOperation('get_connection', 'ConnectionPool')
->withData([
'current_connections' => $this->currentConnections,
'max_connections' => $this->poolConfig->maxConnections,
'connections_in_use' => count($this->inUse),
])
);
}
private function findAvailableHealthyConnection(): ?PooledConnection
@@ -122,7 +133,18 @@ final class ConnectionPool
} catch (\Exception $e) {
$retries++;
if ($retries >= $maxRetries) {
throw new DatabaseException("Failed to create connection after {$maxRetries} retries: " . $e->getMessage());
throw DatabaseException::create(
ErrorCode::DB_CONNECTION_FAILED,
"Failed to create connection after {$maxRetries} retries: " . $e->getMessage(),
ExceptionContext::forOperation('create_connection', 'ConnectionPool')
->withData([
'max_retries' => $maxRetries,
'current_connections' => $this->currentConnections,
'max_connections' => $this->poolConfig->maxConnections,
])
->withDebug(['original_error' => $e->getMessage()]),
$e
);
}
// Exponential backoff
@@ -131,7 +153,15 @@ final class ConnectionPool
}
}
throw new DatabaseException('Unable to create new connection');
throw DatabaseException::create(
ErrorCode::DB_CONNECTION_FAILED,
'Unable to create new connection',
ExceptionContext::forOperation('create_connection', 'ConnectionPool')
->withData([
'current_connections' => $this->currentConnections,
'max_connections' => $this->poolConfig->maxConnections,
])
);
}
public function releaseConnection(string $id): void

View File

@@ -197,6 +197,32 @@ final class DatabaseManager
}
}
public function hasConnection(): bool
{
return $this->connectionPool !== null || $this->readWriteConnection !== null;
}
/**
* @return array<string>
*/
public function getConnectionNames(): array
{
$names = ['default'];
if ($this->config->readWriteConfig->enabled) {
$names[] = 'read';
$names[] = 'write';
}
if ($this->config->poolConfig->enabled) {
for ($i = 0; $i < $this->config->poolConfig->maxConnections; $i++) {
$names[] = "pool_connection_{$i}";
}
}
return $names;
}
/**
* Get profiling statistics for this database manager
*/

View File

@@ -23,14 +23,24 @@ final readonly class User
#[Column(name: 'email')]
public ?string $email;
#[Column(name: 'failed_attempts')]
public int $failed_attempts;
#[Column(name: 'password_hash')]
public ?string $password_hash;
public function __construct(
string $name,
?string $email = null,
?string $id = null
?string $id = null,
int $failed_attempts = 0,
?string $password_hash = null
) {
$this->id = $id ?? IdGenerator::generate();
$this->name = $name;
$this->email = $email;
$this->failed_attempts = $failed_attempts;
$this->password_hash = $password_hash;
}
/**
@@ -38,7 +48,7 @@ final readonly class User
*/
public function withName(string $name): self
{
return new self($name, $this->email, $this->id);
return new self($name, $this->email, $this->id, $this->failed_attempts, $this->password_hash);
}
/**
@@ -46,6 +56,6 @@ final readonly class User
*/
public function withEmail(?string $email): self
{
return new self($this->name, $email, $this->id);
return new self($this->name, $email, $this->id, $this->failed_attempts, $this->password_hash);
}
}

View File

@@ -152,6 +152,8 @@ final class MigrationDependencyGraph
/**
* Utility function for circular dependency detection using DFS
* @param array<string, bool> $visited
* @param array<string, bool> $recursionStack
*/
private function detectCircularDependenciesUtil(string $version, array &$visited, array &$recursionStack): bool
{
@@ -178,6 +180,8 @@ final class MigrationDependencyGraph
/**
* Perform a topological sort using depth-first search
* @param array<string, bool> $visited
* @param array<int, string> $order
*/
private function topologicalSort(string $version, array &$visited, array &$order): void
{

View File

@@ -48,6 +48,7 @@ final readonly class MigrationStatus
/**
* Convert to array for backward compatibility
* @return array<string, string|bool>
*/
public function toArray(): array
{

View File

@@ -85,6 +85,7 @@ final readonly class MigrationStatusCollection implements Countable, IteratorAgg
/**
* Convert to array for backward compatibility
* @return array<int, array<string, string|bool>>
*/
public function toArray(): array
{

View File

@@ -32,6 +32,11 @@ final readonly class MigrationVersion
return $this->timestamp;
}
public function toString(): string
{
return $this->timestamp;
}
public function compare(MigrationVersion $other): int
{
return $this->timestamp <=> $other->timestamp;

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Framework\Database\Monitoring\Dashboard;
use App\Framework\Attributes\Route;
use App\Framework\Database\DatabaseManager;
use App\Framework\Database\Driver\Optimization\MySQLOptimizer;
use App\Framework\Database\Driver\Optimization\PostgreSQLOptimizer;
@@ -175,6 +176,7 @@ final readonly class DatabaseDashboardController implements Controller
/**
* Get connection statistics
* @return array<string, mixed>
*/
private function getConnectionStats(string $connection): array
{

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Framework\Database\Monitoring\Dashboard;
use App\Framework\Attributes\Route;
use App\Framework\Database\DatabaseManager;
use App\Framework\Database\Monitoring\Health\DatabaseHealthChecker;
use App\Framework\Http\Controller;

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Framework\Database\Monitoring\Dashboard;
use App\Framework\Attributes\Route;
use App\Framework\Database\Monitoring\History\QueryHistoryLogger;
use App\Framework\Http\Controller;
use App\Framework\Http\Response\JsonResponse;

View File

@@ -154,6 +154,7 @@ final class HealthCheckResult
/**
* Get the result as an array
* @return array<string, mixed>
*/
public function toArray(): array
{

View File

@@ -72,6 +72,7 @@ final readonly class HealthCheckStatus
/**
* Get the status as an array
* @return array<string, string>
*/
public function toArray(): array
{

View File

@@ -27,7 +27,7 @@ final class PdoConnection implements ConnectionInterface
return $statement->rowCount();
} catch (\PDOException $e) {
throw new DatabaseException("Failed to execute query: {$e->getMessage()}", 0, $e);
throw DatabaseException::simple("Failed to execute query: {$e->getMessage()}", $e);
}
}
@@ -39,7 +39,7 @@ final class PdoConnection implements ConnectionInterface
return new PdoResult($statement);
} catch (\PDOException $e) {
throw new DatabaseException("Failed to execute query: {$e->getMessage()} --- SQL: {$sql} PARAMETERS: {".implode(", ", $parameters)."}", 0, $e);
throw DatabaseException::simple("Failed to execute query: {$e->getMessage()} --- SQL: {$sql} PARAMETERS: {".implode(", ", $parameters)."}", $e);
}
}
@@ -69,7 +69,7 @@ final class PdoConnection implements ConnectionInterface
try {
$this->pdo->beginTransaction();
} catch (\PDOException $e) {
throw new DatabaseException("Failed to begin transaction: {$e->getMessage()}", 0, $e);
throw DatabaseException::simple("Failed to begin transaction: {$e->getMessage()}", $e);
}
}
@@ -78,7 +78,7 @@ final class PdoConnection implements ConnectionInterface
try {
$this->pdo->commit();
} catch (\PDOException $e) {
throw new DatabaseException("Failed to commit transaction: {$e->getMessage()}", 0, $e);
throw DatabaseException::simple("Failed to commit transaction: {$e->getMessage()}", $e);
}
}
@@ -87,7 +87,7 @@ final class PdoConnection implements ConnectionInterface
try {
$this->pdo->rollBack();
} catch (\PDOException $e) {
throw new DatabaseException("Failed to rollback transaction: {$e->getMessage()}", 0, $e);
throw DatabaseException::simple("Failed to rollback transaction: {$e->getMessage()}", $e);
}
}

View File

@@ -39,6 +39,7 @@ abstract class EntityRepository
/**
* Findet alle Entities
* @return array<int, object>
*/
public function findAll(): array
{
@@ -47,6 +48,7 @@ abstract class EntityRepository
/**
* Findet alle Entities (eager loading)
* @return array<int, object>
*/
public function findAllEager(): array
{
@@ -55,6 +57,9 @@ abstract class EntityRepository
/**
* Findet Entities nach Kriterien
* @param array<string, mixed> $criteria
* @param array<string, string>|null $orderBy
* @return array<int, object>
*/
public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null): array
{
@@ -63,6 +68,7 @@ abstract class EntityRepository
/**
* Findet eine Entity nach Kriterien
* @param array<string, mixed> $criteria
*/
public function findOneBy(array $criteria): ?object
{
@@ -79,6 +85,8 @@ abstract class EntityRepository
/**
* Speichert mehrere Entities
* @param array<int, object> $entities
* @return array<int, object>
*/
public function saveAll(array $entities): array
{

View File

@@ -120,6 +120,7 @@ final class IndexUsageAnalyzer
/**
* Get MySQL index usage statistics
* @return array<array<string, mixed>>
*/
private function getMySqlIndexUsageStatistics(?string $table = null): array
{
@@ -183,6 +184,7 @@ final class IndexUsageAnalyzer
/**
* Get MySQL unused indexes
* @return array<array<string, mixed>>
*/
private function getMySqlUnusedIndexes(?string $table = null, int $minDaysSinceCreation = 30): array
{
@@ -226,6 +228,7 @@ final class IndexUsageAnalyzer
/**
* Get MySQL duplicate indexes
* @return array<array<string, mixed>>
*/
private function getMySqlDuplicateIndexes(?string $table = null): array
{
@@ -280,6 +283,7 @@ final class IndexUsageAnalyzer
/**
* Get MySQL oversized indexes
* @return array<array<string, mixed>>
*/
private function getMySqlOversizedIndexes(?string $table = null, int $sizeThresholdMb = 100): array
{
@@ -318,6 +322,7 @@ final class IndexUsageAnalyzer
/**
* Get MySQL fragmented indexes
* @return array<array<string, mixed>>
*/
private function getMySqlFragmentedIndexes(?string $table = null, float $fragmentationThreshold = 0.3): array
{
@@ -354,6 +359,7 @@ final class IndexUsageAnalyzer
/**
* Get PostgreSQL index usage statistics
* @return array<array<string, mixed>>
*/
private function getPostgreSqlIndexUsageStatistics(?string $table = null): array
{
@@ -389,6 +395,7 @@ final class IndexUsageAnalyzer
/**
* Get PostgreSQL unused indexes
* @return array<array<string, mixed>>
*/
private function getPostgreSqlUnusedIndexes(?string $table = null, int $minDaysSinceCreation = 30): array
{
@@ -427,6 +434,7 @@ final class IndexUsageAnalyzer
/**
* Get PostgreSQL duplicate indexes
* @return array<array<string, mixed>>
*/
private function getPostgreSqlDuplicateIndexes(?string $table = null): array
{
@@ -491,6 +499,7 @@ final class IndexUsageAnalyzer
/**
* Get PostgreSQL oversized indexes
* @return array<array<string, mixed>>
*/
private function getPostgreSqlOversizedIndexes(?string $table = null, int $sizeThresholdMb = 100): array
{
@@ -530,6 +539,7 @@ final class IndexUsageAnalyzer
/**
* Get PostgreSQL fragmented indexes
* @return array<array<string, mixed>>
*/
private function getPostgreSqlFragmentedIndexes(?string $table = null, float $fragmentationThreshold = 0.3): array
{

View File

@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace App\Framework\Database\StoredProcedure\Exception;
use App\Framework\Database\Exception\DatabaseException;
use App\Framework\Exception\ExceptionContext;
/**
* Base exception for stored function errors
@@ -19,9 +20,13 @@ class StoredFunctionException extends DatabaseException
string $message,
?\Throwable $previous = null
): self {
$context = ExceptionContext::forOperation('stored_function.error', 'StoredFunction')
->withData(['function_name' => $functionName]);
return new self(
"Error in stored function '{$functionName}': {$message}",
0,
$context,
500,
$previous
);
}
@@ -34,9 +39,13 @@ class StoredFunctionException extends DatabaseException
string $message,
?\Throwable $previous = null
): self {
$context = ExceptionContext::forOperation('stored_function.execution', 'StoredFunction')
->withData(['function_name' => $functionName]);
return new self(
"Error executing stored function '{$functionName}': {$message}",
0,
$context,
500,
$previous
);
}
@@ -49,9 +58,13 @@ class StoredFunctionException extends DatabaseException
string $message,
?\Throwable $previous = null
): self {
$context = ExceptionContext::forOperation('stored_function.creation', 'StoredFunction')
->withData(['function_name' => $functionName]);
return new self(
"Error creating stored function '{$functionName}': {$message}",
0,
$context,
500,
$previous
);
}
@@ -61,7 +74,14 @@ class StoredFunctionException extends DatabaseException
*/
public static function notFound(string $functionName): self
{
return new self("Stored function '{$functionName}' not found");
$context = ExceptionContext::forOperation('stored_function.not_found', 'StoredFunction')
->withData(['function_name' => $functionName]);
return new self(
"Stored function '{$functionName}' not found",
$context,
404
);
}
/**
@@ -69,9 +89,18 @@ class StoredFunctionException extends DatabaseException
*/
public static function invalidReturnType(string $functionName, string $expectedType, string $actualType): self
{
$context = ExceptionContext::forOperation('stored_function.invalid_return_type', 'StoredFunction')
->withData([
'function_name' => $functionName,
'expected_type' => $expectedType,
'actual_type' => $actualType,
]);
return new self(
"Invalid return type for stored function '{$functionName}': " .
"expected {$expectedType}, got {$actualType}"
"expected {$expectedType}, got {$actualType}",
$context,
400
);
}
}

View File

@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace App\Framework\Database\StoredProcedure\Exception;
use App\Framework\Database\Exception\DatabaseException;
use App\Framework\Exception\ExceptionContext;
/**
* Base exception for stored procedure errors
@@ -19,9 +20,13 @@ class StoredProcedureException extends DatabaseException
string $message,
?\Throwable $previous = null
): self {
$context = ExceptionContext::forOperation('stored_procedure.error', 'StoredProcedure')
->withData(['procedure_name' => $procedureName]);
return new self(
"Error in stored procedure '{$procedureName}': {$message}",
0,
$context,
500,
$previous
);
}
@@ -34,9 +39,13 @@ class StoredProcedureException extends DatabaseException
string $message,
?\Throwable $previous = null
): self {
$context = ExceptionContext::forOperation('stored_procedure.execution', 'StoredProcedure')
->withData(['procedure_name' => $procedureName]);
return new self(
"Error executing stored procedure '{$procedureName}': {$message}",
0,
$context,
500,
$previous
);
}
@@ -49,9 +58,13 @@ class StoredProcedureException extends DatabaseException
string $message,
?\Throwable $previous = null
): self {
$context = ExceptionContext::forOperation('stored_procedure.creation', 'StoredProcedure')
->withData(['procedure_name' => $procedureName]);
return new self(
"Error creating stored procedure '{$procedureName}': {$message}",
0,
$context,
500,
$previous
);
}
@@ -61,6 +74,13 @@ class StoredProcedureException extends DatabaseException
*/
public static function notFound(string $procedureName): self
{
return new self("Stored procedure '{$procedureName}' not found");
$context = ExceptionContext::forOperation('stored_procedure.not_found', 'StoredProcedure')
->withData(['procedure_name' => $procedureName]);
return new self(
"Stored procedure '{$procedureName}' not found",
$context,
404
);
}
}