diff --git a/CHANGELOG.md b/CHANGELOG.md index 559ad74..5df1905 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,23 +2,26 @@ All notable changes to this project will be documented in this file. -#### Unreleased +#### [0.1.0] - 2026-01-06 -- Core ORM (DAO/DTO) with dynamic finders, `fields()` projection, relations (hasOne/hasMany/belongsTo), nested eager loading, per‑relation constraints, and SQL `belongsToMany` with pivot helpers (`attach`, `detach`, `sync`). -- MongoDB production adapter (`ext-mongodb` + `mongodb/mongodb`) and Mongo DAO layer with relations (MVP), projections/sort/limit, pagination, and a small filter builder. -- Pagination helpers for SQL and Mongo: `paginate` and `simplePaginate`. -- Model metadata & schema mapping: column casts (incl. custom casters), timestamps, soft deletes. -- Migrations & Schema Builder (portable): create/drop/alter; CLI (`vendor/bin/pairity`) with migrate/rollback/status/reset/make:migration. Drivers: MySQL/MariaDB, SQLite, PostgreSQL, SQL Server, Oracle. -- Join‑based eager loading (opt‑in, SQL, single‑level) with safe fallbacks. -- Unit of Work (opt‑in): identity map; deferred updates/deletes; relation‑aware delete cascades; optimistic locking; snapshot diffing (flagged); identity map controls; coalescing. -- Event system: DAO and UoW events; listeners/subscribers. -- CI: GitHub Actions matrix (PHP 8.1–8.3) with MySQL + Mongo services; guarded tests. +##### Added +- **Caching Layer**: Integrated PSR-16 (Simple Cache) support into `AbstractDao` and `AbstractMongoDao`. + - Automated cache invalidation on write operations. + - Identity Map synchronization for cached objects. + - Customizable TTL and prefix per DAO. +- **CLI Enhancements**: + - Extracted CLI logic into dedicated Command classes in `Pairity\Console`. + - Added `--pretend` flag to `migrate`, `rollback`, and `reset` commands for dry-run support. + - Added `--template` flag to `make:migration` for custom migration boilerplate. +- **MongoDB Refinements**: + - Production-ready `MongoClientConnection` wrapping `mongodb/mongodb` ^2.0. + - Integrated caching into `AbstractMongoDao`. + - Improved `_id` normalization and BSON type handling. +- **Improved Testing**: Added `PretendTest`, `MigrationGeneratorTest`, and `CachingTest`. -#### 0.1.0 — 2025-12-11 +##### Changed +- Updated `AbstractDto` to support `\Serializable` and PHP 8.1+ `__serialize()`/`__unserialize()` for better cache persistence. +- Refactored `bin/pairity` to use the new Console Command structure. -- Dependencies: upgrade `mongodb/mongodb` to `^2.0` (tested at 2.1.x). Requires `ext-mongodb >= 2.1`. -- Tests: migrate to PHPUnit 10; update `phpunit.xml.dist` and test cases accordingly. -- MongoDB tests: marked as `@group mongo-integration` and skipped by default via `phpunit.xml.dist` group exclusion. Each test pings the server and skips when not available. -- SQL tests: stabilized MySQL `belongsToMany` eager and join-eager tests by aligning anonymous DAO constructors with runtime patterns and adding needed projections. -- PostgreSQL: fix identifier quoting by using double quotes in `PostgresGrammar` (no more backticks in generated DDL). `PostgresSmokeTest` passes when a Postgres instance is available. -- SQLite: portable DDL in schema tests; minor assertion cleanups for accessors/casters. +#### [0.0.1] - 2025-12-11 +- Initial development version with core ORM, Relations, Migrations, and basic MongoDB support. diff --git a/MILESTONES.md b/MILESTONES.md index 255ff6b..0c1d799 100644 --- a/MILESTONES.md +++ b/MILESTONES.md @@ -53,10 +53,10 @@ - [x] Eager loader IN-batching - [x] Metadata memoization -## Milestone 8: Road Ahead +## Milestone 8: Road Ahead [x] - [x] Broader Schema Builder ALTER coverage - [x] More dialect nuances for SQL Server/Oracle - [x] Enhanced CLI commands - [x] Caching layer -- [ ] Production-ready MongoDB adapter refinements -- [ ] Documentation and expanded examples +- [x] Production-ready MongoDB adapter refinements +- [x] Documentation and expanded examples diff --git a/src/NoSql/Mongo/AbstractMongoDao.php b/src/NoSql/Mongo/AbstractMongoDao.php index 1eadac0..442919a 100644 --- a/src/NoSql/Mongo/AbstractMongoDao.php +++ b/src/NoSql/Mongo/AbstractMongoDao.php @@ -2,6 +2,8 @@ namespace Pairity\NoSql\Mongo; +use Pairity\Contracts\CacheableDaoInterface; +use Pairity\Orm\Traits\CanCache; use Pairity\Model\AbstractDto; use Pairity\Orm\UnitOfWork; use Pairity\Events\Events; @@ -11,8 +13,10 @@ use Pairity\Events\Events; * * Usage: extend and implement collection() + dtoClass(). */ -abstract class AbstractMongoDao +abstract class AbstractMongoDao implements CacheableDaoInterface { + use CanCache; + protected MongoConnectionInterface $connection; /** @var array|null */ @@ -54,6 +58,11 @@ abstract class AbstractMongoDao $this->connection = $connection; } + public function getTable(): string + { + return $this->collection(); + } + /** Collection name (e.g., "users"). */ abstract protected function collection(): string; @@ -144,6 +153,25 @@ abstract class AbstractMongoDao public function findOneBy(array|Filter $filter): ?AbstractDto { $filterArr = $this->normalizeFilterInput($filter); + + $cacheKey = null; + if ($this->cache !== null && empty($this->with) && $this->projection === null && empty($this->runtimeScopes)) { + $cacheKey = $this->getCacheKeyForCriteria($filterArr); + $cached = $this->getFromCache($cacheKey); + if ($cached instanceof AbstractDto) { + $uow = UnitOfWork::current(); + if ($uow && !UnitOfWork::isSuspended()) { + $id = (string)($cached->toArray(false)['_id'] ?? ''); + if ($id !== '') { + $managed = $uow->get(static::class, $id); + if ($managed) { return $managed; } + $uow->attach(static::class, $id, $cached); + } + } + return $cached; + } + } + // Events: dao.beforeFind (Mongo) — allow filter mutation try { $ev = ['dao' => $this, 'collection' => $this->collection(), 'filter' => &$filterArr]; Events::dispatcher()->dispatch('dao.beforeFind', $ev); } catch (\Throwable) {} $this->applyRuntimeScopesToFilter($filterArr); @@ -155,6 +183,15 @@ abstract class AbstractMongoDao $row = $docs[0] ?? null; $dto = $row ? $this->hydrate($row) : null; try { $payload = ['dao' => $this, 'collection' => $this->collection(), 'dto' => $dto]; Events::dispatcher()->dispatch('dao.afterFind', $payload); } catch (\Throwable) {} + + if ($dto && $cacheKey) { + $this->putInCache($cacheKey, $dto); + $id = (string)($dto->toArray(false)['_id'] ?? ''); + if ($id !== '') { + $this->putInCache($this->getCacheKeyForId($id), $dto); + } + } + return $dto; } @@ -166,6 +203,33 @@ abstract class AbstractMongoDao public function findAllBy(array|Filter $filter = [], array $options = []): array { $filterArr = $this->normalizeFilterInput($filter); + + $cacheKey = null; + if ($this->cache !== null && empty($this->with) && $this->projection === null && empty($this->runtimeScopes)) { + $cacheKey = $this->getCacheKeyForCriteria($filterArr); + $cached = $this->getFromCache($cacheKey); + if (is_array($cached)) { + $uow = UnitOfWork::current(); + $out = []; + foreach ($cached as $dto) { + if (!$dto instanceof AbstractDto) { continue; } + $id = (string)($dto->toArray(false)['_id'] ?? ''); + if ($uow && !UnitOfWork::isSuspended() && $id !== '') { + $managed = $uow->get(static::class, $id); + if ($managed) { + $out[] = $managed; + } else { + $uow->attach(static::class, $id, $dto); + $out[] = $dto; + } + } else { + $out[] = $dto; + } + } + return $out; + } + } + // Events: dao.beforeFind (Mongo) try { $ev = ['dao' => $this, 'collection' => $this->collection(), 'filter' => &$filterArr]; Events::dispatcher()->dispatch('dao.beforeFind', $ev); } catch (\Throwable) {} $this->applyRuntimeScopesToFilter($filterArr); @@ -180,6 +244,17 @@ abstract class AbstractMongoDao $this->resetModifiers(); $this->resetRuntimeScopes(); try { $payload = ['dao' => $this, 'collection' => $this->collection(), 'dtos' => $dtos]; Events::dispatcher()->dispatch('dao.afterFind', $payload); } catch (\Throwable) {} + + if ($cacheKey && !empty($dtos)) { + $this->putInCache($cacheKey, $dtos); + foreach ($dtos as $dto) { + $id = (string)($dto->toArray(false)['_id'] ?? ''); + if ($id !== '') { + $this->putInCache($this->getCacheKeyForId($id), $dto); + } + } + } + return $dtos; } @@ -192,7 +267,25 @@ abstract class AbstractMongoDao return $managed; } } - return $this->findOneBy(['_id' => $id]); + + if ($this->cache !== null && empty($this->with) && $this->projection === null && empty($this->runtimeScopes)) { + $cacheKey = $this->getCacheKeyForId($id); + $cached = $this->getFromCache($cacheKey); + if ($cached instanceof AbstractDto) { + if ($uow && !UnitOfWork::isSuspended()) { + $uow->attach(static::class, (string)$id, $cached); + } + return $cached; + } + } + + $dto = $this->findOneBy(['_id' => $id]); + + if ($dto && $this->cache !== null && empty($this->with) && $this->projection === null && empty($this->runtimeScopes)) { + $this->putInCache($this->getCacheKeyForId($id), $dto); + } + + return $dto; } /** @param array $data */ @@ -212,6 +305,8 @@ abstract class AbstractMongoDao /** @param array $data */ public function update(string $id, array $data): AbstractDto { + $this->removeFromCache($this->getCacheKeyForId($id)); + $uow = UnitOfWork::current(); if ($uow && !UnitOfWork::isSuspended()) { $self = $this; $conn = $this->connection; $theId = $id; $payload = $data; @@ -243,6 +338,8 @@ abstract class AbstractMongoDao public function deleteById(string $id): int { + $this->removeFromCache($this->getCacheKeyForId($id)); + $uow = UnitOfWork::current(); if ($uow && !UnitOfWork::isSuspended()) { $self = $this; $conn = $this->connection; $theId = $id; @@ -268,6 +365,10 @@ abstract class AbstractMongoDao /** @param array|Filter $filter */ public function deleteBy(array|Filter $filter): int { + if ($this->cache !== null) { + $this->clearCache(); + } + $uow = UnitOfWork::current(); if ($uow && !UnitOfWork::isSuspended()) { $self = $this; $conn = $this->connection; $flt = $this->normalizeFilterInput($filter); @@ -297,12 +398,16 @@ abstract class AbstractMongoDao /** Upsert by id convenience. */ public function upsertById(string $id, array $data): string { + $this->removeFromCache($this->getCacheKeyForId($id)); return $this->connection->upsertOne($this->databaseName(), $this->collection(), ['_id' => $id], ['$set' => $data]); } /** @param array|Filter $filter @param array $update */ public function upsertBy(array|Filter $filter, array $update): string { + if ($this->cache !== null) { + $this->clearCache(); + } return $this->connection->upsertOne($this->databaseName(), $this->collection(), $this->normalizeFilterInput($filter), $update); }