cryonighter/formula-doctrine
Hibernate-style #[Formula] computed fields for Doctrine ORM 3 entities. Adds read-only, SQL-computed properties populated via subqueries, aggregations, and joins, without N+1 queries. Works standalone or with a Symfony bundle.
orderCount, totalRevenue, lastOrderDate, CSAT-style rollups — first-class entity properties populated by an embedded SQL/DQL subquery. It directly enables features that previously needed either a per-row query (N+1), a hand-written DTO projection, or a nightly ETL/reporting table: customer health badges, admin list sorting by lifetime value, segmentation thresholds, “inactive customer” ordering, revenue tiers in a single result set.SqlWalker/hydrator, a projection DTO layer, or a materialized view plus refresh job. This package is a ~1–2 day spike versus an estimated 1–2 weeks of internal implementation for the same behaviour. The decision is therefore borrow-and-own, not buy-and-trust: plan to vendor or fork it into an internal namespace rather than depend on upstream support.WHERE, ORDER BY, GROUP BY, HAVING, CASE WHEN and standard aggregates, it unblocks roadmap items that are currently gated on “we need a reporting store first”: filterable metrics, ranked leaderboards, threshold-based categorization. It does not replace a warehouse — it defers the need for one for small/medium datasets.SELECT of that entity, with no opt-in/opt-out. That is a genuine product trade-off: cheap for detail pages and admin screens, potentially expensive for high-traffic list endpoints where the metric isn’t displayed. This should be an explicit decision recorded against the affected endpoints, not an incidental one.Consider it when all of the following hold:
cryonighter/formula-doctrine-bundle wires the stack via DI and removes the manual bootstrap).Look elsewhere when:
{this} placeholder and a parentheses convention (native SQL must be wrapped, DQL must not) — a real footgun that will be misused without review.To executives (one paragraph): We can ship “customer health” style metrics — order counts, lifetime value, last activity, revenue tiering — without standing up a reporting database or writing a bespoke aggregation layer. Instead of one database round-trip per row, the metric is computed inside the main query, which is cheaper and simpler. The catch is that the library is an early-stage, single-maintainer open-source project with no known adopters, so the recommendation is a short, time-boxed pilot on one entity, with a deliberate plan to fork the code internally if it proves out. Cost to evaluate is days, not weeks; the main risk is maintenance ownership, not licensing (MIT).
To engineering leads: Declarative #[Formula] attribute on Doctrine 3 entities, registered through a DBAL FormulaMiddleware plus loadClassMetadata/postGenerateSchema listeners. Formula fields become non-insertable, non-updatable mapped fields, so the standard ObjectHydrator populates them and they never appear in INSERT/UPDATE; the schema listener strips them from generated DDL. The subquery is embedded once per query regardless of how many clauses reference it, and it works transparently with DQL, QueryBuilder, find/findBy/findAll, eager associations and lazy proxies — no changes to calling code. Ask engineering to verify: emitted SQL on our target driver, behaviour under pagination and with setMaxResults, cost on our largest list endpoint, and whether the always-on subquery is acceptable there. Treat the attribute as an infrastructure detail behind repository methods, not something the domain layer references directly.
Recommendation: Approve a 1–2 day spike on a single non-critical entity in a Doctrine-based service. Success criteria: correct values across find, QueryBuilder, and aggregate/GROUP BY queries; no measurable regression on the target endpoint’s p95; no INSERT/UPDATE leakage. If it passes, vendor the package (pinned commit, internal namespace, integration tests around generated SQL) rather than consuming it upstream. If the stack is Laravel/Eloquent, close the evaluation — the pattern is worth borrowing, the package is not applicable.
How can I help you explore Laravel packages today?