Getting started

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.

Database PDO / CDOCache RedisWhat matters the scope

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.

scripts/report.php
<?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:

php
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::Requestits own connection for every request.

PDO

main/Configurations/DbConfiguration.php
<?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.

main/Configurations/DbConfiguration.php
<?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

main/Configurations/RedisConfiguration.php
<?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:

php
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-redis package: a connection pool, prefixed stores, hashes, lists and streams.
  • Dependency injection — scopes in depth, #[Configuration], #[Bean] and #[Value].