biiiiiigmonster/hasin
biiiiiigmonster/hasin brings recursive "has in" relationship queries to Laravel's Eloquent ORM, letting you fetch ancestors, descendants, and other nested tree data with fluent, hasMany-style syntax instead of raw recursive SQL. Ideal for category trees, comment threads, and org charts.
biiiiiigmonster/hasin swaps Eloquent's where exists (...) correlation for a where in (subquery) — usually a large win on MySQL when the parent table is big and the relation key is indexed. v5.1.0 is a compatibility release: dependency bumps plus Laravel 13.x support (via Laravel Shift, follow-up fix in #66). There are no API changes, deprecations, or breaking changes for existing 5.x users.
Install and let auto-discovery register the service provider:
composer require biiiiiigmonster/hasin:^5.1
Add the trait to every model whose relations you want to query this way:
use BiiiiiigMonster\Hasin\HasIn;
class User extends Model
{
use HasIn;
}
First use case — take a slow existing scope and swap the method name only:
// before
User::whereHas('posts', fn ($q) => $q->where('published', true))->paginate();
// after
User::whereHasIn('posts', fn ($q) => $q->where('published', true))->paginate();
Where to look first: ->toSql() on both variants for one of your real queries. The subquery shape (select ... where in (select ...)) is the whole package — if it doesn't change, the trait isn't loaded on that model or the relation is one the package delegates back to core Eloquent.
Drop-in parity with whereHas. The trait exposes the familiar quartet, so a whole codebase can be migrated mechanically:
| Core Eloquent | hasin |
|---|---|
whereHas / orWhereHas |
whereHasIn / orWhereHasIn |
whereDoesntHave / orWhereDoesntHave |
whereDoesntHaveIn / orWhereDoesntHaveIn |
has / orHas |
hasIn / orHasIn |
doesntHave / orDoesntHave |
doesntHaveIn / orDoesntHaveIn |
Nested relations. Dot notation and closures compose the same way, so multi-hop filters convert without rewriting:
User::whereHasIn('posts.comments', fn ($q) => $q->where('approved', true))->get();
Grouping with or. The classic precedence bug is unchanged — always wrap orWhereHasIn in a closure:
User::where('active', true)
->where(fn ($q) => $q
->whereHasIn('posts')
->orWhereHasIn('comments'))
->get();
Mixed with eager loading. Filters and with()/withCount() are independent; combine freely so one query set both filters and hydrates:
User::query()
->whereHasIn('posts', fn ($q) => $q->where('published', true))
->withCount('posts')
->with('posts:id,user_id,title')
->paginate(50);
Index-driven hot paths. Reach for whereHasIn on list/report endpoints where the parent set is large and the FK on the child table is indexed; leave whereHas on small or highly selective sets where the planner already short-circuits. A useful workflow is to convert one scope at a time behind a feature flag, compare EXPLAIN output, then roll forward.
Reusable scopes. Wrap the choice in a model scope so the decision is in one place and reversible if a future DB engine prefers exists:
public function scopePublished(Builder $q): Builder
{
return $q->whereHasIn('posts', fn ($p) => $p->where('published', true));
}
Upgrading to Laravel 13. Bump to ^5.1 alongside the framework bump; nothing else in the package API moved from 5.0.1. Pin ^5.1 rather than ^5.0 if you're on L13 so you don't resolve a release without the fix from #66.
NOT IN and NULLs. whereDoesntHaveIn / doesntHaveIn can render as not in (subquery); if the subquery can yield a NULL key, the whole predicate evaluates to no rows. Confirm the generated SQL and, where relevant, constrain the subquery or fall back to whereDoesntHave.has('posts', '>=', 5) has no hasIn equivalent — keep core has() or use withCount(...)->having(...) for cardinality filters.in (subquery) and exists return the same rows but can plan very differently. Benchmark on real data volume; on some engines/selectivity the rewrite is neutral or slower.use HasIn will error or fall through to core behaviour — check the trait first when a call "does nothing".IN list.orWhereHasIn at the top level leaks across the whole WHERE, exactly like orWhereHas. Wrap in ->where(fn ($q) => ...).->dd() / ->toSql() plus EXPLAIN on the outer query is the fastest path; the parent key correlation shows up as the inner whereColumn-style constraint. If the subquery selects more than the relation key, something (a select() in your closure, a withCount) has contaminated it.In vs exists as an application-level decision and keep it behind scopes.^5.1 requires a recent framework; if you run L10–L12 you can stay on the same line, but verify PHP constraints in composer.json before a broad bump in a monorepo.How can I help you explore Laravel packages today?