- 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
220 lines
5.7 KiB
PHP
220 lines
5.7 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Framework\Database;
|
|
|
|
use App\Framework\Database\Config\ReadWriteConfig;
|
|
use App\Framework\Database\Exception\DatabaseException;
|
|
use App\Framework\Database\ReadWrite\MasterSlaveRouter;
|
|
use App\Framework\Database\ReadWrite\ReplicationLagDetector;
|
|
use App\Framework\Database\ValueObjects\SqlQuery;
|
|
use App\Framework\DateTime\Clock;
|
|
|
|
/**
|
|
* Class ReadWriteConnection provides a mechanism to handle separate writing and reading database connections.
|
|
* It ensures that write-heavy operations are directed to a designated write connection,
|
|
* while read operations are distributed among a pool of read connections.
|
|
*
|
|
* Enhanced with load balancing, health checks, and lag detection.
|
|
*/
|
|
final class ReadWriteConnection implements ConnectionInterface
|
|
{
|
|
private ConnectionInterface $writeConnection;
|
|
|
|
private array $readConnections;
|
|
|
|
private bool $forceWrite = false;
|
|
|
|
private ?MasterSlaveRouter $router = null;
|
|
|
|
public function __construct(
|
|
ConnectionInterface $writeConnection,
|
|
array $readConnections,
|
|
private readonly ?ReadWriteConfig $config = null,
|
|
private readonly ?Clock $clock = null,
|
|
private readonly ?ReplicationLagDetector $lagDetector = null
|
|
) {
|
|
$this->writeConnection = $writeConnection;
|
|
$this->readConnections = $readConnections;
|
|
|
|
if (empty($this->readConnections)) {
|
|
throw new DatabaseException('At least one read connection is required');
|
|
}
|
|
|
|
// Initialize advanced router if dependencies are provided
|
|
if ($this->config && $this->clock && $this->lagDetector) {
|
|
$this->router = new MasterSlaveRouter(
|
|
$writeConnection,
|
|
$readConnections,
|
|
$this->config,
|
|
$this->clock,
|
|
$this->lagDetector
|
|
);
|
|
}
|
|
}
|
|
|
|
public function execute(SqlQuery $query): int
|
|
{
|
|
$this->forceWrite = true;
|
|
|
|
return $this->writeConnection->execute($query);
|
|
}
|
|
|
|
public function query(SqlQuery $query): ResultInterface
|
|
{
|
|
$connection = $this->getConnection($query->sql);
|
|
|
|
return $connection->query($query);
|
|
}
|
|
|
|
public function queryOne(SqlQuery $query): ?array
|
|
{
|
|
$connection = $this->getConnection($query->sql);
|
|
|
|
return $connection->queryOne($query);
|
|
}
|
|
|
|
public function queryColumn(SqlQuery $query): array
|
|
{
|
|
$connection = $this->getConnection($query->sql);
|
|
|
|
return $connection->queryColumn($query);
|
|
}
|
|
|
|
public function queryScalar(SqlQuery $query): mixed
|
|
{
|
|
$connection = $this->getConnection($query->sql);
|
|
|
|
return $connection->queryScalar($query);
|
|
}
|
|
|
|
public function beginTransaction(): void
|
|
{
|
|
$this->forceWrite = true;
|
|
$this->writeConnection->beginTransaction();
|
|
}
|
|
|
|
public function commit(): void
|
|
{
|
|
$this->writeConnection->commit();
|
|
$this->forceWrite = false;
|
|
}
|
|
|
|
public function rollback(): void
|
|
{
|
|
$this->writeConnection->rollback();
|
|
$this->forceWrite = false;
|
|
}
|
|
|
|
public function inTransaction(): bool
|
|
{
|
|
return $this->writeConnection->inTransaction();
|
|
}
|
|
|
|
public function lastInsertId(): string
|
|
{
|
|
return $this->writeConnection->lastInsertId();
|
|
}
|
|
|
|
public function getPdo(): \PDO
|
|
{
|
|
return $this->writeConnection->getPdo();
|
|
}
|
|
|
|
public function forceWriteConnection(): void
|
|
{
|
|
$this->forceWrite = true;
|
|
}
|
|
|
|
public function resetConnectionMode(): void
|
|
{
|
|
$this->forceWrite = false;
|
|
$this->router?->resetStickyConnection();
|
|
}
|
|
|
|
private function shouldUseWriteConnection(string $sql): bool
|
|
{
|
|
if ($this->forceWrite || $this->inTransaction()) {
|
|
return true;
|
|
}
|
|
|
|
$sql = trim(strtoupper($sql));
|
|
$writeOperations = ['INSERT', 'UPDATE', 'DELETE', 'CREATE', 'ALTER', 'DROP', 'TRUNCATE'];
|
|
|
|
return array_any($writeOperations, fn ($operation) => str_starts_with($sql, $operation));
|
|
|
|
}
|
|
|
|
/**
|
|
* Get appropriate connection for SQL query
|
|
*/
|
|
private function getConnection(string $sql): ConnectionInterface
|
|
{
|
|
if ($this->router) {
|
|
return $this->router->route($sql, $this->forceWrite);
|
|
}
|
|
|
|
// Fallback to simple routing
|
|
if ($this->shouldUseWriteConnection($sql)) {
|
|
return $this->writeConnection;
|
|
}
|
|
|
|
return $this->getReadConnection();
|
|
}
|
|
|
|
/**
|
|
* Simple round-robin read connection selection (fallback)
|
|
*/
|
|
private function getReadConnection(): ConnectionInterface
|
|
{
|
|
static $currentReadIndex = 0;
|
|
|
|
$connection = $this->readConnections[$currentReadIndex];
|
|
$currentReadIndex = ($currentReadIndex + 1) % count($this->readConnections);
|
|
|
|
return $connection;
|
|
}
|
|
|
|
/**
|
|
* Get read/write statistics
|
|
*/
|
|
public function getStatistics(): array
|
|
{
|
|
if ($this->router) {
|
|
return $this->router->getStatistics();
|
|
}
|
|
|
|
return [
|
|
'router' => 'simple',
|
|
'master_connection' => 'active',
|
|
'replica_count' => count($this->readConnections),
|
|
'load_balancing_strategy' => 'round_robin',
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Get write connection (for direct access if needed)
|
|
*/
|
|
public function getWriteConnection(): ConnectionInterface
|
|
{
|
|
return $this->writeConnection;
|
|
}
|
|
|
|
/**
|
|
* Get read connections
|
|
*/
|
|
public function getReadConnections(): array
|
|
{
|
|
return $this->readConnections;
|
|
}
|
|
|
|
/**
|
|
* Check if advanced routing is enabled
|
|
*/
|
|
public function hasAdvancedRouting(): bool
|
|
{
|
|
return $this->router !== null;
|
|
}
|
|
}
|