happyr/doctrine-specification
Happyr Doctrine Specification brings the Specification pattern to Doctrine: compose small, reusable, testable specification classes into query expressions instead of bloated repositories with long QueryBuilder calls and duplicated logic.
Architecture fit
This is a small, single-purpose library that applies the Specification pattern to Doctrine ORM query building. It targets a real and well-understood pain point: repositories that accumulate dozens of parameterized findX() methods with duplicated QueryBuilder logic that cannot be composed. The library converts query predicates into composable value objects (Spec::andX, Spec::orX, Spec::eq, Spec::join, Spec::isNull, and so on), which can then be applied through a repository match() call or evaluated in-memory via isSatisfiedBy() and filterCollection().
Architecturally, this fits best in a layered PHP application where read paths are non-trivial and business rules are reused across use cases (e.g. "adverts we should close" AND "owned by company"). The in-memory satisfiability API is the strategically interesting part for a TPM: it lets the same rule be enforced at query time in SQL and validated against an entity in a unit test, removing the database from part of the test loop. That is a testability and defect-prevention argument, not just a code-cleanliness one.
The trade-off is that it is an abstraction over the QueryBuilder, not a query language. Any specification you cannot express through the provided combinators either escapes the abstraction or forces custom Specification classes. It also deliberately hides ORM implementation details, which is an asset for teams that keep producing inconsistent DQL, and a liability for teams with genuinely complex fetching (multi-root joins, WITH clauses, native SQL, window functions).
Integration feasibility
Integration is cheap and reversible. It is a Composer package with no service container requirements, no configuration files, no database schema changes, and no runtime infrastructure. Adoption is essentially: require the package, make the target repository extend the provided base repository or add a match() method, and write specs. A single read path can be converted in an afternoon and evaluated in a branch.
The main gating factors are version alignment, not effort. The library's 2.x line required Doctrine ORM 2.5+ and PHP 7.2+ at the time of the 2.0 release; 2.2.0 added Doctrine ORM 3 support while explicitly restoring ORM 2 compatibility. This is a positive signal — the maintainers responded to the ecosystem's most significant breaking upgrade — but it is also where most integration risk lives, because a project pinned to an older Doctrine or an older PHP will have to reconcile constraints before the first commit.
Technical risk
EntitySpecificationRepository and Having, abstracted LogicX/Comparison, moved classes into a Repository namespace). Teams that adopted 1.x paid a real migration tax. Assume a 3.x will arrive and budget accordingly.Key questions
FROM, or vendor-specific DQL that this cannot express?isSatisfiedBy() path becomes an enforcement point and must be covered by tests.Stack fit
This is a native fit for any Laravel application using Doctrine ORM directly — typically via LaravelDoctrine/orm or a Doctrine bundle equivalent. It is not relevant to Laravel's default Eloquent stack. A TPM must establish this early, because "Laravel/PHP package" in a catalogue entry can mislead a stakeholder into assuming Eloquent applicability. If the product uses Eloquent with a Doctrine island for reporting or a legacy module, the scope of adoption is that island only, which materially reduces the business case.
Where it fits: read-heavy domains with repeatable filter combinations — catalog/search, eligibility and entitlement checks, moderation queues, saved-segment reporting. Where it does not: simple CRUD, single-use queries, or teams already standardized on API Platform filters or a CQRS/read-model layer that owns query construction.
Migration path
Adopt incrementally, never as a rewrites. Recommended path:
match($spec) and one test to isSatisfiedBy(); confirm results are identical against a seeded fixture.Existing repository methods should be left untouched until a spec naturally displaces them. A "convert all repositories" mandate is the failure mode here: high effort, no user-visible benefit, and a large diff for reviewers.
Compatibility
Count, Concat, Trim declarations were added or corrected in 2.x); duplicate registration is a runtime failure, not a compile-time one.Sequencing
Doctrine ORM upgrade (if pending) → package install and spike → one production read path → test-suite adoption of isSatisfiedBy() → coding standard / ADR documenting when to write a spec versus plain DQL → broader retrofit. The ADR step is the one most often skipped; without it, some engineers embrace the abstraction for everything and others ignore it, and the repository ends up with two idioms.
Maintenance
Ongoing maintenance is genuinely light: no services, no schema, no caches, no daemons. Cost concentrates in two places — keeping the Composer constraint current across Doctrine and PHP upgrades, and reviewing new spec classes for correct alias and parameter handling. Budget a small recurring line item for dependency-upgrade work and a named owner for the package relationship.
Support
Community-only. GitHub issues, MIT licence, best-effort maintainer response. There is no vendor to escalate a production DQL failure to. Mitigation: pin to a known-good minor, keep the package at a leaf of the dependency graph so it can be replaced, and require a fixture-backed test for every specification that encodes a business rule. The specs themselves are unit-testable without a database, which is the cheapest form of support insurance available here.
Scaling
The library adds no runtime overhead beyond object construction and QueryBuilder assembly, so database performance is unchanged — good news, and worth stating plainly to anyone who hears "abstraction layer" and assumes a tax. Scaling concerns are therefore application-level: specifications must still produce index-friendly predicates, must not encourage N+1 through naive joins, and must not be used to build deeply nested andX/orX trees that generate unreadable, unindexable SQL. Review specs the way you review SQL.
Operational scale of the codebase is the real target: this exists to stop repository classes becoming 20-function, many-parameter dumping grounds. The measurable benefit is fewer duplicated filter implementations and fewer bugs where two "active user" definitions drift apart.
Failure modes
isSatisfiedBy() implementation and the SQL path diverge, authorization or eligibility checks pass in one and fail in the other. This is the highest-severity risk in the package and deserves explicit test pairing.Ramp-up
For a PHP/Doctrine developer, the mental model is a few hours: the README and the two docs pages (0-usage, 1-creatingSpecs) are sufficient to write first specs. Composition (andX/orX over new Spec(...)) is the only concept needing reinforcement. Realistically plan one day for a mid-level developer to be productive and one week before they write specs others should imitate. Senior reviewers need a documented convention for "spec vs. plain DQL" or the codebase will fork into two idioms within a quarter.
Overall: low-cost, low-infrastructure, moderate-value tool whose justification rests on duplicated business rules and testability rather than performance. Approve as a scoped experiment on one or two read paths, with a decision review after the first production spec ships.
How can I help you explore Laravel packages today?