Database
A razor-thin PDO wrapper that gives every MonkeysLegion package a fast, consistent way to talk to MySQL 8.4+.
It handles DSN construction, lazy connection, automatic reconnection, and convenience helpers that keep raw SQL readable.
1 · Installation
composer require monkeyscloud/monkeyslegion-database
Nothing else to install—just PHP 8.4 and the native pdo_mysql extension.
2 · Creating a Connection
use MonkeysLegion\Database\Connection;
$config = [
'host' => '127.0.0.1',
'port' => 3306,
'dbname' => 'monkeys_app',
'user' => 'root',
'pass' => '',
// optional
'charset' => 'utf8mb4',
'options' => [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
],
];
$conn = new Connection($config); // lazy-connects on first query
Config keys
Key | Required | Default | Notes |
---|---|---|---|
host, dbname, user | ✓ | n/a | Standard MySQL creds |
pass | ✓ | '' | Empty string for unix-socket auth |
port | ✗ | 3306 | |
charset | ✗ | utf8mb4 | Sets SET NAMES … after connect |
options | ✗ | reasonable PDO defaults | Merge/override freely |
3 · Everyday API
Method | What it does |
---|---|
pdo() | Returns the underlying PDO instance (for libraries that need raw access) |
run(string $sql, array $params = []) | Prepares & executes, returns PDOStatement |
fetchAll(string $sql, array $params = [], ?string $into = null) | Runs and hydrates rows; pass a class name to hydrate objects |
fetchOne(...) | Same as fetchAll but returns a single row / object or null |
column(...) | Shorthand for SELECT col FROM … LIMIT 1 |
All placeholders are bound by name (:id, :slug, …) to eliminate SQL injection mistakes.
4 · Transactions
$conn->pdo()->beginTransaction();
try {
$conn->run('INSERT INTO orders ...', $orderData);
$conn->run('INSERT INTO invoices ...', $invoiceData);
$conn->pdo()->commit();
} catch (\Throwable $e) {
$conn->pdo()->rollBack();
throw $e; // bubble up for logging / 500 handler
}
Because QueryBuilder and EntityRepository both accept a Connection, their writes automatically participate in the same transaction.
5 · Convenience Helpers
// Auto-reconnect after “MySQL server has gone away”
$conn->ping(); // returns true if connection is healthy
// Dump last statement for debugging
$sql = $conn->lastQuery(); // e.g. "SELECT * FROM users WHERE id = ?"
// Simple pagination helper
[$rows, $total] = $conn->paginate(
'SELECT * FROM posts WHERE status = :status',
['status' => 'published'],
page: 3,
perPage: 20,
);
Exact helper names may evolve; check the doc-blocks in Connection.php for the definitive list.
6 · Integrating with DI
return [
// Share one Connection across the app
MonkeysLegion\Database\Connection::class => fn() =>
new Connection(require base_path('config/database.php')),
];
Now anything that type-hints Connection—QueryBuilder, repositories, custom services—gets the same pooled PDO instance.
7 · Working with QueryBuilder
use MonkeysLegion\Query\QueryBuilder;
/** @var QueryBuilder $qb */
$qb = new QueryBuilder($conn);
$posts = $qb->select('*')
->from('posts')
->where('author_id', '=', 42)
->orderBy('published_at', 'DESC')
->limit(10)
->fetchAll(App\Entity\Post::class); // hydrated objects
8 · Extending
8.1 · Query-level events (metrics / logging)
Connection exposes a simple event system: register any callable against beforeQuery or afterQuery and it will fire for every run()/fetch*() call.
use MonkeysLegion\Database\Connection;
// bootstrap
$conn->on('beforeQuery', static function (string $sql, array $params): float {
return microtime(true); // return something to pass to afterQuery
});
$conn->on('afterQuery', static function (string $sql, array $params, $start) {
$t = (microtime(true) - $start) * 1000;
printf("[db] %.1f ms – %s\n", $t, $sql);
});
Emit to stdout (like above), push to Prometheus, or dispatch to any PSR-3 logger.
8.2 · Custom fetch modes (DTOs / JSON columns)
Need objects that aren’t just mapped rows? Wrap fetchAll() in a trait or helper:
trait JsonAwareFetch
{
public function fetchAllJson(string $sql, array $params = []): array
{
return array_map(
fn ($row) => new UserSummary(
id: (int) $row['id'],
name: $row['name'],
profile: json_decode($row['profile_json'], true, flags: JSON_THROW_ON_ERROR),
),
parent::fetchAll($sql, $params) // <- original method
);
}
}
final class MyConnection extends Connection
{
use JsonAwareFetch;
}
// usage
$users = $conn->fetchAllJson('SELECT * FROM users');
Because the wrapper calls parent::fetchAll(), you still benefit from named-parameter binding and automatic reconnection.
8.3 · Read/Write splitting
For high-traffic apps you can point writes to the primary but reads to replicas by overriding pdo():
final class SplitConnection extends Connection
{
private ?PDO $reader = null;
protected function pdo(bool $write = true): PDO
{
if ($write) { // INSERT/UPDATE/DELETE
return parent::pdo(true);
}
// lazy-build replica connection
return $this->reader ??= new PDO(
'mysql:host=replica-1;dbname=monkeys_app;charset=utf8mb4',
'app', 'secret',
$this->options
);
}
/** Select helpers call the read pool */
public function fetchAll(string $sql, array $params = [], ?string $into = null): array
{
$stmt = $this->pdo(false)->prepare($sql);
$stmt->execute($params);
return $into ? $stmt->fetchAll(PDO::FETCH_CLASS, $into) : $stmt->fetchAll();
}
}
Override fetchOne(), column(), and any other read helpers the same way. All writes (run(), insertId() …) continue to hit the primary.
License
MIT © 2025 MonkeysCloud