Database · Configuration

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.

Package flytachi/winter-ppaOn top of winter-cdoDrivers PostgreSQL · MySQL · SQLite

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

main/Configurations/MainDbConfig.php
<?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, …
text
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
text
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:
text
sqlite:/var/app.sqlite

No 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.

php
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:

php
$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:

.env
DB_HOST=localhost
DB_PORT=5432
DB_NAME=app
DB_USER=postgres
DB_PASS=secret

The 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:

php
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:

php
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:

php
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

bash
php call db ping

The 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:

json
{ "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:

php
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:

text
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