Конфигурация БД
Конфигурация описывает одну точку подключения: драйвер, адрес, учётные данные, схему. Из неё пул строит все свои соединения, поэтому здесь лежит всё, что должно быть одинаковым у каждого соединения — и здесь же включаются миграции.
Что такое класс конфигурации и зачем
Проблема. Подключение к базе нужно в трёх разных местах и в трёх разных формах:
репозиторию — чтобы знать, куда идти; пулу — чтобы открывать соединения; миграциям и
health-проверкам — чтобы обойти все базы проекта. Если параметры лежат в массиве, каждое
из этих мест должно откуда-то узнать ключ этого массива, и связь между «строкой 'main'»
и настоящим сервером существует только в голове.
Решение. Точка подключения описывается классом. Класс — это уже идентификатор: на него
ссылаются типом, а не строкой. Его же находит сканер при старте, поэтому реестра баз не
нужно. А свойства у класса типизированы, и $port, приехавший из .env строкой, не
доедет до драйвера незамеченным.
Объявление
<?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, … |
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 |
mysql:host=db;port=3306;dbname=app;charset=utf8mb4;Схемы у MySQL нет: там база и есть пространство имён, поэтому getSchema() вернёт null.
SqliteDbConfig
| Свойство | Тип | По умолчанию | Что задаёт |
|---|---|---|---|
$path |
string |
:memory: |
путь к файлу или :memory: |
sqlite:/var/app.sqliteНи сервера, ни учётных данных — поэтому в тестах он и удобен. :memory: живёт ровно
столько, сколько соединение.
DbConfig
Общий базовый класс: те же свойства плюс $driver, который вы задаёте сами. Нужен для
драйвера, под который отдельного класса нет.
Общие свойства
| Свойство | Тип | По умолчанию | Что задаёт |
|---|---|---|---|
$isPersistent |
bool |
false |
постоянное соединение PDO |
`$isPersistent` и пул — разные вещи, и вместе не нужны
Постоянное соединение PDO переживает запрос внутри процесса PHP, и придумано оно для FPM, где процесс иначе закрывал бы сокет каждый раз. У резидентного воркера соединения и так живут — этим занимается пул, и он же их проверяет и обновляет.
Включив оба, вы получаете соединение, которое пул считает своим, а PDO — своим, и закрытие одним не видно другому.
Трейты
PpaPoolTrait
Позволяет конфигурации задать свой размер пула. Без него берутся значения по умолчанию.
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 — короткий доступ к соединению из пула прямо от
класса конфигурации:
$cdo = MainDbConfig::instance(); // соединение текущей единицы работы
$cte = MainDbConfig::cte(); // CTE-репозиторий на этой же базе| Метод | Что возвращает |
|---|---|
instance() |
CDO из пула — то же соединение, что и у репозиториев этой базы |
cte() |
CteRepo, привязанный к этой конфигурации |
Прикладной код обычно ходит через репозиторий; это дверь для инфраструктурного — миграций, разовых обслуживающих запросов, CTE поверх нескольких таблиц.
Методы конфигурации
Прикладной код эти методы обычно не вызывает: соединение ему выдаёт пул, а запросы собирает репозиторий. Нужны они там, где приложение разговаривает с базой напрямую — health-эндпоинт, обслуживающий скрипт, диагностика.
setUp()
Заполняет свойства конфигурации. Единственный метод, который вы обязаны написать сами.
Вызывается один раз на каждый создаваемый экземпляр — а экземпляр создаётся на каждое соединение в пуле, не один на приложение. Отсюда правило: читать окружение здесь нормально, ходить в сеть или в базу — нет, иначе открытие соединения потянет за собой чужой запрос.
Синтаксис
public function setUp(): voidПараметры
Нет — метод пишет в свойства объекта.
Пример
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()
Возвращает соединение, открывая его при первом обращении.
Ленивость здесь существенна: объект конфигурации можно создать, передать, разобрать сканером — и ни одного сокета при этом не откроется. Соединение появляется ровно тогда, когда его попросили.
Синтаксис
final public function connection(): CDOВозвращает
CDO — объект соединения. Это PDO, к которому добавлены insert, update, delete,
пакетные операции и определение драйвера; всё, что умеет PDO, работает и здесь.
Ошибки
CDOException — если соединение установить не удалось.
Это не то же самое, что взять соединение из пула
connection() держит собственное соединение конфигурации, мимо пула. Для прикладного
кода нужен instance() — он выдаёт соединение текущей единицы работы,
то самое, которым пользуются репозитории этой базы.
connect(), disconnect(), reconnect()
Управляют сокетом вручную.
connect() открывает соединение, если оно ещё не открыто; повторный вызов ничего не
делает. disconnect() отпускает ссылку — настоящее закрытие произойдёт, когда сборщик
мусора PHP уберёт объект. reconnect() — то и другое подряд.
Синтаксис
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.
Синтаксис
final public function ping(): boolВозвращает
true, если база ответила; false при любой ошибке.
pingDetail()
То же самое, но с задержкой и текстом ошибки.
Нужен там, где мало знать «жива или нет»: в ответе health-эндпоинта задержка показывает,
что база отвечает, но медленно, а текст ошибки избавляет от похода в логи. Как и ping(),
не бросает.
Синтаксис
final public function pingDetail(): arrayВозвращает
Массив из трёх ключей: status (bool), latency (время ответа в миллисекундах,
округлённое до сотых) и error (текст исключения или null).
Пример
$health = new MainDbConfig()->pingDetail();Результат
{ "status": true, "latency": 1.24, "error": null }getDns()
Собирает строку подключения из свойств.
Пароля в ней нет — учётные данные передаются драйверу отдельно, — поэтому строку можно
без опаски печатать в лог или в диагностический вывод. Каждый базовый класс дописывает
своё: PostgreSQL добавляет sslmode и кодировку, MySQL — charset.
Синтаксис
public function getDns(): stringВозвращает
Строку вида:
pgsql:host=db;port=5432;dbname=app;sslmode=disable;Остальные методы
Простые читатели свойств — описывать в них нечего.
| Метод | Что возвращает |
|---|---|
getDriver() |
'pgsql', 'mysql', 'sqlite' |
getSchema() |
схему или null, если у драйвера её нет |
getUsername() |
имя пользователя |
getPassword() |
пароль |
getPersistentStatus() |
включено ли постоянное соединение PDO |
Переменные окружения
Конфигурация читает .env через env() — со значением по умолчанию вторым аргументом:
DB_HOST=localhost
DB_PORT=5432
DB_NAME=app
DB_USER=postgres
DB_PASS=secretИмена переменных — ваши, слой их не диктует. Значения по умолчанию стоит держать
безопасными для локальной разработки: приложение должно подниматься на пустом .env, а
не падать с «нет такой переменной».
Привязка репозитория
Репозиторий указывает на конфигурацию классом, а не строкой:
class UserRepository extends Repository
{
public static string $table = 'users';
protected string $dbConfigClassName = MainDbConfig::class;
}Отсюда и берётся, в какую базу пойдёт запрос. Переименование класса подхватит IDE, а опечатка станет ошибкой на этапе разбора, а не в рантайме.
Схема
Схема указывается в конфигурации ($schema у PostgreSQL) и, при необходимости, у
репозитория:
class AuditRepository extends Repository
{
public static string $table = 'events';
protected ?string $schema = 'audit'; // → audit.events
}Схема репозитория перекрывает схему конфигурации. Это нужно, когда одна база держит
несколько логических пространств: public для приложения, audit для журналов.
Несколько баз
Столько классов, сколько точек подключения:
class MainDbConfig extends PgDbConfig { /* основная база */ }
class AuditDbConfig extends PgDbConfig { /* журналы, другой сервер */ }
class LegacyDbConfig extends MySqlDbConfig { /* старая система */ }У каждой — свой пул со своим потолком, и в call db pool они видны по отдельности.
Репозитории расходятся по базам через $dbConfigClassName.
Запрос не ходит между базами
Джойн, UNION и CTE выполняются на одном соединении. Репозитории разных конфигураций
соединить в одном запросе нельзя — данные придётся сводить в приложении либо средствами
самой базы (внешние таблицы, репликация).
Проверить связь
php call db pingКоманда находит все конфигурации проекта, подключается к каждой и печатает задержку. Те же
цифры отдаёт /actuator/health — там это делает pingDetail():
{ "status": true, "latency": 1.24, "error": null }ping() никогда не бросает: недоступная база — это false, а не исключение. Поэтому
проверку можно звать из health-эндпоинта, не оборачивая в try.
Включить миграции
Конфигурация участвует в миграциях только с явным опт-ином:
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] команда честно скажет, чего не хватает:
No migratable configs — add #[Migratable] to a DbConfig to opt in.Опт-ин здесь не формальность: он не даёт инструменту схемы дотянуться до базы, которой владеет кто-то другой — например, до легаси-системы, куда приложение только читает.
Дальше
- Сущности — что описывает таблицы этой базы
- Миграции — как описанное доезжает до сервера
- Пул соединений — сколько соединений откроется и когда