ascetic-soft/rowcast
Lightweight DataMapper over PDO for PHP 8.4+. Maps database rows to DTOs and back using reflection, with auto or explicit mapping, type conversion, and a fluent query builder featuring dialect-aware UPSERT.
What it actually is. Rowcast is a framework-agnostic PHP 8.4 library (PDO + reflection) for mapping DTOs to and from database rows, with a fluent query builder and dialect-aware UPSERT. It is not a Laravel package: there is no service provider, facade, config file, publishable assets, or Artisan surface. In a Laravel codebase it is a parallel persistence path that sits beside Eloquent and the DB facade, not a replacement for either. Product framing should be "a low-level persistence utility we may consume in narrow places," not "an ORM alternative."
Architecture fit. Good fit where the codebase already treats domain objects as plain DTOs and wants reads/writes without model lifecycle baggage: bulk import/export, ETL, reporting read models, legacy-schema access, large batch writes. Poor fit for anything needing relationships, events/observers, soft deletes, global scopes, multi-tenant filtering, or model-level authorization — none of which exist here, and rebuilding them would defeat the purpose.
Positive quality signals. PHPStan level 9, Codecov, CI badge, make ci gate documented, dialect logic centralized in src/QueryBuilder/Dialect/, changelog and releases present, bilingual docs plus llms.txt, and unusually candid in-repo agent notes. The code discipline reads better than the adoption metrics suggest.
Integration feasibility. Mechanically trivial: composer require, construct Connection, construct DataMapper, bind both as singletons. Roughly half a day including a service provider you write yourself. The actual cost is convention-setting — mapping ownership, converter registry, save() vs upsert() policy — not installation.
Technical risk.
Mapping. Explicitly documented as mutable and configured in place. This is a standing code-review tax and a footgun for shared instances across services.save() is read-before-write. Two statements, a race window, and no conflict constraint required. upsert() is the efficient path but depends on dialect and schema. Teams will pick the wrong one without a written rule.mysql, pgsql, sqlite are mapped; everything else silently falls back to GenericDialect. ILIKE/NOT ILIKE are Postgres-only — a portability trap if we test on Postgres and run on MySQL.findAll materializes; iterateAll exists but streaming behavior needs verification against a real cursor.Key questions for the maintainer/owner decision.
Stack fit. Works anywhere PDO does, which means zero coupling to the Laravel version — a genuine plus during framework upgrades. The trade is that we own the wiring: a ~30-line service provider binding Connection and DataMapper as singletons, plus config for DSN, nested-transaction default, and the converter registry.
Where to use it (and where not to).
| Use case | Fit | Rationale |
|---|---|---|
| Bulk import / ETL / backfills | Strong | batchInsert/batchUpsert with automatic bind-parameter chunking (SQLite 999) inside one transaction is the core value |
| Reporting read models over legacy schemas | Strong | findAll/iterateAll + DTO hydration without inventing models |
| API response projections | Moderate | Avoids Eloquent overhead, but only worth a second persistence idiom if volume justifies it |
| Domain aggregate writes | Poor | No events, relationships, scopes, soft deletes, or concurrency control |
| Anything multi-tenant | Poor | No scoping mechanism; tenancy would be hand-rolled on every query |
Migration path — strangler, per job, no big bang. Introduce Rowcast as a new path inside one bounded background job or one read-only endpoint. Keep Eloquent as the system of record for domain writes. Establish an explicit ownership rule: a given table is written by exactly one layer. Mixing Rowcast writes and Eloquent writes on the same aggregate without that rule is how stale-model bugs get born.
Wrap it. Put Rowcast behind an internal interface (e.g. BulkWriter, WarehouseReader) with the mapper injected privately. Given zero dependents and a single maintainer, this is the highest-leverage TPM decision in the whole assessment: it converts a vendor risk into a swap cost of a few days instead of a rewrite.
Compatibility decisions to make up front.
ILIKE to a MySQL deployment.DateTimeConverter formats to UTC strings — that is an opinionated decision we are inheriting, not a neutral default. Validate JSON, bool (0/1), enum, and nullability behavior against our actual schema before trusting hydration.Sequencing.
Mapping instances built once at setup and treated as immutable; a static-analysis or review rule banning in-place mutation; a written save() vs upsert() policy.onBeforeQuery / onAfterQuery into the existing logging and metrics pipeline at adoption time.Maintenance. Tiny surface, zero runtime dependencies (PDO only), so dependency drift and CVE exposure are near zero. That is the good news. The bad news is that if upstream goes quiet, we own the bug fixes. Budget roughly 1–2 engineer-days per quarter as an ongoing stewardship tax, plus a named contingency: fork-and-vendor the repo internally if release cadence stalls.
Support. No vendor, no SLA, no commercial backing, community of effectively zero. Issues are best-effort. Name a specific owning team and a specific individual; "the platform team" is not an owner. In exchange, the documentation is better than typical for a zero-star project — bilingual docs, an llms.txt for AI-assisted development, and agent notes that describe CI order and code layout, which materially lowers the cost of internal fork maintenance.
Scaling. Horizontal scaling is a property of PDO and the database, not of this library. The library-specific scaling concerns are: result-set materialization in findAll (use iterateAll for large reads, and verify it is a true cursor rather than an eagerly built array); reflection overhead in tight hydrate/extract loops (measure, don't assume); and chunk sizing for batch operations, which governs bind-parameter limits and transaction size but not round-trip latency. Tune chunk sizes empirically per driver.
Failure modes to instrument and document.
bool ↔ 0/1, JSON decode failures, timezone assumptions in the DateTime converter, non-nullable typed properties receiving NULL.save() read-before-write races under concurrency; wrong choice between save() and upsert().UserDto → user_dtos, not users. Cheap to hit, embarrassing in production.GenericDialect fallback masking unsupported-driver behavior instead of failing loudly.Ramp-up. The API is small enough for a mid-level engineer to be productive in a day; the docs plus llms.txt accelerate that further. The real ramp is not the API but the conventions — mapping ownership, converter registry behavior, and the save/upsert policy — and none of the team will have production mileage on it. The pilot is the training, which is another argument for piloting on a non-critical background job rather than a customer-facing path. Onboarding cost is low; confidence cost is only paid down by running it.
Observability. The query event hooks (onBeforeQuery, onAfterQuery with duration and exception) are the substitute for Eloquent's event surface. They must be wired into existing logging/metrics during the pilot — retrofitting observability after an incident is the standard way small libraries become mysterious ones.
How can I help you explore Laravel packages today?