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.
Why a class, not an array
An array in config/database.php is the familiar shape, but it knows nothing about
itself: a typo in a key shows up at runtime, and “which databases does this project even
have” is answered by eye.
A class solves three problems at once: the class name becomes the identifier a
repository points at; the scanner finds every configuration on its own, so migrations
and health checks need no registry; and properties are typed, so $port will not
quietly be a string.
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 — convenient locally and wrong in production: traffic
to the database goes in the clear. Managed databases normally want at least require.
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 |
how long to wait for a free connection |
$keepaliveTime |
0.0 |
background validation of idle connections; 0 — off |
$idleTimeout |
0.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. 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
| Method | What it returns |
|---|---|
setUp() |
fills the properties; called once per instance |
connection() |
a CDO, opening the connection on first use |
connect() / disconnect() / reconnect() |
socket control |
ping() |
true/false; never throws |
pingDetail() |
['status' => true, 'latency' => 0.32, 'error' => null] |
getDns() |
the DSN without the password — safe to log |
getDriver() |
'pgsql', 'mysql', 'sqlite' |
getSchema() |
the schema, or null |
getUsername() / getPassword() |
credentials |
getPersistentStatus() |
whether 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