База данных · Конфигурация

Конфигурация БД

Конфигурация описывает одну точку подключения: драйвер, адрес, учётные данные, схему. Из неё пул строит все свои соединения, поэтому здесь лежит всё, что должно быть одинаковым у каждого соединения — и здесь же включаются миграции.

Пакет flytachi/winter-ppaПоверх winter-cdoДрайверы PostgreSQL · MySQL · SQLite

Что такое класс конфигурации и зачем

Проблема. Подключение к базе нужно в трёх разных местах и в трёх разных формах: репозиторию — чтобы знать, куда идти; пулу — чтобы открывать соединения; миграциям и health-проверкам — чтобы обойти все базы проекта. Если параметры лежат в массиве, каждое из этих мест должно откуда-то узнать ключ этого массива, и связь между «строкой 'main'» и настоящим сервером существует только в голове.

Решение. Точка подключения описывается классом. Класс — это уже идентификатор: на него ссылаются типом, а не строкой. Его же находит сканер при старте, поэтому реестра баз не нужно. А свойства у класса типизированы, и $port, приехавший из .env строкой, не доедет до драйвера незамеченным.

Объявление

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() вызывается один раз на каждый создаваемый экземпляр — то есть на каждое соединение в пуле. Читать окружение здесь нормально; ходить в сеть или в базу — нет.

Генератор

php call make -C Main создаёт заготовку в main/Configurations/. Класс, попавший в проект, дальше находит сканер — регистрировать его нигде не нужно.


Справочник

Базовые классы

Выбор базового класса — это выбор драйвера. Набор свойств у них разный, потому что разные базы принимают разное.

PgDbConfig

Свойство Тип По умолчанию Что задаёт
$host string localhost адрес сервера
$port int 5432 порт
$database string postgres имя базы
$username string postgres пользователь
$password string '' пароль
$schema string public схема по умолчанию
$charset ?string null кодировка соединения
$sslmode string disable режим TLS: require, verify-full, …
text
pgsql:host=db;port=5432;dbname=app;sslmode=disable;

$sslmode по умолчанию disable, и это не про удобство: под Swoole сокет PostgreSQL переведён в неблокирующий режим, а согласование TLS внутри libpq с таким сокетом конфликтует — на холодном или удалённом подключении первая попытка падает с could not send SSL negotiation packet.

Для базы, которая требует шифрования, значение переопределяют в setUp() — 'require' или 'verify-full'. Пустая строка '' убирает ключ из строки подключения целиком, и тогда решает сам libpq (его умолчание — prefer).

Отдавать трафик до базы открытым за пределами локальной машины не стоит: если сервер доступен по сети, require — минимум.

MySqlDbConfig

Свойство Тип По умолчанию Что задаёт
$host string localhost адрес сервера
$port int 3306 порт
$database string '' имя базы
$username string root пользователь
$password string '' пароль
$charset ?string null кодировка; для эмодзи нужен utf8mb4
text
mysql:host=db;port=3306;dbname=app;charset=utf8mb4;

Схемы у MySQL нет: там база и есть пространство имён, поэтому getSchema() вернёт null.

SqliteDbConfig

Свойство Тип По умолчанию Что задаёт
$path string :memory: путь к файлу или :memory:
text
sqlite:/var/app.sqlite

Ни сервера, ни учётных данных — поэтому в тестах он и удобен. :memory: живёт ровно столько, сколько соединение.

DbConfig

Общий базовый класс: те же свойства плюс $driver, который вы задаёте сами. Нужен для драйвера, под который отдельного класса нет.

Общие свойства

Свойство Тип По умолчанию Что задаёт
$isPersistent bool false постоянное соединение PDO

`$isPersistent` и пул — разные вещи, и вместе не нужны

Постоянное соединение PDO переживает запрос внутри процесса PHP, и придумано оно для FPM, где процесс иначе закрывал бы сокет каждый раз. У резидентного воркера соединения и так живут — этим занимается пул, и он же их проверяет и обновляет.

Включив оба, вы получаете соединение, которое пул считает своим, а PDO — своим, и закрытие одним не видно другому.

Трейты

PpaPoolTrait

Позволяет конфигурации задать свой размер пула. Без него берутся значения по умолчанию.

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 { … }
}
Свойство По умолчанию Что задаёт
$poolMaxConnections 5 потолок соединений на воркер
$poolWaitTimeout 3.0 дедлайн всей выдачи соединения
$keepaliveTime 120.0 фоновая проверка простаивающих; 0 — выключено
$idleTimeout 600.0 закрывать простаивающие; 0 — не закрывать
$minimumIdle 0 тёплый минимум соединений

Трейт объявляет только методы, без свойств, — поэтому класс объявляет свойства сам и не ломается, когда в интерфейс добавляют новое. Умолчания уже включают фоновую уборку: простаивающие соединения пингуются и отпускаются сами, так что за простой не платит первый вернувшийся запрос. Как выбирать числа — в Пуле соединений.

PpaCallTrait

Flytachi\\Winter\\Ppa\\PpaCallTrait — короткий доступ к соединению из пула прямо от класса конфигурации:

php
$cdo = MainDbConfig::instance();   // соединение текущей единицы работы
$cte = MainDbConfig::cte();        // CTE-репозиторий на этой же базе
Метод Что возвращает
instance() CDO из пула — то же соединение, что и у репозиториев этой базы
cte() CteRepo, привязанный к этой конфигурации

Прикладной код обычно ходит через репозиторий; это дверь для инфраструктурного — миграций, разовых обслуживающих запросов, CTE поверх нескольких таблиц.

Методы конфигурации

Прикладной код эти методы обычно не вызывает: соединение ему выдаёт пул, а запросы собирает репозиторий. Нужны они там, где приложение разговаривает с базой напрямую — health-эндпоинт, обслуживающий скрипт, диагностика.

setUp()

Заполняет свойства конфигурации. Единственный метод, который вы обязаны написать сами.

Вызывается один раз на каждый создаваемый экземпляр — а экземпляр создаётся на каждое соединение в пуле, не один на приложение. Отсюда правило: читать окружение здесь нормально, ходить в сеть или в базу — нет, иначе открытие соединения потянет за собой чужой запрос.

Синтаксис

php
public function setUp(): void

Параметры

Нет — метод пишет в свойства объекта.

Пример

php
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()

Возвращает соединение, открывая его при первом обращении.

Ленивость здесь существенна: объект конфигурации можно создать, передать, разобрать сканером — и ни одного сокета при этом не откроется. Соединение появляется ровно тогда, когда его попросили.

Синтаксис

php
final public function connection(): CDO

Возвращает

CDO — объект соединения. Это PDO, к которому добавлены insert, update, delete, пакетные операции и определение драйвера; всё, что умеет PDO, работает и здесь.

Ошибки

CDOException — если соединение установить не удалось.

Это не то же самое, что взять соединение из пула

connection() держит собственное соединение конфигурации, мимо пула. Для прикладного кода нужен instance() — он выдаёт соединение текущей единицы работы, то самое, которым пользуются репозитории этой базы.

connect(), disconnect(), reconnect()

Управляют сокетом вручную.

connect() открывает соединение, если оно ещё не открыто; повторный вызов ничего не делает. disconnect() отпускает ссылку — настоящее закрытие произойдёт, когда сборщик мусора PHP уберёт объект. reconnect() — то и другое подряд.

Синтаксис

php
final public function connect(int $timeout = 3): void
final public function disconnect(): void
final public function reconnect(): void

Параметры

$timeout у connect() — сколько секунд ждать установки соединения. По умолчанию 3.

Ошибки

CDOException — из connect() и reconnect(), если подключиться не удалось.

ping()

Проверяет, отвечает ли база.

Выполняет SELECT 1 и сообщает результат логическим значением. Не бросает ни при каких обстоятельствах: недоступный сервер, неверный пароль, оборванный сокет — всё это false. Поэтому метод можно звать из health-эндпоинта, не оборачивая в try.

Синтаксис

php
final public function ping(): bool

Возвращает

true, если база ответила; false при любой ошибке.

pingDetail()

То же самое, но с задержкой и текстом ошибки.

