protonemedia/laravel-eloquent-where-not
Adds whereNot and orWhereNot methods to Laravel Eloquent, letting you negate simple conditions, nested closures, and relationships cleanly. A small, expressive package for readable inverse WHERE clauses without raw SQL or awkward operator flipping.
Install the package with Composer and verify the macro is registered by the package's service provider:
composer require protonemedia/laravel-eloquent-where-not
There is no config file or publish step. The first place to look is the package README and src/ — specifically the service provider that registers the whereNot macro on Illuminate\Database\Eloquent\Builder. Confirm which macros are actually registered in your installed version before designing around them.
Version 1.2.0 adds Laravel 9 support, so the package now installs cleanly on Laravel 9 projects. That is the practical ceiling: there is no release covering Laravel 10 or later, so treat 1.2.0 as the last known-good version for L9 and below.
The first use case is inverting a local scope without rewriting its conditions. Given:
// app/Models/User.php
public function scopeActive(Builder $query): void
{
$query->where('status', 'active');
}
you can query the complement directly:
$inactive = User::whereNot('active')->get();
// SQL roughly: where not (status = 'active')
The same macro accepts a closure for ad-hoc constraints:
$users = User::whereNot(function (Builder $query) {
$query->where('status', 'active')
->whereNotNull('email_verified_at');
})->get();
Treat this as a query-construction convenience, not a replacement for every != or whereDoesntHave. Reach for it when the condition you want to invert is already expressed as a scope or a grouped closure.
Invert an existing scope instead of duplicating it. The highest-value pattern is keeping one canonical definition of a filter and deriving its complement:
// One source of truth
public function scopePublished(Builder $query): void
{
$query->where('published_at', '<=', now())
->whereNull('archived_at');
}
// Complement, without restating the conditions
$unpublished = Post::whereNot('published')->get();
This is useful for admin screens, cleanup jobs, and "everything except this segment" reports.
Wrap compound constraints in a closure. When the condition involves multiple where calls, pass a closure so the whole group is negated rather than only the first clause:
$eligible = User::whereNot(function (Builder $query) {
$query->where('status', 'active')
->where('plan', 'pro');
})->get();
Use it in query objects or repository helpers. If your app centralizes filters, expose intent-revealing methods rather than leaking the macro everywhere:
class UserQuery
{
public function excludingActive(): Builder
{
return User::query()->whereNot('active');
}
}
This keeps call sites readable and gives you one place to migrate to Laravel's native whereNot later.
Combine with relationship constraints carefully. For "does not have" logic, prefer native whereDoesntHave() — it is clearer and better understood by other developers. If you must negate a whereHas, group it explicitly and test the generated SQL.
Test both sides of the negation. Assert that a matching row is excluded and a non-matching row is included. Use toSql() and getBindings() in a unit test if the generated grouping matters.
The package is archived. It receives no maintenance, and 1.2.0 (Laravel 9 support) is the final supported target — expect dependency constraints to block Laravel 10 and newer. On modern Laravel the query builder includes a native whereNot, so the package is usually unnecessary. If you are on Laravel 9 or older and need it, pin to ^1.2 and plan a migration; for anything newer, use the native builder method, fork the package, or copy the macro into your own service provider.
Check your Laravel/PHP matrix before upgrading. 1.2.0 broadens illuminate/* constraints to include Laravel 9, but the package's own PHP constraint and test suite were written for that generation. If you are on a mixed-version monorepo or a PHP version outside its supported range, verify with composer why-not rather than assuming the new tag covers you.
SQL three-valued logic will surprise you. whereNot('active') produces not (status = 'active'), which does not include rows where status is NULL (the expression evaluates to NULL, not true). If "not active" should include nulls, write the condition explicitly:
User::where(function (Builder $query) {
$query->where('status', '!=', 'active')
->orWhereNull('status');
})->get();
Watch orWhereNot precedence. As with any orWhere, an ungrouped OR condition can silently widen the result set. Always wrap OR negations in a nested where group:
User::where('tenant_id', $tenantId)
->where(function (Builder $query) {
$query->whereNot('active');
})
->get();
Only where constraints are meaningfully invertible. A scope that also adds select, with, join, orderBy, or limit will not behave as a clean negation. Keep negation-able scopes focused on where clauses, and split side effects into separate scopes.
Global scopes are not affected. whereNot negates the constraint you pass; it does not disable SoftDeletes or other global scopes. Combine with withTrashed() or withoutGlobalScope() when your intended complement includes excluded rows.
Debug by inspecting the SQL. Call ->toSql() and ->getBindings() on the builder before executing. If the result looks wrong, the usual cause is missing grouping or an unexpected nested orWhere from the scope you inverted.
Extension point: if you need additional behavior (for example, a custom orWhereNot or a builder mixin), register your own macro in a service provider rather than editing vendor code — the archived package is unlikely to receive fixes, and a local macro keeps the upgrade path open.
How can I help you explore Laravel packages today?