The Game Panel Design record
The Game Panel

Database

Versions
History

Database access is built directly on PDO, per ADR-0008. Any number of named connections are configured, one of them is the primary connection, and a connection executes queries and manages transactions.

Only MySQL is reachable, per ADR-0007. The driver is not configurable: ConnectionConfig sets it to mysql in its constructor.

Configuration

DatabaseConfig and ConnectionConfig are configuration objects, hydrated through fromArray(). Array shapes are asserted there and value rules are checked in each constructor, so make() cannot bypass them.

DatabaseConfig Rule
primary Required, not empty, and a connection must be configured under it.
connections Required, a non-empty map. Each entry is a non-empty map hydrated into a ConnectionConfig, and a failure is rethrown naming the connection.
persistent Optional, boolean, defaulting to false.
ConnectionConfig Rule
database, username, password Required, and none may be empty.
socket Optional. When set, the host and port are ignored and left null.
host, port Required when no socket is given. The port must be an integer.
options Optional, and an array.
driver Not configurable. Always mysql.

persistent is held and validated. Nothing reads it, so no connection is created with PDO::ATTR_PERSISTENT.

Connections

ConnectionFactory is constructed with the DatabaseConfig and hands out connections:

$primary = $factory->make();
$replica = $factory->make('replica');

make(?string $name = null) uses the primary connection's name when none is given. It returns the connection already created under that name, or otherwise builds one and keeps it for the life of the factory. A name with no configuration throws ConnectionException, and so does a PDO failure while connecting.

The DSN is built from the configuration:

Configuration DSN
A socket mysql:unix_socket={socket};dbname={database}
A host mysql:host={host};port={port};dbname={database}

A driver other than mysql throws DatabaseException.

Options

Every connection is created with these defaults:

Option Value
PDO::ATTR_ERRMODE PDO::ERRMODE_EXCEPTION
PDO::ATTR_DEFAULT_FETCH_MODE PDO::FETCH_ASSOC
PDO::ATTR_EMULATE_PREPARES false
PDO::ATTR_STRINGIFY_FETCHES false

The configured options are combined as $config->options + self::$defaultOptions, so a configured option takes precedence over the default of the same name, the error mode included.

Injection

A connection is injected into a parameter typed exactly Connection and carrying the Database attribute, which is a resolvable attribute paired with DatabaseResolver, per ADR-0002.

public function __construct(
    #[Database] private Connection $primary,
    #[Database('replica')] private Connection $replica,
) {}

The resolver returns the connection the attribute names, and the primary connection when it names none. It throws DatabaseException when the dependency's resolvable attribute is not Database, and when the parameter's type is not exactly Connection.

Executing

Connection carries its name and wraps a PDO connection.

Method Returns
query(Expression|string $query, array $bindings = []) A Result.
execute(Expression|string $query, array $bindings = []) A WriteResult.
stream(Expression|string $query, array $bindings = []) A Cursor.

Each takes SQL as a string with its bound values, or an expression, whose SQL and bound values are read from toSql() and getBindings(). Bound values given alongside an expression are discarded.

Every statement is prepared and then executed. Booleans among the bound values are converted to integers first. A statement that cannot be prepared or executed throws QueryException, which carries the SQL and bound values through getSql() and getBindings(), and the PDO failure as its previous exception.

execute() reads the last inserted identifier from the connection rather than from the statement, so the value it returns after an update or a delete is whatever that connection last inserted.

Results

Result fetches every row the first time any row is read, and keeps them.

Method Effect
first() The first row, or null.
all() Every row.
each(callable $callback) Calls the callback with each row.
count() The statement's row count.
isEmpty() Whether that count is zero.

Cursor fetches one row at a time as it is iterated, so a large result is not held in memory at once. Its rows can be iterated once, through each() or the generator from rows(). It has the same count() and isEmpty().

Both take their row count from PDO's rowCount(), which for a select is driver-dependent.

WriteResult holds affectedRows(), lastInsertId() and wasSuccessful(), which is whether at least one row was affected. Result, Cursor and WriteResult each carry the bound values they were executed with.

Rows

A Row holds one row's values, keyed by column name.

Method Effect
get(string $column) The value, or null when the column is absent.
has(string $column) Whether the column is present, including when its value is null.
isNull(string $column) Whether the column is present and holds null. An absent column returns false.
toArray() Every value, keyed by column name.
string(), int(), float(), bool(), array() The value cast to that type, through ValueGetter.

Transactions

Method Effect
transaction(callable $callback) Begins a transaction, calls the callback with the connection, commits, and returns the callback's result. A throwing callback rolls back and the exception is rethrown.
beginTransaction(), commit(), rollback() Begin, commit and roll back directly.
isInTransaction() Whether a transaction is active.

A PDO failure beginning, committing or rolling back throws DatabaseException. Transactions do not nest, and there are no savepoints.

Migrations

Migrator is an empty class. Nothing runs migrations, records which have run, or rolls one back. The Migration and ReversibleMigration contracts are part of the schema schema builder.

Errors

Exception Extends Thrown when
DatabaseException RuntimeException The driver is not supported, a transaction cannot be begun, committed or rolled back, or the resolver is given a dependency it cannot resolve.
ConnectionException DatabaseException No configuration exists for a connection name, or the connection cannot be established. It carries the name.
QueryException DatabaseException A statement cannot be prepared or executed. It carries the SQL and bound values.

Source spec/database.md · commit 072385a · exported