Нужен там, где мало знать «жива или нет»: в ответе health-эндпоинта задержка показывает, что база отвечает, но медленно, а текст ошибки избавляет от похода в логи. Как и ping(), не бросает.

Синтаксис

php
final public function pingDetail(): array

Возвращает

Массив из трёх ключей: status (bool), latency (время ответа в миллисекундах, округлённое до сотых) и error (текст исключения или null).

Пример

php
$health = new MainDbConfig()->pingDetail();

Результат

json
{ "status": true, "latency": 1.24, "error": null }

getDns()

Собирает строку подключения из свойств.

Пароля в ней нет — учётные данные передаются драйверу отдельно, — поэтому строку можно без опаски печатать в лог или в диагностический вывод. Каждый базовый класс дописывает своё: PostgreSQL добавляет sslmode и кодировку, MySQL — charset.

Синтаксис

php
public function getDns(): string

Возвращает

Строку вида:

text
pgsql:host=db;port=5432;dbname=app;sslmode=disable;

Остальные методы

Простые читатели свойств — описывать в них нечего.

Метод Что возвращает
getDriver() 'pgsql', 'mysql', 'sqlite'
getSchema() схему или null, если у драйвера её нет
getUsername() имя пользователя
getPassword() пароль
getPersistentStatus() включено ли постоянное соединение PDO

Переменные окружения

Конфигурация читает .env через env() — со значением по умолчанию вторым аргументом:

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

Имена переменных — ваши, слой их не диктует. Значения по умолчанию стоит держать безопасными для локальной разработки: приложение должно подниматься на пустом .env, а не падать с «нет такой переменной».

Привязка репозитория

Репозиторий указывает на конфигурацию классом, а не строкой:

php
class UserRepository extends Repository
{
  public static string $table         = 'users';
  protected string $dbConfigClassName = MainDbConfig::class;
}

Отсюда и берётся, в какую базу пойдёт запрос. Переименование класса подхватит IDE, а опечатка станет ошибкой на этапе разбора, а не в рантайме.

Схема

Схема указывается в конфигурации ($schema у PostgreSQL) и, при необходимости, у репозитория:

php
class AuditRepository extends Repository
{
  public static string $table = 'events';
  protected ?string $schema   = 'audit';      // → audit.events
}

Схема репозитория перекрывает схему конфигурации. Это нужно, когда одна база держит несколько логических пространств: public для приложения, audit для журналов.

Несколько баз

Столько классов, сколько точек подключения:

php
class MainDbConfig extends PgDbConfig    { /* основная база */ }
class AuditDbConfig extends PgDbConfig   { /* журналы, другой сервер */ }
class LegacyDbConfig extends MySqlDbConfig { /* старая система */ }

У каждой — свой пул со своим потолком, и в call db pool они видны по отдельности. Репозитории расходятся по базам через $dbConfigClassName.

Запрос не ходит между базами

Джойн, UNION и CTE выполняются на одном соединении. Репозитории разных конфигураций соединить в одном запросе нельзя — данные придётся сводить в приложении либо средствами самой базы (внешние таблицы, репликация).

Проверить связь

bash
php call db ping

Команда находит все конфигурации проекта, подключается к каждой и печатает задержку. Те же цифры отдаёт /actuator/health — там это делает pingDetail():

json
{ "status": true, "latency": 1.24, "error": null }

ping() никогда не бросает: недоступная база — это false, а не исключение. Поэтому проверку можно звать из health-эндпоинта, не оборачивая в try.

Включить миграции

Конфигурация участвует в миграциях только с явным опт-ином:

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 { … }
Атрибут Что делает
#[Migratable] разрешает call db migrate трогать эту базу; priority задаёт порядок
#[Extension] расширение PostgreSQL, создаваемое до таблиц

Без #[Migratable] команда честно скажет, чего не хватает:

text
No migratable configs — add #[Migratable] to a DbConfig to opt in.

Опт-ин здесь не формальность: он не даёт инструменту схемы дотянуться до базы, которой владеет кто-то другой — например, до легаси-системы, куда приложение только читает.

Дальше