Basic connections
An application nearly always needs a connection — to a database, to Redis. This page shows how to open one and where to keep it so that it lives exactly as long as it should: once in a script, one per request in an application. For most projects that is all you need.
What is actually being decided
Opening a connection is one line. The real question is a different one: who holds that object, and how long does it live. Speed and correctness both follow from the answer.
A connection is a single socket with a sequential protocol: request → response → next request. While it is busy with one thing, nothing else gets through it. And a Winter worker under Swoole serves many requests at once, each in its own coroutine. Hence one rule, from which everything else on this page follows:
One connection per unit of work. Never hand the same socket to two requests.
A one-off connection
The simplest case: a script, a migration, a one-shot cron job, a test. The connection is needed once and there is nowhere to keep it — take an inline configuration and get the connection straight away.
<?php
require 'vendor/autoload.php';
use Flytachi\Winter\Cdo\Config\Call\PgDbCall;
$cdo = (new PgDbCall(
host: env('DB_HOST', 'localhost'),
port: (int) env('DB_PORT', 5432),
database: env('DB_NAME'),
username: env('DB_USER'),
password: env('DB_PASS'),
))->connection();
$id = $cdo->insert('reports', ['title' => 'November', 'ready' => false]);There is a variant per driver — PgDbCall, MySqlDbCall, SqliteDbCall, and
DbCall for anything else. SQLite needs neither a server nor credentials, which is
handy in tests:
use Flytachi\Winter\Cdo\Config\Call\SqliteDbCall;
$cdo = (new SqliteDbCall())->connection(); // in memory
$cdo = (new SqliteDbCall(path: 'storage/app.sqlite'))->connection();For one-off work only
Every such call opens a new connection, and it closes only when the object is garbage-collected. In an application serving requests this is the wrong shape — declare the connection as a dependency instead, below.
A connection per request
In an application the connection is declared as a bean: the container creates it when
it is needed and hands it to whoever asked. The scope is Scope::Request — its own
connection for every request.
PDO
<?php
namespace Main\Configurations;
use Flytachi\Winter\Kernel\App\Attribute\{Bean, Configuration, Value};
use Flytachi\Winter\Kernel\App\Scope;
#[Configuration]
final class DbConfiguration
{
#[Bean(scope: Scope::Request)]
public function pdo(
#[Value('DB_HOST')] string $host,
#[Value('DB_NAME')] string $name,
#[Value('DB_USER')] string $user,
#[Value('DB_PASS')] string $pass,
): \PDO {
return new \PDO("pgsql:host={$host};dbname={$name}", $user, $pass, [
\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
]);
}
}CDO
The same thing, except the connection comes back as a CDO — a layer over PDO with
insert(), update(), delete(), upserts and the Qb condition builder. CDO
extends PDO, so prepare() / query() are still there.
<?php
namespace Main\Configurations;
use Flytachi\Winter\Cdo\Config\Call\PgDbCall;
use Flytachi\Winter\Cdo\Connection\CDO;
use Flytachi\Winter\Kernel\App\Attribute\{Bean, Configuration, Value};
use Flytachi\Winter\Kernel\App\Scope;
#[Configuration]
final class DbConfiguration
{
#[Bean(scope: Scope::Request)]
public function db(
#[Value('DB_HOST')] string $host,
#[Value('DB_NAME')] string $name,
#[Value('DB_USER')] string $user,
#[Value('DB_PASS')] string $pass,
): CDO {
return (new PgDbCall(
host: $host,
database: $name,
username: $user,
password: $pass,
))->connection();
}
}Redis
<?php
namespace Main\Configurations;
use Flytachi\Winter\Kernel\App\Attribute\{Bean, Configuration, Value};
use Flytachi\Winter\Kernel\App\Scope;
#[Configuration]
final class RedisConfiguration
{
#[Bean(scope: Scope::Request, name: 'redisStore')]
public function store(
#[Value('REDIS_HOST', 'localhost')] string $host,
#[Value('REDIS_PORT', 6379)] int $port,
#[Value('REDIS_PASS', '')] string $password,
#[Value('REDIS_DB', 0)] int $database,
): \Redis {
$redis = new \Redis();
$redis->connect($host, $port, 9);
if ($password !== '') {
$redis->auth($password);
}
$redis->select($database);
return $redis;
}
}The bean is named explicitly (name: 'redisStore') because there can be more than one
Redis connection — a separate database for the cache and for a queue, say. By the
\Redis type alone they would be indistinguishable.
Using it
From here the connection is injected like any other dependency:
use Flytachi\Winter\Cdo\Connection\CDO;
use Flytachi\Winter\Cdo\Qb;
use Flytachi\Winter\DI\Attribute\{Autowired, Inject};
class OrderService
{
#[Autowired]
private CDO $db; // by type
#[Inject('redisStore')]
private \Redis $cache; // by bean name
public function markPaid(int $id): void
{
$this->db->update('orders', ['status' => 'paid'], Qb::eq('id', $id));
$this->cache->del("order:{$id}");
}
}Why Request and not Singleton
This is the important part of the page: the one setting that is easy to get wrong.
A singleton connection breaks under load
#[Bean] defaults to singleton: the method runs once and the object lives as long
as the worker does. For a connection that is a mistake, and not one that shows up
immediately.
A worker serves requests as concurrent coroutines. One socket handed to all of them means two coroutines writing into it interleaved: someone else’s response, “packets out of order”, and under Swoole a worker that dies outright.
Scope::Request gives one connection per request — that is already correct.
The same rule has a second consequence: a connection must not be stored in a field
of a #[Singleton] service. A singleton’s properties are filled once, when it is
built — so it would remember the first request’s connection forever and hand it to
everyone afterwards. The kernel checks this at startup and refuses to boot, naming the
offending path.
Under FPM the rules are looser — but do not write for FPM
There a process serves one request, there is no concurrency, and a singleton
connection would be correct. But an application written that way breaks when it moves
to Swoole — quietly, and under load. Scope::Request is correct in both runtimes,
which is why it is the default answer.
What this connection style lacks
Scope::Request is correct, and many applications can stop right here. But it has a
price, and when that price becomes visible it is time to look at a pool.
The connection is opened again on every request. A TCP handshake, TLS, authentication — and with PostgreSQL a process fork on the server side as well.
Their number is unbounded. A thousand concurrent requests means a thousand
connection attempts. The database answers too many connections, and what falls over
is not the spike but the whole application.
A database failure no longer heals itself. A resident worker lives for weeks and may hold a socket the server has already closed. A per-request connection softens that but does not remove it: the socket can die mid-request too.
Next
Both advanced routes are about pooling: connections are reused, their number is capped, and dead ones are replaced.
- PPA — working with the database: the connection pool, repositories, entities and migrations.
- Redis — the
winter-redispackage: a connection pool, prefixed stores, hashes, lists and streams. - Dependency injection — scopes in depth,
#[Configuration],#[Bean]and#[Value].