Bicycle/.claude/memory/roadmap.md
Egor Isaev 69ce89a695 dev
2026-08-11 17:05:39 +03:00

11 KiB
Raw Permalink Blame History

name description metadata
roadmap Статус слоя БД (MariaDB/Mongo/Elasticsearch) в Bicycle — что реализовано, что ждёт внешней инфраструктуры
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-точка входа отложена именно до появления этого слоя (первый реальный потребитель — миграции; теперь, когда слой БД есть, эта идея стала актуальнее).