Database connection
A configuration describes one connection endpoint: driver, address, credentials, schema. The pool builds all of its connections from it, so everything that must be identical across connections lives here — and this is also where migrations are enabled.
What a configuration class is and why
The problem. A database connection is needed in three different places in three
different forms: by a repository, to know where to go; by the pool, to open connections;
and by migrations and health checks, to walk every database in the project. If the
parameters live in an array, each of those places has to learn that array’s key from
somewhere, and the link between the string 'main' and a real server exists only in
someone’s head.
The solution. A connection point is described by a class. The class is already an
identifier: it is referenced by type, not by string. The scanner finds it at startup, so no
registry of databases is needed. And a class’s properties are typed, so a $port that
arrived from .env as a string does not reach the driver unnoticed.
Declaring one
<?php
namespace Main\Configurations;
use Flytachi\Winter\Cdo\Config\PgDbConfig;
class MainDbConfig extends PgDbConfig
{
public function setUp(): void
{
$this->host = env('DB_HOST', 'localhost');
$this->port = (int) env('DB_PORT', 5432);
$this->database = env('DB_NAME', 'app');
$this->username = env('DB_USER', 'postgres');
$this->password = env('DB_PASS', '');
}
}setUp() runs once per instance created — that is, once per connection in the pool.
Reading the environment here is fine; going to the network or to the database is not.
Generator
php call make -C Main writes a stub into main/Configurations/. Once the class is in
the project the scanner picks it up — there is nothing to register.
Reference
Base classes
Choosing a base class is choosing a driver. Their property sets differ because different databases accept different things.
PgDbConfig
| Property | Type | Default | What it sets |
|---|---|---|---|
$host |
string |
localhost |
server address |
$port |
int |
5432 |
port |
$database |
string |
postgres |
database name |
$username |
string |
postgres |
user |
$password |
string |
'' |
password |
$schema |
string |
public |
default schema |
$charset |
?string |
null |
connection encoding |
$sslmode |
string |
disable |
TLS mode: require, verify-full, … |
pgsql:host=db;port=5432;dbname=app;sslmode=disable;$sslmode defaults to disable, and that is not about convenience: under Swoole the
PostgreSQL socket is put into non-blocking mode, and libpq’s TLS negotiation conflicts with
such a socket — on a cold or remote connection the first attempt fails with
could not send SSL negotiation packet.
For a database that requires encryption, override the value in setUp() — 'require' or
'verify-full'. An empty string '' removes the key from the connection string entirely,
leaving the decision to libpq itself (its default is prefer).
Sending database traffic in the clear beyond the local machine is not worth it: if the
server is reachable over the network, require is the minimum.
MySqlDbConfig
| Property | Type | Default | What it sets |
|---|---|---|---|
$host |
string |
localhost |
server address |
$port |
int |
3306 |
port |
$database |
string |
'' |
database name |
$username |
string |
root |
user |
$password |
string |
'' |
password |
$charset |
?string |
null |
encoding; emoji need utf8mb4 |
mysql:host=db;port=3306;dbname=app;charset=utf8mb4;MySQL has no schemas: there the database is the namespace, so getSchema() returns
null.
SqliteDbConfig
| Property | Type | Default | What it sets |
|---|---|---|---|
$path |
string |
:memory: |
file path, or :memory: |
sqlite:/var/app.sqliteNo server and no credentials — which is exactly why it is handy in tests. :memory: lives
as long as the connection does.
DbConfig
The common base: the same properties plus a $driver you set yourself. Use it for a
driver that has no dedicated class.
Shared properties
| Property | Type | Default | What it sets |
|---|---|---|---|
$isPersistent |
bool |
false |
PDO persistent connection |
`$isPersistent` and the pool are different things, and you do not want both
A PDO persistent connection outlives a request within one PHP process, and it exists for FPM, where the process would otherwise close the socket every time. In a resident worker connections live anyway — that is the pool’s job, and the pool is what validates and recycles them.
Turn both on and you get a connection the pool considers its own and PDO considers its own, where a close by one is invisible to the other.
Traits
PpaPoolTrait
Lets a configuration set its own pool size. Without it the defaults apply.
use Flytachi\Winter\Ppa\Pool\{PpaPoolConfigInterface, PpaPoolTrait};
class MainDbConfig extends PgDbConfig implements PpaPoolConfigInterface
{
use PpaPoolTrait;
public int $poolMaxConnections = 10;
public float $poolWaitTimeout = 5.0;
public function setUp(): void { … }
}| Property | Default | What it sets |
|---|---|---|
$poolMaxConnections |
5 |
ceiling on connections per worker |
$poolWaitTimeout |
3.0 |
deadline for a whole connection borrow |
$keepaliveTime |
120.0 |
background validation of idle connections; 0 — off |
$idleTimeout |
600.0 |
close idle connections; 0 — never |
$minimumIdle |
0 |
warm minimum of connections |
The trait declares methods only, no properties — so the class declares them itself and nothing breaks when the interface gains one. The defaults already include background housekeeping: idle connections are pinged and released on their own, so the first request back does not pay for the pause. How to pick the numbers is covered in Connection pool.
PpaCallTrait
Flytachi\\Winter\\Ppa\\PpaCallTrait — short access to a pooled connection straight from
the configuration class:
$cdo = MainDbConfig::instance(); // the current unit of work's connection
$cte = MainDbConfig::cte(); // a CTE repository on the same database| Method | What it returns |
|---|---|
instance() |
the pooled CDO — the same connection this database’s repositories use |
cte() |
a CteRepo bound to this configuration |
Application code normally goes through a repository; this is the door for infrastructure code — migrations, one-off maintenance queries, CTEs spanning several tables.
Configuration methods
Application code does not usually call these: the pool hands it a connection and the repository assembles the queries. They are for the places where the application talks to the database directly — a health endpoint, a maintenance script, diagnostics.
setUp()
Fills the configuration’s properties. The one method you have to write yourself.
It is called once per instance created — and an instance is created for every connection in the pool, not once per application. Hence the rule: reading the environment here is fine, going to the network or to the database is not, or opening a connection drags somebody else’s request along with it.
Syntax
public function setUp(): voidParameters
None — the method writes into the object’s properties.
Example
public function setUp(): void
{
$this->host = env('DB_HOST', 'localhost');
$this->port = (int) env('DB_PORT', 5432);
$this->database = env('DB_NAME', 'app');
$this->username = env('DB_USER', 'postgres');
$this->password = env('DB_PASS', '');
}connection()
Returns the connection, opening it on first use.
The laziness matters here: a configuration object can be created, passed around and taken apart by the scanner without a single socket being opened. The connection appears exactly when it is asked for.
Syntax
final public function connection(): CDOReturns
A CDO — the connection object. It is a PDO with insert, update, delete, batch
operations and driver detection added; everything PDO can do works here too.
Errors
CDOException — if the connection could not be established.
This is not the same as taking a connection from the pool
connection() holds the configuration’s own connection, bypassing the pool. What
application code needs is instance() — it hands out the connection of the
current unit of work, the same one the repositories of this database use.
connect(), disconnect(), reconnect()
Control the socket by hand.
connect() opens the connection if it is not open already; calling it again does nothing.
disconnect() releases the reference — the actual close happens when PHP’s garbage
collector removes the object. reconnect() is the two in sequence.
Syntax
final public function connect(int $timeout = 3): void
final public function disconnect(): void
final public function reconnect(): voidParameters
$timeout on connect() — how many seconds to wait for the connection to be established.
3 by default.
Errors
CDOException — from connect() and reconnect(), if the connection attempt failed.
ping()
Checks whether the database answers.
Runs a SELECT 1 and reports the result as a boolean. It never throws, under any
circumstances: an unreachable server, a wrong password, a broken socket — all of it is
false. That is why the method can be called from a health endpoint without a try around
it.
Syntax
final public function ping(): boolReturns
true if the database answered; false on any error.
pingDetail()
The same thing, but with the latency and the error text.
For places where “alive or not” is not enough: in a health endpoint’s response the latency
shows that the database answers but slowly, and the error text saves a trip to the logs.
Like ping(), it does not throw.
Syntax
final public function pingDetail(): arrayReturns
An array of three keys: status (bool), latency (the response time in milliseconds,
rounded to two places) and error (the exception’s message, or null).
Example
$health = new MainDbConfig()->pingDetail();Result
{ "status": true, "latency": 1.24, "error": null }getDns()
Assembles the connection string from the properties.
There is no password in it — credentials are passed to the driver separately — so the
string can be printed to a log or to diagnostic output without worry. Each base class adds
its own part: PostgreSQL appends sslmode and the encoding, MySQL appends charset.
Syntax
public function getDns(): stringReturns
A string of the form:
pgsql:host=db;port=5432;dbname=app;sslmode=disable;The remaining methods
Plain property readers — there is nothing to describe in them.
| Method | What it returns |
|---|---|
getDriver() |
'pgsql', 'mysql', 'sqlite' |
getSchema() |
the schema, or null if the driver has none |
getUsername() |
the user name |
getPassword() |
the password |
getPersistentStatus() |
whether PDO persistent connections are on |
Environment variables
A configuration reads .env through env(), with the fallback as the second argument:
DB_HOST=localhost
DB_PORT=5432
DB_NAME=app
DB_USER=postgres
DB_PASS=secretThe names are yours; the layer does not dictate them. Keep the fallbacks safe for local
development: the application should boot on an empty .env rather than die with “no such
variable”.
Binding a repository
A repository points at a configuration by class, not by string:
class UserRepository extends Repository
{
public static string $table = 'users';
protected string $dbConfigClassName = MainDbConfig::class;
}That is what decides which database the query goes to. A rename is handled by the IDE, and a typo becomes a parse-time error instead of a runtime one.
Schema
The schema comes from the configuration ($schema on PostgreSQL) and, when needed, from
the repository:
class AuditRepository extends Repository
{
public static string $table = 'events';
protected ?string $schema = 'audit'; // → audit.events
}A repository’s schema overrides the configuration’s. This is what you want when one
database holds several logical spaces: public for the application, audit for journals.
Several databases
One class per connection endpoint:
class MainDbConfig extends PgDbConfig { /* the main database */ }
class AuditDbConfig extends PgDbConfig { /* journals, another server */ }
class LegacyDbConfig extends MySqlDbConfig { /* the old system */ }Each gets its own pool with its own ceiling, and call db pool lists them separately.
Repositories are routed between databases by $dbConfigClassName.
A query does not span databases
Joins, UNION and CTEs run on one connection. Repositories of different configurations
cannot be combined in a single query — the data has to be joined in the application, or by
the database itself (foreign tables, replication).
Checking connectivity
php call db pingThe command finds every configuration in the project, connects to each and prints the
latency. /actuator/health reports the same numbers — there it is pingDetail() doing the
work:
{ "status": true, "latency": 1.24, "error": null }ping() never throws: an unreachable database is false, not an exception. That is why it
can be called from a health endpoint without a try.
Opting into migrations
A configuration takes part in migrations only when it says so:
use Flytachi\Winter\Ppa\Mapping\Attributes\Config\{Extension, Migratable};
use Flytachi\Winter\Ppa\Mapping\Constants\MigratablePriority;
#[Migratable(priority: MigratablePriority::High)]
#[Extension('uuid-ossp')]
class MainDbConfig extends PgDbConfig { … }| Attribute | What it does |
|---|---|
#[Migratable] |
lets call db migrate touch this database; priority sets the order |
#[Extension] |
a PostgreSQL extension, created before the tables |
Without #[Migratable] the command says plainly what is missing:
No migratable configs — add #[Migratable] to a DbConfig to opt in.The opt-in is not a formality: it keeps the schema tool away from a database somebody else owns — a legacy system the application only reads from, for instance.
Next
- Entities — what describes this database’s tables
- Migrations — how the description reaches the server
- Connection pool — how many connections open, and when