--- name: roadmap description: Статус слоя БД (MariaDB/Mongo/Elasticsearch) в Bicycle — что реализовано, что ждёт внешней инфраструктуры metadata: type: project --- Реализовано (2026-08-07) — три независимых сервиса, каждый со своей фабрикой в духе `Services\Auth`, без общего интерфейса поверх всех трёх (SQL/документы/поиск — слишком разные модели, чтобы прятать за одной абстракцией). Подробности использования — CLAUDE.md → разделы DataBase/Elasticsearch/Mongo. - **Реляционная БД (MariaDB, полностью рабочая и протестированная, реальное подключение)** — `Services\Database` (фабрика), `Services\DataBase\Classes\PdoConnection` (failover по хостам, транзакции с вложенными SAVEPOINT — единственный источник истины по вложенности, общий на всё подключение), `Services\DataBase\Classes\Statement` (`showQuery()`/`sq()`, `execute()` профилирует в `Profiler`), `Services\DataBase\Classes\Profiler` (тайминги, см. также `System\Classes\ProfilerToolbar`). - **`System\Classes\Repository`** (`@template T of object`, abstract CRUD) — портирован с `eoffice_v3/System/Classes/Repository.php` (пользователь явно попросил именно этот паттерн), но НЕ один-в-один — два сознательных отличия от оригинала (см. CLAUDE.md → раздел DataBase за подробностями): 1. `getList()` по умолчанию `PDO::FETCH_CLASS` с проброшенным `$this->obj_class` (в оригинале — `PDO::FETCH_KEY_PAIR` по умолчанию, и `FETCH_CLASS` не получает `obj_class` вовсе — нестыковка PHPDoc/кода в оригинале, не повторяли). 2. Транзакции (`beginTransaction/commit/rollBack`) — тонкие делегаты в `PdoConnection` (см. выше), а не свой счётчик вложенности в каждом `Repository`, как в оригинале (там это ломается, если два репозитория на одном соединении оба вызывают `beginTransaction()` — оба думают, что «внешние»). Методы: `get()/getItemWhere()/getList()/create()/update()/delete()/deleteWhere()`, `primary_col`/`is_auto_increment` публичные, `processData()` — сборка bind-параметров с автоопределением `PDO::PARAM_*`. Конкретные репозитории — в `App/Repositories/*` (например `App\Repositories\BudgetRepository`), НЕ в `Services\DataBase` — так решил пользователь явно; таблица/класс строки задаются через конструктор (`parent::__construct('budget', Model::class)`), не переопределением свойства. - **`Services\DataBase\Model`** (`#[AllowDynamicProperties]`) — класс строки по умолчанию для `T`. Важно: свойства настоящие (не спрятаны за внутренним `$attributes`-массивом) — `PDO::FETCH_CLASS`/ `fetchObject()` выставляет их напрямую, **минуя `__set()`** (внутренний механизм PDO, до вызова конструктора). Версия со скрытым массивом (первая, что я написал) была рабочей только для ручного `new Model([...])`, но ломалась именно в главном сценарии использования — пользователь сам это поймал и прислал правильный вариант. - **Elasticsearch** — `Services\Elasticsearch` + `Services\Elasticsearch\Client`, тонкая REST-обёртка на уже существующем `System\Classes\HTTP\Client\Curl` (без тяжёлой composer-зависимости `elasticsearch/elasticsearch` — см. `feedback_educational_project.md`). - **Mongo** — `Services\Mongo` + `Services\Mongo\MongoDriver`, обёртка над `MongoDB\Client` (композер-пакет `mongodb/mongodb` уже поставлен через `composer.phar require --ignore-platform-req=ext-mongodb`, плюс `"platform": {"ext-mongodb": "2.3"}` в `composer.json`, чтобы дальнейшие `composer install` не требовали флага). - **`System\Classes\ProfilerToolbar`** — debug-панель внизу страницы (аналог ProfilerToolbar для Kohana), время/память/SQL из `Profiler`, видна только `role === 'admin'`, подключена в `App/view/layout.html`. **Профилирование — в `Statement::execute()`, не в `PdoConnection::query()` (важно, был реальный баг):** первая версия вешала таймер только на `PdoConnection::query()` через отдельный класс `ProfilerPDO::wrap()` (теперь удалён) — но `Repository::get()/getList()/create()/update()/delete()/deleteWhere()` сами делают `$this->pdo->prepare($sql); $stmt->execute();` напрямую, в обход `query()` — то есть почти все реальные запросы через `Repository` не логировались вообще (пользователь поймал это по `ProfilerToolbar`, показывавшему "SQL: 0" при заведомо выполненном запросе). Раз `PDO::ATTR_STATEMENT_CLASS` гарантирует, что через `Statement` проходит вообще любое выполнение запроса (и `query()`, и ручной `prepare()+execute()`), таймер и вызов `Profiler::log()` теперь в `Statement::execute()` — единственной точке, которую нельзя обойти. Общий урок: если добавляешь профилирование/логирование поверх PDO-обёртки — вешать его на низкоуровневый общий метод (`Statement`/`PDOStatement`), а не на удобный высокоуровневый (`PdoConnection::query()`), если у обёртки есть другие пути выполнения запроса в обход этого высокоуровневого метода. **Свойство `Repository`/`PdoConnection` называется `$pdo`** (не `$_driver`/`$_pdo` с подчёркиванием, хотя это нарушает общий стиль `_prefixed` protected-свойств проекта) — пользователь явно попросил именно так, дважды сам написал `$this->pdo` в своём коде раньше, чем я успел объяснить структуру. См. также `PdoConnection::rollback()` — с маленькой буквы (не `rollBack()`, хотя нативный `\PDO::rollBack()` — с большой); `Repository::rollBack()` (с большой буквы) — публичный метод, который делегирует в `$this->pdo->rollback()` (с маленькой) — это НЕ опечатка, два разных метода двух разных классов. **Что ждёт внешней инфраструктуры (не моя зона — пользователь занимается сам):** - `ext-mongodb` не установлен в `docker/php-apache/8.2.8/Dockerfile` (общий для нескольких проектов на хосте — мне трогать нельзя). Пользователь добавит сам. Код написан безопасно без расширения — падает только на реальном `new MongoDB\Client()` внутри `MongoDriver::__construct()`, не раньше. - Контейнеров `mongo` и `elasticsearch` в `docker-dev/docker-compose.yml` нет — пользователь поднимет сам (там уже есть закомментированный черновик блока `elasticsearch` в файле). - MariaDB — пользователь сам прописал в `App/config/config.php`/`config.local.php` реальное подключение (внешний хост, не контейнер `db` из docker-dev) — эта часть инфраструктуры уже готова пользователем, тесты по MariaDB реально гоняются, не skip. **Тесты**: `PdoConnectionTest`/`StatementTest`/`RepositoryTest` бьют по реальной MariaDB (не мокают) — гейт через `markTestSkipped()` в `setUp()` при неудачном подключении остался в коде на случай, если у кого-то ещё не настроен `config.local.php`, но сейчас реально проходят (не skip). `ElasticsearchClientTest` — через `Client::ping()`. `MongoDriverTest` — через `extension_loaded('mongodb')`. Эти два оживут сами, без правки кода, когда появится инфраструктура — сейчас (2026-08-07) 8 тестов из 239 пропущены именно поэтому. **How to apply:** При запросах, касающихся БД/моделей/репозиториев — этот слой уже есть, не писать заново с нуля, расширять существующие классы. Новый конкретный репозиторий — в `App/Repositories/* extends System\Classes\Repository`, не в `Services\DataBase`. Если пользователь присылает код из другого своего проекта («у меня так работает») как референс — не копировать один-в-один вслепую: проверять на реальные баги/нестыковки (как с `getList()`/`FETCH_CLASS` и дублированием transaction-счётчика выше) и чинить под Bicycle, если он прямо говорит «не так же, а правильно». См. также `idea_cli_entrypoint.md` — CLI-точка входа отложена именно до появления этого слоя (первый реальный потребитель — миграции; теперь, когда слой БД есть, эта идея стала актуальнее).