versaorm/versaorm-php
VersaORM is a lightweight, expressive PHP ORM featuring a fluent query builder, active-record models, relationships, and schema migrations. It supports popular databases and keeps your data layer clean without heavy dependencies or complex configuration.
Architecture fit. VersaORM is a standalone PDO-backed ORM with its own fluent builder, its own connection handle, and its own Active Record base class. It ships no service provider, no facade, no DB manager driver, and no Eloquent bridge — so it does not slot into the framework's data layer, it sits beside it. That is the central architectural fact and it should drive the product decision: this is a second data layer for a narrow workload, not a replacement for Eloquent and not a general-purpose persistence strategy. A realistic fit is one of two shapes: (a) pgvector similarity search on PostgreSQL 16, or (b) hand-tuned analytical/reporting reads where DB::select with a heredoc would otherwise be the alternative. Anything involving domain CRUD, relationships, events, or transactions stays in Eloquent, which means two PDO connections per request in the common case.
Integration feasibility. Low-to-moderate effort, high ambiguity. There is no first-party Laravel integration, so wiring is a manual singleton in AppServiceProvider plus a config/versaorm.php sourced from the same env vars as config/database.php. The only complete end-to-end sample is example/app/ — and its BaseModel was broken until 2.0.1 because it overrode the now-final find(). Two items must be resolved in the first hour of any spike: (1) the Composer package name is genuinely ambiguous (versaorm/versaorm-php in the listing vs. kriollo/versa-orm in the repo) — confirm against Packagist before planning install steps; (2) pin 2.0.1 literally, not ^2.0, because 2.0.0 shipped a correctness bug.
Technical risk. The dominant risk is not breakage — it is silent wrong answers. In 2.0.0, selectRaw()/orderByRaw() dropped their bindings; PostgreSQL threw, but SQLite and MySQL returned NULL in a result column with no exception. That is the failure mode to plan around, and it is a data-integrity risk, not an availability risk, which makes it materially more expensive. Compounding factors:
# is treated as a MySQL comment start unless preceded by <. That filter was tightened in 2.0.1 to unblock pgvector's <#> operator, and it must be re-reviewed on every upgrade.SoftDeletes, no casts, no mutators, no withCount, no find() override (final since 1.8.8). Each is a scoped decision, not a surprise to discover mid-sprint.public; the DB-level GRANT is insufficient and every CI job dies without GRANT ALL ON SCHEMA public.Key questions to answer before committing roadmap or headcount:
DB::select?Stack fit. Adopt only where Laravel is genuinely awkward: vector similarity retrieval, cohort/funnel arithmetic, weekly revenue rollups, CSV/report exports for admin screens. Keep Eloquent for CRUD, relationships, policies, factories, casts, Nova, and all writes and transactions — if a vector insert and a domain write must be atomic, they must share one connection, and straddling two stacks inside a transaction produces partial commits that are hard to explain after the fact. Resolve the ORM lazily inside the controllers, jobs, and commands that need it; keep it out of hot CRUD paths.
Migration path. Do not start by porting a domain model. Start with a read-only spike against a restored copy of production, and gate it on three environment facts confirmed up front: PostgreSQL major version, vector extension installed, and GRANT ALL ON SCHEMA public applied. Use 127.0.0.1, not localhost — PdoConnection has defaulted to TCP since 1.9.0 and localhost can resolve to a Unix socket and fail with a confusing error. Prove one query end-to-end (similarity search or an aggregate), then wrap it in a repository.
Compatibility. Assume zero framework features and plan each one explicitly. Concretely: hand-roll LengthAwarePaginator construction from a count plus limit/offset fetch on every listing view; write schema changes (vector columns, HNSW/IVFFlat indexes, grants) as Laravel migrations, since VersaORM is not a schema tool; confine env() to config files, doubly so here because the package reads a plain array you build and config:cache will silently return null elsewhere. Respect the hard binding-order rule (SELECT → WHERE → GROUP BY → HAVING → ORDER BY) — if a value appears in both SELECT and ORDER BY, supply it twice. And note that select() replaces the column list wholesale, so calling it after selectVectorSimilarity() erases the similarity expression.
Sequencing.
2.0.1, wire the singleton, read example/app/config.php, example/app/BaseModel.php (do not copy the find() override), tests/unit/Regression/ (19 tests, 14 of which fail against 1.9.0 — the closest thing to a behavioural spec), and the 2.0.1 changelog end to end.app/Queries/ or app/Repositories/. Assert non-null on every computed column.NULLs elsewhere, which is exactly the asymmetry that makes "passed in staging" untrustworthy.versaorm.readonly), since the package has no read/write splitting; an Artisan reindex command; a queued backfill job resolved from the container.Maintenance. Ongoing cost is meaningfully above a typical Composer dependency: you own the upgrade gate, the security review of the # comment heuristic, the changelog-as-correctness-notes discipline, and every gap the framework normally fills. Budget a recurring line item — roughly a day per upgrade cycle plus test-suite revalidation — and name an owner. If no one owns it, it will be upgraded opportunistically during a dependency sweep and that is precisely how a silent-corruption regression reaches production.
Support. Effectively none, in the commercial sense. Zero dependents means no community corpus: no Stack Overflow answers, no blog posts, no third-party packages. Internal knowledge is whatever the spike team writes down. Mitigate by keeping every ->table() chain inside app/Queries//app/Repositories/ so the abstraction boundary is a real exit ramp, and by documenting the pin rationale so the next engineer does not "helpfully" bump to ^2.0.
Scaling. Two dimensions. Throughput: two PDO connections per request is additive but not free; keep the singleton out of hot paths and resolve lazily. Under Octane or long-lived workers, a singleton holding a PDO handle survives across requests — usually fine, but if stale-connection or "server has gone away" errors appear, bind scoped/transient rather than singleton. Data volume: pgvector with a proper HNSW index is comfortable into the low millions of vectors; past that, revisit the "no separate vector database" premise before the roadmap depends on it.
Failure modes, ranked by expected cost:
NULL in a computed column, no exception, on SQLite/MySQL. Countermeasures: pre-execution SQL+binding logging in local/test, non-null assertions on every computed column, and engine parity in CI. Any production exposure on 2.0.0 is a data audit against a source of truth, not a version bump.localhost-as-socket resolution, missing GRANT ALL ON SCHEMA public on PG 15+, cached config returning null for a stray env(). All preventable in setup scripts.#-handling change could re-break <#> or, worse, weaken injection defenses. Re-check every upgrade.Ramp-up. A mid-level Laravel developer becomes productive in roughly half a day if they read the regression suite and the 2.0.1 changelog first, and roughly a week of avoidable thrash if they don't — the package's own maintainers lost three tests to $orm->getAll() (which does not exist; row retrieval lives on the builder chain) and to calling select() after selectVectorSimilarity(). Frame onboarding explicitly as "a specialised query library for one workload," not "a lighter Eloquent," because the second framing invites a model port that will hit the final find(), the missing casts/timestamps, and the absent pagination all in the same sprint.
Bottom line. Viable as a bounded, read-only, non-correctness-critical accelerator for pgvector or analytical reads, contingent on a two-week spike, a named upgrade owner, and repository wrapping. Not viable as a data-layer replacement, not viable where a numeric result must be right without an independent assertion, and not viable without accepting single-maintainer supply-chain risk.
How can I help you explore Laravel packages today?