Skip to content
Muhammet Şafak edited this page May 24, 2026 · 1 revision

FAQ

Short answers to questions that have come up more than once. For anything that doesn't fit here, open a discussion or an issue.

General

Can I use DBAL without the rest of the InitORM stack?

Yes. DBAL has no runtime dependencies beyond ext-pdo. The other InitORM packages (query-builder, database, orm) depend on DBAL — not the other way around.

Why PDO and not mysqli / pgsql-native / …?

Because PDO is the only first-party PHP abstraction that gives you the same surface across MySQL, PostgreSQL, SQLite, and a handful of other engines. DBAL leans on that uniformity rather than re-implementing it.

Will DBAL ever ship a query builder?

No. That is the deliberate scope of initorm/query-builder. DBAL only runs strings.

Is there an async / non-blocking version?

Not in this package. PDO is blocking by design. If you run inside Swoole/ReactPHP, look for the runtime's native driver — it will give you better throughput than wrapping PDO.

Connections

Why is no connection opened until I run a query?

Connection is lazy on purpose. Building a Connection is cheap; opening a TCP/SSL connection to the database is not. The lazy default lets you construct connections eagerly in your container without paying for the ones you don't use.

If you want to validate credentials at boot, call getPDO():

$db = new Connection([...]);
$db->getPDO();   // throws ConnectionException if anything is wrong

How do I switch databases at runtime?

$db->disconnect();
$db->setDatabase('other');
$db->getPDO();

setDatabase() throws while the connection is open — that's why the disconnect() call comes first. See Connection.

How do I run against a read replica for SELECTs?

clone() the primary connection, change the host, and route on the caller side:

$primary = new Connection($writeCfg);
$replica = $primary->clone()->setHost('replica.internal');

DBAL itself does not pick replicas automatically — that's a routing concern best handled by your application or a SQL-aware proxy (ProxySQL, PgBouncer).

Why is PDO::ATTR_PERSISTENT off by default in 2.x?

Persistent connections are a sharp tool. They survive across requests under most SAPIs, which means an aborted transaction leaks to the next request that picks the connection up. They also keep prepared-statement caches alive across migrations. See Transactions for the failure modes.

Can I supply a fully-formed DSN myself?

Yes — set the dsn credential and DBAL will not touch it:

new Connection([
    'dsn' => 'mysql:host=db;dbname=shop;charset=utf8mb4',
]);

This is how you reach drivers DBAL doesn't know about (Oracle, IBM DB/2, MS SQL …).

Queries

Why does numRows() lie on SELECT?

numRows() forwards to PDOStatement::rowCount(). The PHP manual says this clearly: "For most databases, PDOStatement::rowCount() does not return the number of rows affected by a SELECT statement."

Use a separate SELECT COUNT(*), or count rows() after the fetch.

Can I use positional ? placeholders?

Not through Connection::query() — DBAL is named-only. If you need positional binding, drop down to getPDO():

$stmt = $db->getPDO()->prepare('SELECT * FROM users WHERE id = ?');
$stmt->execute([$id]);

How do I bind an array (for IN (...))?

PDO does not support array binding. Build the placeholders yourself:

$ids   = [1, 2, 3];
$marks = implode(',', array_fill(0, count($ids), '?'));
$stmt  = $db->getPDO()->prepare("SELECT * FROM users WHERE id IN ($marks)");
$stmt->execute($ids);

Or, with named placeholders:

$ids    = [1, 2, 3];
$keys   = array_map(static fn(int $i) => ":id{$i}", array_keys($ids));
$params = array_combine($keys, $ids);
$db->query('SELECT * FROM users WHERE id IN (' . implode(',', $keys) . ')', $params);

A higher layer (a query builder) is a better place to abstract this.

How do I batch insert?

DBAL has no special API. Build a single multi-row statement:

$rows  = [['n' => 'a', 'e' => 'a@x'], ['n' => 'b', 'e' => 'b@x']];
$parts = [];
$args  = [];
foreach ($rows as $i => $r) {
    $parts[] = "(:n{$i}, :e{$i})";
    $args["n{$i}"] = $r['n'];
    $args["e{$i}"] = $r['e'];
}
$db->query('INSERT INTO users (name, email) VALUES ' . implode(',', $parts), $args);

Again, this is exactly the kind of thing initorm/query-builder exists to write for you.

Results

Why is rows() returning [] and not null?

2.x behaviour. The return type is non-nullable array. See Migration from 1.x.

How do I stream a large result set?

$mapper = $db->query('SELECT * FROM events ORDER BY id');
$mapper->asAssoc();
while (($row = $mapper->row()) !== null) {
    handle($row);
}
$mapper->closeCursor();

PDO buffers based on driver attributes. For MySQL, also consider PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => false in your options credential.

asClass() returns User objects but __construct sees pre-filled state?

Yes — that is how PDO::FETCH_CLASS works. Properties are written before the constructor runs. If your constructor needs the raw input, use FETCH_CLASS | FETCH_PROPS_LATE via the raw statement:

$stmt = $db->getPDO()->prepare('SELECT * FROM users');
$stmt->execute();
$users = $stmt->fetchAll(\PDO::FETCH_CLASS | \PDO::FETCH_PROPS_LATE, User::class);

Logging

Will the query log buffer grow forever?

Yes — it is an in-memory append-only list with no rotation. Drain it yourself in long-running workers, or leave the buffer off in production.

Why doesn't createLog() accept arbitrary levels?

It is a single entry point for critical failures. PSR-3 callers can log at any level themselves once they have the message. The minimal API keeps the credential resolution logic readable; richer loggers are better assembled in user code.

Testing

Should I mock Connection in unit tests?

Almost never. SQLite in-memory is fast enough (sub-millisecond), and real PDO behaviour catches binding bugs that mocks happily ignore. See Testing Your Application.

My tests pass on SQLite but fail on MySQL/PostgreSQL — why?

The dialects are different — see the table in Testing Your Application. Run an integration suite against the real engine in CI as well.

Versioning & compatibility

Which PHP versions are tested?

8.0, 8.1, 8.2, 8.3, 8.4 in CI. Older PHP is not supported and not tested.

Is the package SemVer?

Yes. Breaking changes wait for the next major. The 1.x → 2.x bump collected several breaking fixes; the 2.x line will be additive only.

How long is 1.x supported?

The 1.x branch is in maintenance-only mode. Security fixes are considered case-by-case for one minor cycle after 2.0 ships. Plan to upgrade.

Clone this wiki locally