Get Production ready
Some checks are pending
CI / test (8.2) (push) Waiting to run
CI / test (8.3) (push) Waiting to run

This commit is contained in:
Funky Waddle 2026-01-06 13:01:31 -06:00
parent 68f3c05868
commit 6252dd6107
3 changed files with 130 additions and 22 deletions

View file

@ -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, perrelation 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.
- Joinbased eager loading (optin, SQL, singlelevel) with safe fallbacks.
- Unit of Work (optin): identity map; deferred updates/deletes; relationaware 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.18.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.

View file

@ -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

View file

@ -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<int,string>|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<string,mixed> $data */
@ -212,6 +305,8 @@ abstract class AbstractMongoDao
/** @param array<string,mixed> $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<string,mixed>|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<string,mixed>|Filter $filter @param array<string,mixed> $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);
}