Bicycle/.claude/memory/roadmap.md
Egor Isaev 9ff9cc54e6 dev
2026-08-07 12:34:27 +03:00

92 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
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\PdoDriver` (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`) — тонкие делегаты в `PdoDriver` (см. выше), а не свой
счётчик вложенности в каждом `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()`, не в `PdoDriver::query()` (важно, был реальный баг):**
первая версия вешала таймер только на `PdoDriver::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`), а не на удобный высокоуровневый (`PdoDriver::query()`),
если у обёртки есть другие пути выполнения запроса в обход этого высокоуровневого метода.
**Свойство `Repository`/`PdoDriver` называется `$pdo`** (не `$_driver`/`$_pdo` с подчёркиванием, хотя это
нарушает общий стиль `_prefixed` protected-свойств проекта) — пользователь явно попросил именно так,
дважды сам написал `$this->pdo` в своём коде раньше, чем я успел объяснить структуру. См. также
`PdoDriver::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.
**Тесты**: `PdoDriverTest`/`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-точка входа отложена именно до появления этого слоя (первый реальный потребитель — миграции; теперь,
когда слой БД есть, эта идея стала актуальнее).