Skip to content

Commit 2a7530d

Browse files
authored
Add ConnectionAdapterInterface and PdoConnectionAdapter (#20)
1 parent 1742631 commit 2a7530d

7 files changed

Lines changed: 479 additions & 154 deletions

File tree

src/ConnectionAdapterInterface.php

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Cog\DbLocker;
6+
7+
/**
8+
* Minimal connection abstraction for PostgreSQL advisory lock operations.
9+
*
10+
* This interface defines only the operations required by PostgresAdvisoryLocker.
11+
* It is NOT a general-purpose database abstraction layer.
12+
*
13+
* Exception Handling Contract:
14+
* - Implementations MUST throw the original database exception unwrapped.
15+
* - For PDO: throw \PDOException with SQLSTATE in getCode()
16+
* - For ORMs: throw their native exceptions (Doctrine\DBAL\Exception, etc.)
17+
* - The locker will inspect exceptions for SQLSTATE '55P03' (lock_not_available)
18+
*/
19+
interface ConnectionAdapterInterface
20+
{
21+
/**
22+
* Execute a parameterized query and return the first column of the first row.
23+
*
24+
* Used for lock acquisition functions that return boolean results:
25+
* - SELECT PG_TRY_ADVISORY_LOCK(:class_id, :object_id)
26+
* - SELECT PG_ADVISORY_UNLOCK(:class_id, :object_id)
27+
* - SHOW lock_timeout
28+
*
29+
* @param string $sql SQL query with named parameters (e.g., :class_id)
30+
* @param array<string, mixed> $params Named parameters (e.g., ['class_id' => 1, 'object_id' => 2])
31+
* @return mixed The value of the first column (typically bool or string)
32+
* @throws \Throwable Database-specific exception on error
33+
*/
34+
public function fetchColumn(string $sql, array $params = []): mixed;
35+
36+
/**
37+
* Execute a statement without returning results.
38+
*
39+
* Used for:
40+
* - Blocking lock acquisition: SELECT PG_ADVISORY_LOCK(:class_id, :object_id)
41+
* - Transaction control: SAVEPOINT, RELEASE SAVEPOINT, ROLLBACK TO SAVEPOINT
42+
* - Configuration: SET [LOCAL] lock_timeout = '...'
43+
* - Cleanup: SELECT PG_ADVISORY_UNLOCK_ALL()
44+
*
45+
* @param string $sql SQL statement with optional named parameters
46+
* @param array<string, mixed> $params Named parameters (e.g., ['class_id' => 1])
47+
* @throws \Throwable Database-specific exception on error
48+
*/
49+
public function execute(string $sql, array $params = []): void;
50+
51+
/**
52+
* Check whether a transaction is currently active on this connection.
53+
*
54+
* Used to validate that transaction-level locks are only acquired within a transaction.
55+
*
56+
* @return bool True if a transaction is active, false otherwise
57+
*/
58+
public function isTransactionActive(): bool;
59+
60+
/**
61+
* Check if an exception indicates a PostgreSQL lock_not_available error (SQLSTATE 55P03).
62+
*
63+
* Different database adapters throw different exception types:
64+
* - PDO: \PDOException with SQLSTATE in getCode()
65+
* - Doctrine DBAL: Doctrine\DBAL\Exception with getSQLState() method
66+
* - Cycle ORM: wraps \PDOException in its own exception
67+
*
68+
* @param \Throwable $exception Exception to inspect
69+
* @return bool True if the exception indicates a lock timeout (SQLSTATE 55P03)
70+
*/
71+
public function isLockNotAvailable(\Throwable $exception): bool;
72+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Cog\DbLocker\DbConnection;
6+
7+
use Cog\DbLocker\ConnectionAdapterInterface;
8+
use PDO;
9+
10+
/**
11+
* PDO-based implementation of ConnectionAdapterInterface.
12+
*
13+
* This adapter wraps a PDO connection and provides the minimal interface
14+
* required by PostgresAdvisoryLocker.
15+
*
16+
* Requirements:
17+
* - PDO connection MUST use PDO::ERRMODE_EXCEPTION
18+
* - Constructor validates this requirement and throws LogicException if violated
19+
*/
20+
final class PdoConnectionAdapter implements ConnectionAdapterInterface
21+
{
22+
private const PG_SQLSTATE_LOCK_NOT_AVAILABLE = '55P03';
23+
24+
public function __construct(
25+
private readonly PDO $pdo,
26+
) {
27+
if ((int) $this->pdo->getAttribute(PDO::ATTR_ERRMODE) !== PDO::ERRMODE_EXCEPTION) {
28+
throw new \LogicException(
29+
'PDO connection must use PDO::ERRMODE_EXCEPTION. '
30+
. 'Set it via: $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION)',
31+
);
32+
}
33+
}
34+
35+
public function fetchColumn(string $sql, array $params = []): mixed
36+
{
37+
$statement = $this->pdo->prepare($sql);
38+
$statement->execute($params);
39+
return $statement->fetchColumn(0);
40+
}
41+
42+
public function execute(string $sql, array $params = []): void
43+
{
44+
if ($params === []) {
45+
// exec() is more efficient for non-parameterized queries
46+
$this->pdo->exec($sql);
47+
return;
48+
}
49+
50+
$statement = $this->pdo->prepare($sql);
51+
$statement->execute($params);
52+
}
53+
54+
public function isTransactionActive(): bool
55+
{
56+
return $this->pdo->inTransaction();
57+
}
58+
59+
public function isLockNotAvailable(\Throwable $exception): bool
60+
{
61+
// PDOException: getCode() returns SQLSTATE string
62+
if ($exception instanceof \PDOException) {
63+
return $exception->getCode() === self::PG_SQLSTATE_LOCK_NOT_AVAILABLE;
64+
}
65+
66+
return false;
67+
}
68+
}

src/Postgres/LockHandle/SessionLevelLockHandle.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,10 @@
1313

1414
namespace Cog\DbLocker\Postgres\LockHandle;
1515

16+
use Cog\DbLocker\ConnectionAdapterInterface;
1617
use Cog\DbLocker\Postgres\Enum\PostgresLockAccessModeEnum;
1718
use Cog\DbLocker\Postgres\PostgresAdvisoryLocker;
1819
use Cog\DbLocker\Postgres\PostgresLockKey;
19-
use PDO;
2020

2121
/**
2222
* @internal
@@ -26,7 +26,7 @@ final class SessionLevelLockHandle
2626
private bool $isReleased = false;
2727

2828
public function __construct(
29-
private readonly PDO $dbConnection,
29+
private readonly ConnectionAdapterInterface $dbConnection,
3030
private readonly PostgresAdvisoryLocker $locker,
3131
public readonly PostgresLockKey $lockKey,
3232
public readonly PostgresLockAccessModeEnum $accessMode,

0 commit comments

Comments
 (0)