What we would actually be buying. The package converts Eloquent query builder state into human-readable English. It is a presentation/serialization concern, not a data-access concern: it reads the builder's wheres, joins, orderings, and limits and renders them as prose. That framing matters for scoping — the value is in admin UX, audit/explainability surfaces, and debugging, not in application logic. It does not change how queries execute, which keeps blast radius small.
Architecture fit. Structurally this is a low-risk add: a Laravel service-provider package with auto-discovery, invoked explicitly at the point where we want prose. It should not wrap or decorate the global query builder, and we must confirm that — a package that hooks Builder macros or fires on every query would impose overhead and failure surface across the whole app rather than one feature. If invocation is opt-in and per-call, it fits cleanly behind our own QueryDescriber interface as an infrastructure adapter.
Where it plausibly earns its place.
Where it does not. It should never be the authoritative record of what a query did. A generated sentence is a derived view — store the canonical query definition and version, and render the prose on read. Anything compliance-adjacent (e.g. "why was this record exported") must be reproducible and hash-stable, which a heuristic prose renderer is not.
Technical risk, ranked.
where closures with AND/OR grouping, whereHas/whereRelation, subqueries, union, whereRaw, whereIn with large arrays, JSON path predicates, and when() conditionals are where prose generation gets hard. Grouping/parenthesization is the classic failure — "A and B or C" is ambiguous English and a correctness bug.$query->wheres, binding structure) change across Laravel majors. A small package tracking those internals is a recurring upgrade tax.Key questions for the spike.
Honest counter-position. The library is small enough that the realistic alternative is a ~150–300 line in-house describer over our own known filter DSL, which we control and can localize. The build-vs-buy call should be made after the spike, on the strength of its predicate-tree output and edge-case handling, not on the prose strings alone.
Stack fit. Native Laravel: composer require, package auto-discovery, optional publishable config. No new infrastructure, no queue, no datastore, no network calls at render time. That is about as cheap an integration as exists. Confirm declared constraints against our current Laravel/PHP targets before committing — a missing major-version constraint is the most likely reason to defer.
Sequencing (2-week envelope).
whereHas, subqueries, raw clauses). Score each output as correct / ambiguous / wrong / throws. Ambiguity and wrongness rates gate everything downstream.composer audit and dependency-tree review.QueryDescriber interface returning a structured result { prose, supported: bool, unsupportedClauses: [] }. Callers depend on our interface, never the package namespace. This is the single most important integration decision: it makes the dependency swappable and lets us fall back to raw filter chips when support is partial.Migration path. None required — this is additive capability, not a replacement. No data migration, no schema change, no backfill. If we later swap to an in-house describer, only the adapter implementation changes; call sites and the interface stay fixed. That is the whole reason for the adapter layer.
Compatibility notes.
composer.json and let our lockfile freeze it; do not float. Revisit only on deliberate Laravel upgrades.Maintenance. Low ongoing cost if we own the adapter, because the dependency is small and isolated. The real maintenance burden is the upgrade tax: every Laravel major bump requires verifying the package still reads Eloquent's internal query representation correctly. Budget a recurring check per major upgrade, and treat "package unmaintained at next major" as a planned-for scenario, not a surprise — the adapter means the mitigation is a contained rewrite, not an incident.
Support. New surface for support tickets: "the description is wrong." Route these to engineering with the raw query attached. The adjacent raw-query toggle in the UI is the primary deflection mechanism and should be non-negotiable in the pilot. No vendor support exists — single maintainer, no SLA — so all support load is ours.
Scaling. Rendering is CPU-bound string assembly over an in-memory builder. Cost is negligible relative to the query itself, but it is per-render, so memoize prose keyed by a hash of the query definition on any list view that renders many descriptions. Do not render descriptions inside loops over result sets — that is the one pattern that turns a cheap feature into a latency problem.
Failure modes to design for.
supported flag and the raw-query escape hatch.Ramp-up. Developer ramp-up is near zero — an afternoon. The real ramp-up is organizational trust: getting product, support, and any compliance stakeholders comfortable that the prose is a convenience view, not an attestation. That conversation, and agreeing that we always store the canonical query alongside, is worth having in week one rather than after the pilot ships.
Recommendation framing. Proceed to a two-day spike plus pilot on a single read-only admin surface under a feature flag, contingent on: (a) opt-in invocation only, (b) explicit unsupported-construct signalling, (c) adapter isolation, (d) raw-query toggle present. If the spike shows a meaningful wrong-or-ambiguous rate on our real query shapes, pivot to a scoped in-house describer over our own filter DSL — we would own it, localize it, and control coverage on exactly the constructs our product allows.
How can I help you explore Laravel packages today?