Connecting & Adapters
A database connection in Pop is a single adapter object. Pop\Db\Db builds one from an array of
credentials, and the query builder, the schema builder and Pop\Db\Record all take that object. Five
adapters implement one shared interface, so what changes between them is the extension required, the options
accepted, and what you catch on failure.
Connecting#
Db::connect() takes the adapter name and the credentials:
use Pop\Db\Db;
$db = Db::connect('mysql', [
'database' => 'popdb',
'username' => 'popuser',
'password' => 'db_password',
'host' => '127.0.0.1'
]);
The name is lower-cased and capitalized into a class under Pop\Db\Adapter, so 'mysql' resolves to
Pop\Db\Adapter\Mysql. A name with no matching class throws Pop\Db\Exception before anything is
opened: Error: The database adapter \Pop\Db\Adapter\Bogus does not exist.
Five shorthand methods skip the adapter string. They take the options array and nothing else:
use Pop\Db\Db;
$db = Db::mysqlConnect(['database' => 'popdb', 'username' => 'popuser', 'password' => 'db_password']);
$db = Db::pgsqlConnect(['database' => 'popdb', 'username' => 'postgres', 'password' => 'db_password']);
$db = Db::sqliteConnect(['database' => '/var/data/my_database.sqlite']);
$db = Db::sqlsrvConnect(['database' => 'popdb', 'username' => 'sa', 'password' => 'secret']);
$db = Db::pdoConnect(['type' => 'mysql', 'database' => 'popdb', 'username' => 'popuser', 'password' => 'db_password']);
host defaults to localhost on every adapter that uses one. The connection is opened in the
adapter's constructor, so a bad password fails at the Db::connect() line rather than at the first
query — but what you catch is not the same on all five, and code that only catches
Pop\Db\Adapter\Exception will miss two of them.
| Adapter | A failed connection surfaces as |
|---|---|
| MySQL | mysqli_sql_exception from the driver: Access denied for user 'popuser'@'localhost' (using password: YES). The adapter's own error handling never runs, because mysqli throws first |
| PostgreSQL | Pop\Db\Adapter\Exception, always with the same generic text: PostgreSQL Connection Error: Unable to connect to the database. The actual reason appears only in a PHP warning raised by pg_connect() |
| SQLite | Pop\Db\Adapter\Exception naming the file: Error: The database file '/var/data/my_database.sqlite' does not exist. |
| SQL Server | Pop\Db\Adapter\Exception,always with the same generic text: SQL Server Connection Error: Unable to connect to the database. |
| PDO | Pop\Db\Adapter\Exception carrying the driver's own message: PDO Connection Error: SQLSTATE[HY000] [1045] Access denied for user 'popuser'@'localhost' (using password: YES) (#1045) |
Every adapter also rejects an incomplete options array up front with
Error: The proper database credentials were not passed., before it tries to reach the server.
When you would rather test a connection than open one, Db::check() returns true on success and the
error message as a string on failure. It catches the driver exception for you, which makes it the
right call for a startup gate:
use Pop\Db\Db;
$options = [
'database' => 'popdb',
'username' => 'popuser',
'password' => 'db_password',
'host' => '127.0.0.1'
];
$check = Db::check('mysql', $options);
if ($check !== true) {
throw new \Pop\Db\Adapter\Exception('Error: ' . $check);
}
Db::check() does not protect you from a missing extension either — it catches \Exception, and a
missing driver raises an \Error. Db::check('sqlsrv', $options) on a machine with no sqlsrv
extension throws Error: Call to undefined function Pop\Db\Adapter\sqlsrv_connect() straight through.
Db::isAvailable() is the guard for that question, and Db::getAvailableAdapters() returns the whole
map at once:
use Pop\Db\Db;
var_dump(Db::isAvailable('mysql')); // true where the mysqli extension is loaded
var_dump(Db::isAvailable('sqlsrv')); // false where the sqlsrv extension is not
var_dump(Db::isAvailable('pdo_pgsql')); // PDO drivers take a pdo_ prefix
print_r(Db::getAvailableAdapters());
MySQL#
Pop\Db\Adapter\Mysql wraps the mysqli class from the mysqli extension.
| Option | |
|---|---|
database |
required |
username |
required |
password |
required |
host |
optional, defaults to localhost |
port |
optional, defaults to the mysqli.default_port ini setting |
socket |
optional, defaults to the mysqli.default_socket ini setting |
use Pop\Db\Db;
$db = Db::mysqlConnect([
'database' => 'popdb',
'username' => 'popuser',
'password' => 'db_password',
'host' => '127.0.0.1'
]);
echo $db->getVersion(); // MySQL 8.4.6-0ubuntu3
MySQL is the one adapter whose connection failure isn't a Pop exception. PHP's mysqli driver throws
mysqli_sql_exception inside new \mysqli(...), before the adapter inspects connect_error, so catch that
as well.
PostgreSQL#
Pop\Db\Adapter\Pgsql wraps the pg_* functions from the pgsql extension. Options beyond the four
basics are appended to the connection string as-is.
| Option | |
|---|---|
database |
required |
username |
required |
password |
required |
host |
optional, defaults to localhost |
port, hostaddr, connect_timeout, options, sslmode |
optional, each appended to the connection string |
persist |
truthy to connect through pg_pconnect() instead of pg_connect() |
use Pop\Db\Db;
$db = Db::pgsqlConnect([
'database' => 'popdb',
'username' => 'postgres',
'password' => 'postgres',
'host' => '127.0.0.1',
'sslmode' => 'prefer'
]);
echo $db->getVersion(); // PostgreSQL 17.6 (Ubuntu 17.6-1build1)
This adapter throws Unable to connect to the database. whatever went wrong. The specifics — FATAL: database "nope_popdb" does not exist against FATAL: role "nosuchuser" does not exist — reach you through
a PHP warning from pg_connect(), so read the server log if your handler discards warnings.
SQLite#
Pop\Db\Adapter\Sqlite wraps the SQLite3 class from the sqlite3 extension. There's no host and
no credentials — database is a path on disk.
| Option | |
|---|---|
database |
required — the path to the database file |
flags |
optional, defaults to SQLITE3_OPEN_READWRITE | SQLITE3_OPEN_CREATE |
key |
optional encryption key, for builds of SQLite that support one |
use Pop\Db\Db;
$db = Db::sqliteConnect(['database' => '/var/data/my_database.sqlite']);
echo $db->getVersion(); // SQLite 3.46.1
Create the SQLite file before connecting. The adapter checks file_exists() while reading its options,
so touch database.sqlite — or Db::install(), which creates it — comes first.
The file also needs to be writable by the user PHP runs as, and so does the directory holding it — SQLite writes a journal alongside the database, so a writable file in a read-only directory still fails on the first write.
SQL Server#
Pop\Db\Adapter\Sqlsrv wraps the sqlsrv_* functions from Microsoft's sqlsrv extension.
| Option | |
|---|---|
database |
required |
username |
required |
password |
required |
host |
optional, defaults to localhost |
info |
optional array merged into the connection info passed to sqlsrv_connect() |
ReturnDatesAsStrings |
defaults to true when info does not set it |
use Pop\Db\Db;
$db = Db::sqlsrvConnect([
'database' => 'popdb',
'username' => 'sa',
'password' => 'secret',
'host' => 'localhost'
]);
The sqlsrv extension is a hard requirement for SQL Server, so gate on Db::isAvailable('sqlsrv') first —
without the extension loaded, Db::sqlsrvConnect() and Db::check('sqlsrv', ...) raise Error: Call to undefined function Pop\Db\Adapter\sqlsrv_connect().
The query builder also treats SQL Server as a special case: it has no LIMIT clause, so the builder
rewrites limits and offsets into a ROW_NUMBER() window, and a limited query with no ORDER BY
throws Error: You must set an order by clause to execute a limit clause on the MS SQL Server database.
PDO#
Pop\Db\Adapter\Pdo reaches every driver PDO has rather than a single database, so it's the fallback
when the native extension for your database is not available — and the one adapter that needs a
type.
| Option | |
|---|---|
type |
required — the PDO driver: mysql, pgsql, sqlite, sqlsrv |
database |
required |
username |
required except for sqlite |
password |
required except for sqlite |
host |
optional, defaults to localhost |
options |
optional array passed straight to the PDO constructor as driver options |
use Pop\Db\Db;
$db = Db::pdoConnect([
'type' => 'pgsql',
'database' => 'popdb',
'username' => 'postgres',
'password' => 'postgres',
'host' => '127.0.0.1'
]);
echo $db->getVersion(); // PDO pgsql 17.6 (Ubuntu 17.6-1build1)
echo $db->getType(); // pgsql
type is checked before the credentials are, and a driver name PDO does not know is reported as
missing credentials rather than as a bad driver: Db::pdoConnect(['type' => 'oracle', ...]) throws
Error: The proper database credentials were not passed. Check the spelling of type first when you
see that message on a config that looks complete.
PDO reports connection failures most usefully — the PDOException is re-thrown as
Pop\Db\Adapter\Exception with the SQLSTATE and driver text intact.
Configuring the Connection in an App#
In an application the credentials are data, and they live in a config file that reads them from the
environment. kettle pop:init writes this file when you answer yes to its database prompt, with the
one default entry:
<?php
return [
'default' => [
'database' => $_ENV['DB_DATABASE'],
'adapter' => $_ENV['DB_ADAPTER'],
'username' => $_ENV['DB_USERNAME'],
'password' => $_ENV['DB_PASSWORD'],
'host' => $_ENV['DB_HOST'],
'type' => $_ENV['DB_TYPE'],
],
'reports' => [
'database' => $_ENV['REPORTS_DB_DATABASE'],
'adapter' => $_ENV['REPORTS_DB_ADAPTER'],
'username' => $_ENV['REPORTS_DB_USERNAME'],
'password' => $_ENV['REPORTS_DB_PASSWORD'],
'host' => $_ENV['REPORTS_DB_HOST'],
'type' => $_ENV['REPORTS_DB_TYPE'],
],
];
adapter is the string Db::connect() takes — mysql, pgsql, sqlite, sqlsrv or pdo — and
type is the PDO driver, left empty for the four native adapters. A second entry, keyed however you
like, is a second connection; the key becomes the service name it is registered under.
Opening the connection is bootstrap work, so it belongs in load() rather than the config file. The
convention is a protected initDb() that walks the entries, gates each with Db::check() and registers the
adapter — see Configuration.
Once that has run, the adapter is reachable two ways without passing it around: as a service from the
application, and from Pop\Db\Record for any table class bound to it.
use Pop\Db\Record;
$db = Record::getDb();
$version = $db->getVersion();
Db::getAll() returns every adapter registered with Pop\Db\Db, and Db::db('App\Table\Users')
resolves the one a given table class is bound to — the same lookup Record does internally, which is
what makes a per-class Users::setDb($db) override work alongside a default connection.
See Also#
- Querying — raw queries, prepared statements and transactions on the adapter this page builds
- Query Builder — portable SQL from the adapter's
createSql() - Records & the ORM —
initDb()in full, and binding a connection to a table class - Applications & Bootstrap — where
load()and the service container fit - pop-db README — the full adapter API surface