Skip to content

Commit 4c1af4c

Browse files
authored
Merge pull request #145 from WebFiori/dev
Dev
2 parents 38e84b7 + 8108546 commit 4c1af4c

9 files changed

Lines changed: 767 additions & 154 deletions

File tree

WebFiori/Database/Connection.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,18 @@ public function getLastResultSet() {
150150
return $this->resultSet;
151151
}
152152
public abstract function rollBack(?string $name = null);
153+
/**
154+
* Close the database connection and release resources.
155+
*
156+
* After calling this method, the connection should not be used for queries.
157+
*/
158+
public abstract function close(): void;
159+
/**
160+
* Check if the connection is still alive and usable.
161+
*
162+
* @return bool True if the connection is active and can execute queries.
163+
*/
164+
public abstract function isAlive(): bool;
153165
/**
154166
* Sets the last query and execute it.
155167
*
Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
<?php
2+
3+
/**
4+
* This file is licensed under MIT License.
5+
*
6+
* Copyright (c) 2025-present WebFiori Framework
7+
*
8+
* For more information on the license, please visit:
9+
* https://github.com/WebFiori/.github/blob/main/LICENSE
10+
*
11+
*/
12+
namespace WebFiori\Database;
13+
14+
use WebFiori\Database\MySql\MySQLConnection;
15+
use WebFiori\Database\MsSql\MSSQLConnection;
16+
17+
/**
18+
* A connection pool that manages database connection lifecycle.
19+
*
20+
* The pool reuses idle connections instead of creating new ones,
21+
* preventing "Too many connections" errors and reducing overhead
22+
* from repeated connection handshakes.
23+
*
24+
* Usage:
25+
* ```php
26+
* $pool = ConnectionPool::getInstance();
27+
* $conn = $pool->acquire($connectionInfo);
28+
* // ... use connection ...
29+
* $pool->release($conn);
30+
* ```
31+
*
32+
* @author Ibrahim
33+
*/
34+
class ConnectionPool {
35+
private static ?ConnectionPool $instance = null;
36+
37+
/** @var array<string, Connection[]> */
38+
private array $idle = [];
39+
40+
/** @var array<string, Connection[]> */
41+
private array $active = [];
42+
43+
private int $maxPerKey = 10;
44+
private int $maxTotal = 100;
45+
46+
private function __construct() {
47+
}
48+
49+
/**
50+
* Returns the singleton pool instance.
51+
*
52+
* @return ConnectionPool
53+
*/
54+
public static function getInstance(): self {
55+
if (self::$instance === null) {
56+
self::$instance = new self();
57+
}
58+
return self::$instance;
59+
}
60+
61+
/**
62+
* Acquire a connection for the given connection info.
63+
*
64+
* If an idle connection exists for the same host/port/db/user combination,
65+
* it will be reused. Otherwise, a new connection is created.
66+
*
67+
* @param ConnectionInfo $info Connection parameters.
68+
*
69+
* @return Connection A ready-to-use database connection.
70+
*
71+
* @throws DatabaseException If the pool is exhausted or connection fails.
72+
*/
73+
public function acquire(ConnectionInfo $info): Connection {
74+
$key = $this->buildKey($info);
75+
76+
// Try to reuse an idle connection
77+
while (!empty($this->idle[$key])) {
78+
$conn = array_pop($this->idle[$key]);
79+
80+
if ($conn->isAlive()) {
81+
$this->active[$key][] = $conn;
82+
return $conn;
83+
}
84+
85+
// Dead connection, discard
86+
$conn->close();
87+
}
88+
89+
// Check total limit
90+
if ($this->getActiveCount() >= $this->maxTotal) {
91+
throw new DatabaseException(
92+
"Connection pool exhausted (max: {$this->maxTotal})"
93+
);
94+
}
95+
96+
// Create new connection
97+
$conn = $this->createConnection($info);
98+
$this->active[$key][] = $conn;
99+
return $conn;
100+
}
101+
102+
/**
103+
* Release a connection back to the pool for reuse.
104+
*
105+
* @param Connection $conn The connection to release.
106+
*/
107+
public function release(Connection $conn): void {
108+
$key = $this->buildKey($conn->getConnectionInfo());
109+
110+
// Remove from active
111+
if (isset($this->active[$key])) {
112+
$index = array_search($conn, $this->active[$key], true);
113+
114+
if ($index !== false) {
115+
unset($this->active[$key][$index]);
116+
$this->active[$key] = array_values($this->active[$key]);
117+
}
118+
}
119+
120+
// Return to idle if under per-key limit
121+
$idleCount = count($this->idle[$key] ?? []);
122+
123+
if ($idleCount < $this->maxPerKey && $conn->isAlive()) {
124+
$this->idle[$key][] = $conn;
125+
} else {
126+
$conn->close();
127+
}
128+
}
129+
130+
/**
131+
* Close all connections (idle and active) and drain the pool.
132+
*/
133+
public function closeAll(): void {
134+
foreach ($this->idle as $connections) {
135+
foreach ($connections as $conn) {
136+
$conn->close();
137+
}
138+
}
139+
140+
foreach ($this->active as $connections) {
141+
foreach ($connections as $conn) {
142+
$conn->close();
143+
}
144+
}
145+
146+
$this->idle = [];
147+
$this->active = [];
148+
}
149+
150+
/**
151+
* Set the maximum number of connections per unique key (host+port+db+user).
152+
*
153+
* @param int $max Maximum idle connections per key.
154+
*/
155+
public function setMaxPerKey(int $max): void {
156+
if ($max > 0) {
157+
$this->maxPerKey = $max;
158+
}
159+
}
160+
161+
/**
162+
* Set the maximum total number of active connections across all keys.
163+
*
164+
* @param int $max Maximum total active connections.
165+
*/
166+
public function setMaxTotal(int $max): void {
167+
if ($max > 0) {
168+
$this->maxTotal = $max;
169+
}
170+
}
171+
172+
/**
173+
* Returns the maximum number of idle connections per key.
174+
*
175+
* @return int
176+
*/
177+
public function getMaxPerKey(): int {
178+
return $this->maxPerKey;
179+
}
180+
181+
/**
182+
* Returns the maximum total active connections allowed.
183+
*
184+
* @return int
185+
*/
186+
public function getMaxTotal(): int {
187+
return $this->maxTotal;
188+
}
189+
190+
/**
191+
* Returns the number of currently active (in-use) connections.
192+
*
193+
* @return int
194+
*/
195+
public function getActiveCount(): int {
196+
$count = 0;
197+
198+
foreach ($this->active as $connections) {
199+
$count += count($connections);
200+
}
201+
202+
return $count;
203+
}
204+
205+
/**
206+
* Returns the number of currently idle (available) connections.
207+
*
208+
* @return int
209+
*/
210+
public function getIdleCount(): int {
211+
$count = 0;
212+
213+
foreach ($this->idle as $connections) {
214+
$count += count($connections);
215+
}
216+
217+
return $count;
218+
}
219+
220+
/**
221+
* Reset the pool singleton. Closes all connections and destroys the instance.
222+
* Primarily useful for testing.
223+
*/
224+
public static function reset(): void {
225+
if (self::$instance !== null) {
226+
self::$instance->closeAll();
227+
}
228+
self::$instance = null;
229+
}
230+
231+
private function buildKey(ConnectionInfo $info): string {
232+
return $info->getHost() . ':' . $info->getPort() . '/'
233+
. $info->getDBName() . '@' . $info->getUsername();
234+
}
235+
236+
private function createConnection(ConnectionInfo $info): Connection {
237+
$driver = $info->getDatabaseType();
238+
239+
return match ($driver) {
240+
'mysql' => new MySQLConnection($info),
241+
'mssql' => new MSSQLConnection($info),
242+
default => throw new DatabaseException("Unsupported driver: $driver"),
243+
};
244+
}
245+
}

