0

Connection class — wrapping PDO with a fluent API

Intermediate5 min read·fw-06-002
sql

Concept

A Connection class wraps PDO to provide a fluent, convenient API that hides PDO's verbose method calls and adds framework-specific functionality like query logging, event firing, and reconnection handling.

What a Connection class adds over raw PDO:

  • Fluent query execution: $db->select('SELECT * FROM users WHERE id = ?', [1]) instead of multi-step prepare/execute/fetchAll.
  • Query logging: Records every query and its bindings + execution time.
  • Reconnection: Detects "gone away" errors and reconnects automatically.
  • Events: Fires events before/after queries.
  • Transaction management: Tracks nesting depth, manages savepoints.
  • Grammar abstraction: Different SQL dialects per database.

Key methods:

  • select(string $sql, array $bindings = []): array: Run a SELECT, return all rows.
  • selectOne(string $sql, array $bindings = []): ?array: Return the first row or null.
  • insert(string $sql, array $bindings = []): bool.
  • update(string $sql, array $bindings = []): int: Returns affected rows.
  • delete(string $sql, array $bindings = []): int: Returns affected rows.
  • statement(string $sql, array $bindings = []): bool: For DDL.
  • getPdo(): PDO: Access the raw PDO.

Reconnection strategy: Catch PDOException with "server has gone away" message. Re-establish the connection and retry the query once.

Code Example

php
<?php
namespace Framework\Database;

class Connection
{
    private ?\PDO $pdo = null;
    private array $queryLog = [];
    private bool $loggingEnabled = false;

    public function __construct(
        private readonly string $dsn,
        private readonly string $username,
        private readonly string $password,
        private readonly array  $options = [],
    ) {}

    public function getPdo(): \PDO
    {
        if ($this->pdo === null) {
            $this->pdo = $this->createPdo();
        }
        return $this->pdo;
    }

    public function select(string $sql, array $bindings = []): array
    {
        return $this->run($sql, $bindings, fn($stmt) => $stmt->fetchAll());
    }

    public function selectOne(string $sql, array $bindings = []): ?array
    {
        $result = $this->select($sql, $bindings);
        return $result[0] ?? null;
    }

    public function insert(string $sql, array $bindings = []): bool
    {
        return $this->run($sql, $bindings, fn($stmt) => $stmt->rowCount() > 0);
    }

    public function update(string $sql, array $bindings = []): int
    {
        return $this->run($sql, $bindings, fn($stmt) => $stmt->rowCount());
    }

    public function delete(string $sql, array $bindings = []): int
    {
        return $this->run($sql, $bindings, fn($stmt) => $stmt->rowCount());
    }

    public function statement(string $sql, array $bindings = []): bool
    {
        return $this->run($sql, $bindings, fn($stmt) => true);
    }

    public function lastInsertId(): string|false
    {
        return $this->getPdo()->lastInsertId();
    }

    private function run(string $sql, array $bindings, callable $callback): mixed
    {
        $start = microtime(true);
        $stmt  = $this->getPdo()->prepare($sql);
        $stmt->execute($bindings);
        $result = $callback($stmt);

        if ($this->loggingEnabled) {
            $this->queryLog[] = [
                'sql'      => $sql,
                'bindings' => $bindings,
                'time'     => round((microtime(true) - $start) * 1000, 2),
            ];
        }

        return $result;
    }

    private function createPdo(): \PDO
    {
        return new \PDO($this->dsn, $this->username, $this->password, array_merge([
            \PDO::ATTR_ERRMODE            => \PDO::ERRMODE_EXCEPTION,
            \PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_ASSOC,
            \PDO::ATTR_EMULATE_PREPARES   => false,
        ], $this->options));
    }

    public function enableQueryLog(): void { $this->loggingEnabled = true; }
    public function getQueryLog(): array   { return $this->queryLog; }
    public function flushQueryLog(): void  { $this->queryLog = []; }
}