Database (Hopper)

Hopper is a thin wrapper around PDO. It handles the connection, parameter binding, and a handful of fetch shapes you write over and over.

Connect

Hopper takes a config array or object. Don't hard-code credentials in code. Keep them in an INI file under app/config/ and load them with $App->getConfig() (or $App->load() in init.php) — that way credentials live outside source control and can differ per environment.

MySQL

Drop your connection details in app/config/MySQL.ini:

driver   = "mysql"
host     = "localhost"
username = "root"
password = "secret"
database = "myapp"
charset  = "utf8mb4"
port     = 3306

Then in init.php, read the file and hand the section to Hopper:

$App->db = new \Truecast\Hopper($App->getConfig('MySQL.ini'));

SQLite

SQLite has no credentials — only a file path — so it's fine to inline. The path lives in the project (not a secret), and the constant BP resolves to your project root:

$App->dbLite = new \Truecast\Hopper([
    'driver'   => 'sqlite',
    'database' => BP.'/app/data/myapp.sqlite',
]);

If you'd still rather keep the filename in config — for example to swap it per environment — use the same pattern:

app/config/SQLite.ini

driver = "sqlite"
database = "myapp.sqlite"
$App->dbLite = new \Truecast\Hopper($App->getConfig('SQLite.ini'));

Already gitignored by default. The starter .gitignore shipped with TrueFramework keeps these out of source control automatically:

app/config
app/data
tests
**/.DS_Store
tmp
.claude
.ddev
logs

So your MySQL.ini and any SQLite files in app/data/ are safe out of the box — no extra setup required. If you want a starter for teammates, commit a sanitized template like MySQL.ini.example at the project root (or anywhere outside app/config/) and have each developer copy it into place.

Reading data

The get() method takes a SQL query, optional parameters, and a return-shape hint:

$user  = $App->db->get("SELECT * FROM users WHERE id = ?", [$id], 'object');
$users = $App->db->get("SELECT * FROM users",                null, 'objects');

Return shapes

$typeReturns
'array'One row, associative array
'arrays'All rows, array of associative arrays
'object'One row, stdClass
'objects'All rows, array of stdClass
'list'Single column from many rows, flat array
'value'Scalar — first column of first row
'number'Scalar number
'keypair'Two-column query as [key => value]
$count    = $App->db->get("SELECT COUNT(*) FROM users",          null, 'value');
$emails   = $App->db->get("SELECT email FROM users",             null, 'list');
$idToName = $App->db->get("SELECT id, name FROM users",          null, 'keypair');

Writing data

set() upserts based on the presence of an id field. No id = INSERT; id present = UPDATE:

// Insert — returns new id
$id = $App->db->set('users', [
    'name'  => 'Daniel',
    'email' => 'd@example.com',
]);

// Update
$App->db->set('users', [
    'id'    => 7,
    'email' => 'new@example.com',
]);

Bulk inserts skip the round-trip-per-row pattern:

$App->db->insertMultiple('users',
    ['name','email'],
    [
        ['Alice', 'a@example.com'],
        ['Bob',   'b@example.com'],
    ]
);

Deleting

$App->db->delete('users', 7);                      // WHERE id = 7
$App->db->delete('users', [1, 2, 3]);              // IN (1,2,3)
$App->db->delete('sessions', ['expired' => 1], 'expired');  // by another column

Raw queries

For DDL or anything outside the upsert/fetch sweet spot:

$App->db->query("CREATE TABLE logs (id INT PRIMARY KEY AUTO_INCREMENT, msg TEXT)");
$App->db->execute("UPDATE users SET active = 0 WHERE last_login < ?", [$cutoff]);

DDL caveat: execute() returns false when the statement reports zero affected rows — which is the normal outcome for CREATE TABLE, DROP TABLE, etc. For DDL, use query() and check $db->getErrors() instead.

Chaining

// Append ORDER BY without rewriting your SQL
$rows = $App->db->sort('created_at DESC')->get($sql, [], 'objects');

Errors & metadata

$App->db->getErrors();       // array of recorded errors
$App->db->getLastQuery();    // last SQL string run
$App->db->lastInsertId();    // last AUTO_INCREMENT value