-
Notifications
You must be signed in to change notification settings - Fork 0
FAQ
Answers to questions that come up repeatedly. Roughly grouped by topic.
No. The facade is one of two equally first-class styles. Skip it entirely if you prefer DI:
$db = new Database($cfg);
$repo = new UserRepository($db);See Static Facade and Multiple Connections for the trade-offs.
Because silent global overrides are a bug magnet. If you genuinely need to swap (typically in tests), DB::replaceImmutable($new) says so — the explicit name is the whole point. See Static Facade.
Because initorm/query-builder ^2.0 requires PHP 8.1. The Database package can't be more permissive than its dependencies. If you're on PHP 8.0, pin to initorm/database ^2.0 (the old major).
Because execution success and rows affected are different questions. true means "the SQL ran without error". To know how many rows changed, call affectedRows():
DB::update('users', ['active' => 0], ['id' => 1]);
echo DB::affectedRows(); // 0 if the row already had active=0, 1 if it was 1 beforeThis was a recurring bug in v2 — the old behaviour returned false for "no rows changed", which conflated execution failure with idempotent updates.
Intentional. The QueryBuilder doesn't refuse to compile a DELETE without WHERE — it just inserts a literal 1 to make the SQL syntactically complete. You are responsible for gating destructive operations. If you want to forbid them in your codebase, wrap the call:
function safeDelete(string $table, array $conditions): void
{
if (empty($conditions)) {
throw new \LogicException('Refusing to DELETE without conditions.');
}
DB::delete($table, $conditions);
}string|false. PDO's lastInsertId() returns the value as a string (because some drivers return non-integer keys), and false on drivers without an auto-incremented column. SQLite returns the rowid as a string; MySQL returns the AUTO_INCREMENT value as a string.
DB::create('users', $data);
$id = DB::insertId(); // "42"
$intId = (int) DB::insertId(); // 42Driver-dependent: PDOStatement::rowCount() is only reliable for INSERT / UPDATE / DELETE. For SELECT on unbuffered MySQL it returns 0; on SQLite for SELECT it also returns 0. For SELECT row counts, prefer count($result->rows()) or numRows() after a buffered fetch.
You probably hit the v2 state-pollution bug. In v3 this is fixed — every CRUD call resets the builder state in a finally block.
If you're on v3 and still seeing it, check that you're not sharing one Database across threads / coroutines (Database isn't thread-safe; spin up siblings with withFreshBuilder() per logical task).
$db->select('id')->from('users')->where('active', '=', 1);
echo $db->generateSelectQuery(); // SELECT id FROM users WHERE active = :active
print_r($db->getParameter()->all()); // [':active' => 1]generate*Query() does not reset the builder — only CRUD execution does. After inspecting, call $db->resetStructure() or a CRUD method to clear.
It doesn't. That was an outdated @method annotation in v2's facade. v3's facade is regenerated against the actual QueryBuilderInterface; the real signature is select(string|RawQuery ...$columns). If your IDE still autocompletes the old shape, restart it or clear its index.
Column names? Yes — they're identifier-quoted by the driver. But don't pass raw user input as a column unless you've validated against an allow-list. The builder will safely quote id or users.created_at, but it can't tell whether id; DROP TABLE users; was supposed to be a column. See Raw Queries for the rule.
Because MySQL (and most other databases) implicit-commit when they encounter a DDL statement like CREATE TABLE or ALTER TABLE. After that, rollBack() has nothing to roll back. Avoid mixing DDL into a retried transaction. See Transactions for the full caveat list.
Not natively — PDO doesn't support it. Starting a transaction inside another throws. Use savepoints (raw SQL) if you need per-section partial rollback:
$db->transaction(function ($db) {
$db->query('SAVEPOINT sp_audit');
try {
$db->create('audit', [...]);
} catch (\Throwable $e) {
$db->query('ROLLBACK TO sp_audit');
}
$db->query('RELEASE SAVEPOINT sp_audit');
});Whatever your database defaults to. The helper doesn't change it. To override, run SET TRANSACTION ISOLATION LEVEL … at the top of the closure, or configure it via options[PDO::MYSQL_ATTR_INIT_COMMAND].
By design. transaction() wraps the original throwable so callers always catch a single, predictable exception type. The original is reachable via getPrevious():
try {
$db->transaction(function () { throw new \RuntimeException('boom'); });
} catch (DatabaseException $e) {
$original = $e->getPrevious(); // \RuntimeException 'boom'
}In v2 the original was silently swallowed. The wrap-and-rethrow is a fix.
Failure log (log) |
Query profiler (queryLogs) |
|
|---|---|---|
| When | On error only | On every query |
| Where | External sink | In-process memory |
| Cost | Negligible | Per-query memory growth |
| Use case | Production error tracking | Dev profiling, test assertions |
They're independent — enable both, either, or neither. See Logging and Debug and Query Profiler.
Because the underlying call didn't pass a $startTime. The DBAL Connection always passes one when it executes through query(); if you see 0.0, you may be looking at an entry added manually via addQueryLog() (which is uncommon but allowed).
No — and intentionally. The whole point of prepared statements is that values never get spliced into the SQL string; they're sent to the server separately. The closest equivalent for debugging is logging query + args side-by-side; that's exactly what the profiler does.
Pass it directly — Monolog implements PSR-3's LoggerInterface, which has the critical(string $message) method the failure log expects:
DB::createImmutable([
'dsn' => '…',
'log' => $monolog,
]);See Logging and Debug for more PSR-3-friendly setups.
Because interface constructors are LSP-hostile: they force every implementation to accept the same arguments. The interface only declares behavioural methods. Implementations are free to choose their own constructor shape.
They're on QueryBuilderInterface. The Database forwards unknown method calls to its inner builder via __call. The @mixin QueryBuilderInterface annotation on DatabaseInterface tells IDEs / PHPStan to autocomplete the full builder surface. See Architecture.
initorm/database is a fluent SQL gateway. You query tables and get back arrays / objects.
initorm/orm builds on top of it: active-record models with $schema, $schemaId, $readable / $writable flags, hooks, accessors / mutators, soft-delete, timestamps, etc. Use the ORM when you want model classes; use Database directly when you want SQL.
Yes — the constructor takes one as the second argument:
new Database($cfg, $myFactory);This is the official DI hook. Useful for tests, custom dialect drivers, and decorator patterns.
Lazily — on the first call that needs it (a query(), getPDO(), or any setter that walks through getPDO()). Constructing a Database does not connect; you can hold an idle Database safely.
By design. Once PDO is open, mutating the credentials would be a silent inconsistency. Either build a fresh Database for the new credentials, or call disconnect() first:
$db->getConnection()->disconnect();
$db->getConnection()->setDatabase('analytics');No. Neither this package nor initorm/dbal pool connections — PDO is created on demand and held for the lifetime of the Connection object. For pooling:
- PHP-FPM keeps connections alive within a request (good enough for most).
- Set
PDO::ATTR_PERSISTENT => trueinoptionsfor cross-request persistence. - For Swoole / RoadRunner / ReactPHP, manage Database instances per worker.
- Architecture — for "how does X actually work?"
- Migration from v2 to v3 — for "did this change?"
- GitHub Issues — for new questions
InitORM Database · MIT · maintained by Muhammet ŞAFAK · part of the InitORM stack
Getting Started
Core Operations
Cross-Cutting
Reference
Upgrading
Project