WebFiori/Database/Database.php

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

1414
use Exception;
1515
use WebFiori\Database\Factory\TableFactory;
16-
use WebFiori\Database\MsSql\MSSQLConnection;
1716
use WebFiori\Database\MsSql\MSSQLQuery;
1817
use WebFiori\Database\MsSql\MSSQLTable;
19-
use WebFiori\Database\MySql\MySQLConnection;
2018
use WebFiori\Database\MySql\MySQLQuery;
2119
use WebFiori\Database\MySql\MySQLTable;
2220
use WebFiori\Database\Performance\PerformanceOption;
@@ -119,6 +117,12 @@ public function __construct(?ConnectionInfo $connectionInfo) {
119117
'message' => ''
120118
];
121119
}
120+
/**
121+
* Release the connection back to the pool when this object is destroyed.
122+
*/
123+
public function __destruct() {
124+
$this->close();
125+
}
122126
/**
123127
* Adds a database query to the set of queries at which they were executed.
124128
*
@@ -417,15 +421,7 @@ public function getConnection() : ?Connection {
417421
$connInfo = $this->getConnectionInfo();
418422

419423
if ($this->connection === null && $connInfo !== null) {
420-
$driver = $connInfo->getDatabaseType();
421-
422-
if ($driver == 'mysql') {
423-
$conn = new MySQLConnection($connInfo);
424-
$this->setConnection($conn);
425-
} else if ($driver == 'mssql') {
426-
$conn = new MSSQLConnection($connInfo);
427-
$this->setConnection($conn);
428-
}
424+
$this->connection = ConnectionPool::getInstance()->acquire($connInfo);
429425
}
430426

431427
return $this->connection;
@@ -830,6 +826,19 @@ public function select(array $cols = ['*']) : AbstractQuery {
830826
public function setConnection(Connection $con) {
831827
$this->connection = $con;
832828
}
829+
/**
830+
* Release the current connection back to the pool.
831+
*
832+
* After calling this method, the connection is returned to the pool
833+
* for reuse by other Database instances. The next call to getConnection()
834+
* will acquire a new connection from the pool.
835+
*/
836+
public function close(): void {
837+
if ($this->connection !== null) {
838+
ConnectionPool::getInstance()->release($this->connection);
839+
$this->connection = null;
840+
}
841+
}
833842
/**
834843
* Sets database connection information.
835844
*

WebFiori/Database/MsSql/MSSQLConnection.php

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,37 @@ public function __construct(ConnectionInfo $connInfo) {
4949
*
5050
*/
5151
public function __destruct() {
52-
sqlsrv_close($this->link);
52+
$this->close();
53+
}
54+
55+
/**
56+
* Close the MSSQL connection and release resources.
57+
*/
58+
public function close(): void {
59+
if ($this->link !== null) {
60+
@sqlsrv_close($this->link);
61+
$this->link = null;
62+
}
63+
}
64+
65+
/**
66+
* Check if the MSSQL connection is still alive.
67+
*
68+
* @return bool True if the connection is active.
69+
*/
70+
public function isAlive(): bool {
71+
if ($this->link === null) {
72+
return false;
73+
}
74+
75+
$result = @sqlsrv_query($this->link, 'SELECT 1');
76+
77+
if ($result !== false) {
78+
sqlsrv_free_stmt($result);
79+
return true;
80+
}
81+
82+
return false;
5383
}
5484
/**
5585
* Starts SQL server transaction.

WebFiori/Database/MySql/MySQLConnection.php

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,26 @@ public function __construct(ConnectionInfo $connInfo) {
6363
* Close database connection.
6464
*/
6565
public function __destruct() {
66-
mysqli_close($this->link);
66+
$this->close();
67+
}
68+
69+
/**
70+
* Close the MySQL connection and release resources.
71+
*/
72+
public function close(): void {
73+
if ($this->link !== null) {
74+
@mysqli_close($this->link);
75+
$this->link = null;
76+
}
77+
}
78+
79+
/**
80+
* Check if the MySQL connection is still alive.
81+
*
82+
* @return bool True if the connection is active.
83+
*/
84+
public function isAlive(): bool {
85+
return $this->link !== null && @$this->link->ping();
6786
}
6887

6988
public function beginTransaction(?string $name = null) {

0 commit comments

Comments
 (0)