Get Production ready
This commit is contained in:
parent
68f3c05868
commit
6252dd6107
37
CHANGELOG.md
37
CHANGELOG.md
|
|
@ -2,23 +2,26 @@
|
||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
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`).
|
##### Added
|
||||||
- MongoDB production adapter (`ext-mongodb` + `mongodb/mongodb`) and Mongo DAO layer with relations (MVP), projections/sort/limit, pagination, and a small filter builder.
|
- **Caching Layer**: Integrated PSR-16 (Simple Cache) support into `AbstractDao` and `AbstractMongoDao`.
|
||||||
- Pagination helpers for SQL and Mongo: `paginate` and `simplePaginate`.
|
- Automated cache invalidation on write operations.
|
||||||
- Model metadata & schema mapping: column casts (incl. custom casters), timestamps, soft deletes.
|
- Identity Map synchronization for cached objects.
|
||||||
- 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.
|
- Customizable TTL and prefix per DAO.
|
||||||
- Join‑based eager loading (opt‑in, SQL, single‑level) with safe fallbacks.
|
- **CLI Enhancements**:
|
||||||
- Unit of Work (opt‑in): identity map; deferred updates/deletes; relation‑aware delete cascades; optimistic locking; snapshot diffing (flagged); identity map controls; coalescing.
|
- Extracted CLI logic into dedicated Command classes in `Pairity\Console`.
|
||||||
- Event system: DAO and UoW events; listeners/subscribers.
|
- Added `--pretend` flag to `migrate`, `rollback`, and `reset` commands for dry-run support.
|
||||||
- CI: GitHub Actions matrix (PHP 8.1–8.3) with MySQL + Mongo services; guarded tests.
|
- 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`.
|
#### [0.0.1] - 2025-12-11
|
||||||
- Tests: migrate to PHPUnit 10; update `phpunit.xml.dist` and test cases accordingly.
|
- Initial development version with core ORM, Relations, Migrations, and basic MongoDB support.
|
||||||
- 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.
|
|
||||||
|
|
|
||||||
|
|
@ -53,10 +53,10 @@
|
||||||
- [x] Eager loader IN-batching
|
- [x] Eager loader IN-batching
|
||||||
- [x] Metadata memoization
|
- [x] Metadata memoization
|
||||||
|
|
||||||
## Milestone 8: Road Ahead
|
## Milestone 8: Road Ahead [x]
|
||||||
- [x] Broader Schema Builder ALTER coverage
|
- [x] Broader Schema Builder ALTER coverage
|
||||||
- [x] More dialect nuances for SQL Server/Oracle
|
- [x] More dialect nuances for SQL Server/Oracle
|
||||||
- [x] Enhanced CLI commands
|
- [x] Enhanced CLI commands
|
||||||
- [x] Caching layer
|
- [x] Caching layer
|
||||||
- [ ] Production-ready MongoDB adapter refinements
|
- [x] Production-ready MongoDB adapter refinements
|
||||||
- [ ] Documentation and expanded examples
|
- [x] Documentation and expanded examples
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
namespace Pairity\NoSql\Mongo;
|
namespace Pairity\NoSql\Mongo;
|
||||||
|
|
||||||
|
use Pairity\Contracts\CacheableDaoInterface;
|
||||||
|
use Pairity\Orm\Traits\CanCache;
|
||||||
use Pairity\Model\AbstractDto;
|
use Pairity\Model\AbstractDto;
|
||||||
use Pairity\Orm\UnitOfWork;
|
use Pairity\Orm\UnitOfWork;
|
||||||
use Pairity\Events\Events;
|
use Pairity\Events\Events;
|
||||||
|
|
@ -11,8 +13,10 @@ use Pairity\Events\Events;
|
||||||
*
|
*
|
||||||
* Usage: extend and implement collection() + dtoClass().
|
* Usage: extend and implement collection() + dtoClass().
|
||||||
*/
|
*/
|
||||||
abstract class AbstractMongoDao
|
abstract class AbstractMongoDao implements CacheableDaoInterface
|
||||||
{
|
{
|
||||||
|
use CanCache;
|
||||||
|
|
||||||
protected MongoConnectionInterface $connection;
|
protected MongoConnectionInterface $connection;
|
||||||
|
|
||||||
/** @var array<int,string>|null */
|
/** @var array<int,string>|null */
|
||||||
|
|
@ -54,6 +58,11 @@ abstract class AbstractMongoDao
|
||||||
$this->connection = $connection;
|
$this->connection = $connection;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getTable(): string
|
||||||
|
{
|
||||||
|
return $this->collection();
|
||||||
|
}
|
||||||
|
|
||||||
/** Collection name (e.g., "users"). */
|
/** Collection name (e.g., "users"). */
|
||||||
abstract protected function collection(): string;
|
abstract protected function collection(): string;
|
||||||
|
|
||||||
|
|
@ -144,6 +153,25 @@ abstract class AbstractMongoDao
|
||||||
public function findOneBy(array|Filter $filter): ?AbstractDto
|
public function findOneBy(array|Filter $filter): ?AbstractDto
|
||||||
{
|
{
|
||||||
$filterArr = $this->normalizeFilterInput($filter);
|
$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
|
// Events: dao.beforeFind (Mongo) — allow filter mutation
|
||||||
try { $ev = ['dao' => $this, 'collection' => $this->collection(), 'filter' => &$filterArr]; Events::dispatcher()->dispatch('dao.beforeFind', $ev); } catch (\Throwable) {}
|
try { $ev = ['dao' => $this, 'collection' => $this->collection(), 'filter' => &$filterArr]; Events::dispatcher()->dispatch('dao.beforeFind', $ev); } catch (\Throwable) {}
|
||||||
$this->applyRuntimeScopesToFilter($filterArr);
|
$this->applyRuntimeScopesToFilter($filterArr);
|
||||||
|
|
@ -155,6 +183,15 @@ abstract class AbstractMongoDao
|
||||||
$row = $docs[0] ?? null;
|
$row = $docs[0] ?? null;
|
||||||
$dto = $row ? $this->hydrate($row) : null;
|
$dto = $row ? $this->hydrate($row) : null;
|
||||||
try { $payload = ['dao' => $this, 'collection' => $this->collection(), 'dto' => $dto]; Events::dispatcher()->dispatch('dao.afterFind', $payload); } catch (\Throwable) {}
|
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;
|
return $dto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -166,6 +203,33 @@ abstract class AbstractMongoDao
|
||||||
public function findAllBy(array|Filter $filter = [], array $options = []): array
|
public function findAllBy(array|Filter $filter = [], array $options = []): array
|
||||||
{
|
{
|
||||||
$filterArr = $this->normalizeFilterInput($filter);
|
$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)
|
// Events: dao.beforeFind (Mongo)
|
||||||
try { $ev = ['dao' => $this, 'collection' => $this->collection(), 'filter' => &$filterArr]; Events::dispatcher()->dispatch('dao.beforeFind', $ev); } catch (\Throwable) {}
|
try { $ev = ['dao' => $this, 'collection' => $this->collection(), 'filter' => &$filterArr]; Events::dispatcher()->dispatch('dao.beforeFind', $ev); } catch (\Throwable) {}
|
||||||
$this->applyRuntimeScopesToFilter($filterArr);
|
$this->applyRuntimeScopesToFilter($filterArr);
|
||||||
|
|
@ -180,6 +244,17 @@ abstract class AbstractMongoDao
|
||||||
$this->resetModifiers();
|
$this->resetModifiers();
|
||||||
$this->resetRuntimeScopes();
|
$this->resetRuntimeScopes();
|
||||||
try { $payload = ['dao' => $this, 'collection' => $this->collection(), 'dtos' => $dtos]; Events::dispatcher()->dispatch('dao.afterFind', $payload); } catch (\Throwable) {}
|
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;
|
return $dtos;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -192,7 +267,25 @@ abstract class AbstractMongoDao
|
||||||
return $managed;
|
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 */
|
/** @param array<string,mixed> $data */
|
||||||
|
|
@ -212,6 +305,8 @@ abstract class AbstractMongoDao
|
||||||
/** @param array<string,mixed> $data */
|
/** @param array<string,mixed> $data */
|
||||||
public function update(string $id, array $data): AbstractDto
|
public function update(string $id, array $data): AbstractDto
|
||||||
{
|
{
|
||||||
|
$this->removeFromCache($this->getCacheKeyForId($id));
|
||||||
|
|
||||||
$uow = UnitOfWork::current();
|
$uow = UnitOfWork::current();
|
||||||
if ($uow && !UnitOfWork::isSuspended()) {
|
if ($uow && !UnitOfWork::isSuspended()) {
|
||||||
$self = $this; $conn = $this->connection; $theId = $id; $payload = $data;
|
$self = $this; $conn = $this->connection; $theId = $id; $payload = $data;
|
||||||
|
|
@ -243,6 +338,8 @@ abstract class AbstractMongoDao
|
||||||
|
|
||||||
public function deleteById(string $id): int
|
public function deleteById(string $id): int
|
||||||
{
|
{
|
||||||
|
$this->removeFromCache($this->getCacheKeyForId($id));
|
||||||
|
|
||||||
$uow = UnitOfWork::current();
|
$uow = UnitOfWork::current();
|
||||||
if ($uow && !UnitOfWork::isSuspended()) {
|
if ($uow && !UnitOfWork::isSuspended()) {
|
||||||
$self = $this; $conn = $this->connection; $theId = $id;
|
$self = $this; $conn = $this->connection; $theId = $id;
|
||||||
|
|
@ -268,6 +365,10 @@ abstract class AbstractMongoDao
|
||||||
/** @param array<string,mixed>|Filter $filter */
|
/** @param array<string,mixed>|Filter $filter */
|
||||||
public function deleteBy(array|Filter $filter): int
|
public function deleteBy(array|Filter $filter): int
|
||||||
{
|
{
|
||||||
|
if ($this->cache !== null) {
|
||||||
|
$this->clearCache();
|
||||||
|
}
|
||||||
|
|
||||||
$uow = UnitOfWork::current();
|
$uow = UnitOfWork::current();
|
||||||
if ($uow && !UnitOfWork::isSuspended()) {
|
if ($uow && !UnitOfWork::isSuspended()) {
|
||||||
$self = $this; $conn = $this->connection; $flt = $this->normalizeFilterInput($filter);
|
$self = $this; $conn = $this->connection; $flt = $this->normalizeFilterInput($filter);
|
||||||
|
|
@ -297,12 +398,16 @@ abstract class AbstractMongoDao
|
||||||
/** Upsert by id convenience. */
|
/** Upsert by id convenience. */
|
||||||
public function upsertById(string $id, array $data): string
|
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]);
|
return $this->connection->upsertOne($this->databaseName(), $this->collection(), ['_id' => $id], ['$set' => $data]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @param array<string,mixed>|Filter $filter @param array<string,mixed> $update */
|
/** @param array<string,mixed>|Filter $filter @param array<string,mixed> $update */
|
||||||
public function upsertBy(array|Filter $filter, array $update): string
|
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);
|
return $this->connection->upsertOne($this->databaseName(), $this->collection(), $this->normalizeFilterInput($filter), $update);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue