ncphillips / laravel-doctrine-query
Ergonomic querying traits for Doctrine Entities in Laravel
Package info
github.com/ncphillips/laravel-doctrine-query
pkg:composer/ncphillips/laravel-doctrine-query
Requires
- php: ^8.2|^8.3|^8.4
- laravel-doctrine/orm: ^3.0
- laravel/framework: ^12.0|^13.0
Requires (Dev)
- laravel/pint: ^1.29
- mockery/mockery: ^1.6
- orchestra/testbench: ^10.0|^11.0
- pestphp/pest: ^v3.1.0
This package is auto-updated.
Last update: 2026-07-27 14:09:21 UTC
README
Ergonomic querying traits for Doctrine entities in Laravel.
This package gives Doctrine entities an Eloquent-flavoured static query API so you can write
User::find(1) instead of reaching for the EntityManager and a repository at every call site.
It also provides a MagicQueryBuilder that exposes shorthand where*, andWhere*, orWhere*,
and is* methods for each mapped field, reducing DQL boilerplate.
Installation
composer require ncphillips/laravel-doctrine-query
The service provider is registered automatically via Laravel package discovery.
Usage
Add the Queryable trait to a Doctrine entity:
use Doctrine\ORM\Mapping as ORM; use Ncphillips\LaravelDoctrineQuery\Queryable; #[ORM\Entity] class User { use Queryable; // ... }
You can then query it statically:
User::all(); // array<User> User::find($id); // ?User User::findBy(['admin' => true]); // array<User> User::findOneBy(['name' => 'Ada']); // ?User User::count(['admin' => true]); // int User::query() // MagicQueryBuilder, aliased "user" ->isAdmin() // shorthand for ->andWhere('user.admin = :admin') with true ->getQuery() ->getResult();
The EntityManager is resolved from the Laravel container on demand, so entities stay free of any constructor wiring.
Magic Query Builder
query() returns a MagicQueryBuilder that converts property-based method calls into DQL.
For every Doctrine mapped field and to-one association, the builder generates:
where{Property}($value)—WHERE entity.property = :propertyandWhere{Property}($value)— chains withANDorWhere{Property}($value)— chains withORis{Property}($value)— boolean shorthand, defaults totrue. Intended for boolean fields; the runtime does not enforce that, but the ide-helper only generatesis*stubs for them. Both naming styles resolve to the same method:isAdmin()matches a property namedisAdminif one exists, otherwise a property namedadmin— soisIsAdmin()is never a thing.
Passing an array to any where* method produces a WHERE ... IN clause instead:
User::query()->whereName(['Bob', 'Doug']); // WHERE user.name IN (:name)
Post::query() ->whereTitle('Hello') ->andWherePublished(true) ->isPublished() // same as isPublished(true) ->isPublished(false) // negate ->orderBy('post.createdAt', 'DESC') ->getQuery() ->getResult(); User::query() ->whereName('Bob') ->orWhereName('Doug') // WHERE user.name = :name OR user.name = :name_2 ->getQuery() ->getResult();
Repeated clauses on the same property bind distinct parameters (:name, :name_2, ...), so
each keeps its own value.
Operator suffixes
Appending an operator suffix to the property name produces the matching comparison instead
of equality. Every suffix works with where, andWhere, and orWhere:
| Method | DQL |
|---|---|
whereTitleNot($value) |
post.title != :title |
whereIdGreaterThan($value) |
post.id > :id |
whereIdGreaterThanOrEqual($value) |
post.id >= :id |
whereIdLessThan($value) |
post.id < :id |
whereIdLessThanOrEqual($value) |
post.id <= :id |
whereTitleLike($pattern) |
post.title LIKE :title |
whereTitleNotLike($pattern) |
post.title NOT LIKE :title |
whereUserIsNull() |
post.user IS NULL |
whereUserIsNotNull() |
post.user IS NOT NULL |
whereIdBetween($min, $max) |
post.id BETWEEN :id AND :id_2 |
whereTitleNotIn($values) |
post.title NOT IN (:title) |
Passing an array to a where*Not method produces NOT IN, mirroring how a plain where*
treats arrays as IN.
If a property's own name happens to end in an operator word, the exact property match wins and the method stays an equality clause.
Unknown methods and non-mapped property names throw a BadMethodCallException at runtime.
IDE Autocomplete
The magic builder methods work at runtime without any setup, but your IDE won't know about them automatically. For autocomplete, run the generator:
php artisan doctrine:query:ide-helper
This produces _ide_helper_doctrine.php with:
- Builder subclasses (
PostMagicQueryBuilder,UserMagicQueryBuilder, etc.) — phantom classes extendingMagicQueryBuilderwith@methodannotations for every mapped field and to-one association on each entity. - Entity stubs — phantom classes extending each real entity with a
@method staticannotation onquery()pointing to the correct builder, so the IDE knows whatPost::query()returns.
The file uses if (false) blocks, so it has no runtime impact — your IDE parses the
annotations for autocomplete without PHP ever loading them.
No changes to your entities are needed — just run the command and let the IDE re-index.
Custom Query Builders
Define a custom builder by extending MagicQueryBuilder:
use Ncphillips\LaravelDoctrineQuery\MagicQueryBuilder; class PostQueryBuilder extends MagicQueryBuilder { public function recent(): static { return $this->andWhere('post.createdAt >= :recent') ->setParameter('recent', new \DateTimeImmutable('-7 days')); } }
Then set $queryBuilder on the entity:
use Ncphillips\LaravelDoctrineQuery\Queryable; #[ORM\Entity] class Post { use Queryable; protected static ?string $queryBuilder = PostQueryBuilder::class; }
Now Post::query()->recent() works at runtime. For IDE autocomplete:
- Custom methods like
recent()are real methods on a real class, so your IDE picks them up automatically. - Entity field methods (
whereTitle,andWherePublished,isPublished, etc.) still come from the ide-helper. Runphp artisan doctrine:query:ide-helperand it will annotatePostQueryBuilderwith@methodstubs for every mapped field instead of generating a phantom subclass. The entity stub'squery()return type will point atPostQueryBuilder, so your IDE sees both the custom methods and the entity field methods on the same builder type.
Laravel Boost
If the host application uses Laravel Boost, this package
ships its own AI guidelines and a laravel-doctrine-query skill. Boost discovers them through
package auto-discovery — no configuration needed. Re-run php artisan boost:install (or your
agent's guideline refresh) after installing the package to pull them in.
The resources live in resources/boost/:
guidelines/core.blade.php— short always-on summary injected into the host project's agent guidelines.skills/laravel-doctrine-query/SKILL.md— detailed usage guidance loaded on demand.
Guiding Principles
Design philosophy and decision-making framework in GUIDING_PRINCIPLES.md.
Roadmap
These are open areas the project may grow into. Nothing here is committed — discussion is welcome via issues and pull requests.
See ROADMAP.md.
Development
composer install composer test # Pest + Orchestra Testbench, integration tests against SQLite composer lint # Laravel Pint
Release
This project uses Conventional Commits and git-cliff to manage releases.
scripts/release.sh
The script:
- Checks for a clean working copy.
- Determines the next version from commit history via
git cliff --bumped-version. - Prepends the new changelog entries to
CHANGELOG.md. - Prompts you to review, then creates a
jjtag.
License
MIT