Composes initorm/dbal (PDO connection + result mapper) and initorm/query-builder (fluent SQL builder) into a single Database manager with CRUD helpers, transactions, query logging, and an optional static facade.
- PHP 8.1 or later
ext-pdo- One of
ext-pdo_mysql,ext-pdo_pgsql, orext-pdo_sqlitedepending on the database you target.
Any database with a PDO driver that follows standard SQL works out of the box. The query builder ships dialect-aware identifier quoting for MySQL/MariaDB, PostgreSQL, and SQLite; for everything else (oci, sqlsrv, …) it falls back to a generic, no-escaping driver.
composer require initorm/database<?phprequire_once'vendor/autoload.php';
useInitORM\Database\Facade\DB;
DB::createImmutable([
'dsn' => 'mysql:host=localhost;port=3306;dbname=app;charset=utf8mb4',
'username' => 'app',
'password' => 'secret',
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
]);
// CRUD shortcutDB::create('users', ['name' => 'Alice', 'email' => 'alice@example.com']);
// Fluent builder + CRUD$rows = DB::select('id', 'name')
->where('active', '=', 1)
->orderBy('id', 'DESC')
->limit(10)
->read('users')
->asAssoc()
->rows();
createImmutable()sets the application-wide facade once. Calling it a second time throws — seedocs/10-facade-vs-instance.mdfor swap and multi-connection patterns.
All keys are passed through to the underlying InitORM\DBAL\Connection\Connection constructor. Defaults are sane for MySQL.
| Key | Type | Default | Notes |
|---|---|---|---|
dsn | string | (built) | When empty, a DSN is constructed from driver, host, port, database, charset. |
driver | string | 'mysql' | mysql, pgsql/postgres/postgresql, sqlite, or any PDO driver name. |
host | string | '127.0.0.1' | Ignored when dsn is set explicitly. |
port | int|string | 3306 | Ignored when dsn is set explicitly. |
database | string | '' | For SQLite use ':memory:' or a file path. |
username | string|null | null | |
password | string|null | null | |
charset | string | 'utf8mb4' | Applied on MySQL via SET NAMES. Pass '' to skip (e.g. SQLite). |
collation | string|null | null | MySQL-only. Validated against [A-Za-z0-9_] before interpolation. |
options | array<int, mixed> | [] | Merged on top of safe PDO defaults (exceptions on errors, FETCH_ASSOC, no emulation). |
queryOptions | array<int, mixed> | [] | PDO prepare() options used for every statement. |
log | string|callable|object|null | null | See Logger. File path, callable, or any object with a critical(string) method. |
debug | bool | false | When true, query failure messages also include the bound parameters (JSON-encoded). |
queryLogs | bool | false | Bootstrap value for the query log buffer (see Query log). |
All CRUD helpers reset the builder's state on completion, so the next call starts with a clean slate. Every helper returns bool true on successful execution and throws on failure — use affectedRows() when you also need to know how many rows changed.
useInitORM\Database\Facade\DB;
DB::create('posts', [
'title' => 'Post Title',
'content' => 'Post Content',
]);
$newId = DB::insertId();Generated SQL: INSERT INTO posts (title, content) VALUES (:title, :content)
DB::createBatch('posts', [
['title' => 'Post #1', 'content' => 'Body 1', 'author_id' => 5],
['title' => 'Post #2', 'content' => 'Body 2'],
]);Generated SQL: INSERT INTO posts (title, content, author_id) VALUES (:title, :content, :author_id), (:title_1, :content_1, NULL)
Missing columns in any row compile to NULL.
$res = DB::select('user.name AS author_name', 'post.id', 'post.title')
->from('post')
->selfJoin('user', 'user.id=post.author')
->where('post.status', '=', 1)
->orderBy('post.id', 'ASC')
->orderBy('post.created_at', 'DESC')
->offset(20)
->limit(10)
->read();
foreach ($res->asAssoc()->rows() as$row) {
echo$row['title'] . ' by ' . $row['author_name'] . PHP_EOL;
}DB::update('post', ['title' => 'New Title', 'content' => 'New Content'], ['id' => 13]);Generated SQL: UPDATE post SET title = :title, content = :content WHERE id = :id
DB::where('status', '!=', 0)
->updateBatch('id', 'post', [
['id' => 5, 'title' => 'New Title #5', 'content' => 'New Content #5'],
['id' => 10, 'title' => 'New Title #10'],
]);Generated SQL (formatted):
UPDATE post SET
title = CASE WHEN id = :id THEN :title WHEN id = :id_1 THEN :title_1 ELSE title END,
content = CASE WHEN id = :id_2 THEN :content ELSE content END
WHERE status != :status AND id IN (:id_3, :id_4)DB::delete('post', ['id' => 13]);Generated SQL: DELETE FROM post WHERE id = :id
DB::update('users', ['active' => 0], ['active' => 1]);
echoDB::affectedRows(); // e.g. 42affectedRows() returns the row count of the most recent CRUD call on the same Database instance.
$res = DB::query(
'SELECT id, title FROM post WHERE user_id = :id',
[':id' => 5]
);
if ($res->numRows() > 0) {
$result = $res->asObject()->row();
echo$result->title;
}You can also use DB::raw() inside the builder to inject literal SQL fragments — never embed unsanitized user input:
$res = DB::select(DB::raw("CONCAT(name, ' ', surname) AS fullname"))
->where(DB::raw('status = 1 OR status = 0'))
->limit(5)
->read('users');DB::transaction(function (\InitORM\Database\Interfaces\DatabaseInterface$db) {
$db->create('orders', ['user_id' => 5, 'total' => 199.90]);
$db->create('order_items',['order_id' => $db->insertId(), 'sku' => 'X', 'qty' => 1]);
});- The closure receives the Database instance.
- Throw to abort: the current transaction is rolled back; if
$attempt > 1the closure is retried; otherwise the original error is rethrown wrapped in aDatabaseException(the original is reachable via$e->getPrevious()). - Pass
testMode: trueto roll back even on success — useful for integration tests.
$caught = null;
try {
DB::transaction(function ($db) {
$db->create('orders', [...]);
thrownew \RuntimeException('boom');
});
} catch (\InitORM\Database\Exceptions\DatabaseException$e) {
$caught = $e->getPrevious(); // \RuntimeException 'boom'
}DB::createImmutable() registers a single shared facade. For secondary connections, use DB::connect() or instantiate Database directly — these do not touch the facade slot.
useInitORM\Database\Database;
$reports = newDatabase([
'dsn' => 'pgsql:host=reports.internal;dbname=reports',
'username' => 'reports_ro',
'password' => '…',
'driver' => 'pgsql',
]);
$reports->read('events')->asAssoc()->rows();If you must swap the immutable facade target (rare; mostly for tests), call DB::replaceImmutable($next) explicitly — silent overrides are forbidden.
The log credential accepts three shapes — a file path, a callable, or any object with a critical(string) method. The DBAL Logger writes a single string message per failed query, prefixed with the SQL and (when debug is on) the bound parameters.
// 1) File path — file_put_contents() with appendDB::createImmutable([
'dsn' => 'mysql:host=localhost;dbname=app;charset=utf8mb4',
'log' => __DIR__ . '/var/log/db-{year}-{month}-{day}.log',
]);
// 2) CallableDB::createImmutable([
'dsn' => 'mysql:host=localhost;dbname=app;charset=utf8mb4',
'log' => function (string$msg): void {
error_log($msg);
},
]);
// 3) Object with critical() (or a [$obj, 'method'] callable)class Logger {
publicfunctioncritical(string$msg): void { /* … */ }
}
DB::createImmutable([
'dsn' => 'mysql:host=localhost;dbname=app;charset=utf8mb4',
'log' => newLogger(),
]);DB::createImmutable([
'dsn' => 'mysql:host=localhost;dbname=app;charset=utf8mb4',
'debug' => true, // include bound parameters in failure messages
]);Enable in development only — bound parameter dumps can include credentials and PII.
DB::enableQueryLog();
DB::read('users', ['id', 'name'], ['active' => 1]);
var_dump(DB::getQueryLogs());
/*[ [ 'query' => 'SELECT id, name FROM users WHERE active = :active', 'args' => [':active' => 1], 'timer' => 0.000642, ],]*/enableQueryLog() / disableQueryLog() return the Database instance for chaining; getQueryLogs() returns every recorded entry. The buffer lives on the Connection — calling disableQueryLog() stops recording but does not clear previously-collected entries.
In-depth, code-first guides live under docs/:
01-getting-started.md02-configuration.md03-crud.md04-query-builder.md05-transactions.md06-raw-queries.md07-multiple-connections.md08-logger-and-debug.md09-query-log-profiler.md10-facade-vs-instance.md11-architecture.md12-upgrade-guide.md— migrating from v2 to v3
Contributions are welcome. The general flow is:
- Fork and branch off
master. - Add tests for the behaviour you change — see
tests/for patterns (SQLite in-memory, fast and dependency-free). - Run the full quality suite locally:
composer qa # phpcs + phpstan + phpunit - Open a PR — CI will run the same suite across PHP 8.1–8.4.
By submitting a contribution you agree to license it under the MIT License.
- Muhammet ŞAFAK —
<info@muhammetsafak.com.tr>
Released under the MIT License.