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