Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Excludable Laravel Package

h-farm/laravel-excludable

Laravel Excludable adds a lightweight trait to Eloquent models so specific records can be excluded from queries by default and included only when needed. Ideal for hiding archived or inactive rows without soft deletes or custom scopes, keeping filtered results clean and relationships predictable.

View on GitHub
Deep Wiki
Context7

Getting Started

maize-tech/laravel-excludable adds an Excludable trait that applies a global scope hiding rows flagged as excluded — a "soft delete" for records you want to keep in the DB but never surface in normal queries. Version 4.1.0 is a compatibility release: it opens the door to Laravel 11 (and its PHP 8.2+ baseline) without API changes, so treat it as a drop-in for existing 4.x apps.

Minimal path:

  1. composer require maize-tech/laravel-excludable:^4.1
  2. Publish/run the migration stub that adds the excluded boolean column to the target table (or add $table->boolean('excluded')->default(false) yourself).
  3. Add the trait to the model:
    use Maize\Excludable\Excludable;
    
    class Article extends Model
    {
        use Excludable;
    }
    
  4. First use case — exclude and restore:
    $article->exclude();   // sets the flag; row disappears from default queries
    $article->restore();   // clears the flag
    
    Article::count();                 // excludes flagged rows
    Article::withExcluded()->count(); // includes them
    Article::onlyExcluded()->get();   // flagged rows only
    

Where to look first: the trait itself (bootExcludable / getExcludedColumn) and the ExcludedScope class, since that is where the global scope is applied. Then the published config/migration stub to confirm the column name and default.

Implementation Patterns

Admin vs. public queries. The default global scope is the "public" view. In admin controllers, be explicit:

public function index() {
    return Article::withExcluded()->paginate();
}

Avoid calling withoutGlobalScope(ExcludedScope::class) by hand — the package scopes (withExcluded, onlyExcluded) are clearer and survive scope renaming.

Bulk exclusions. Prefer a single update() over per-model calls when the business action is mass-hiding:

Article::where('category_id', $id)->update(['excluded' => true]);

This skips model events — fine for the flag itself, but any observers you rely on (search index sync, cache busting) will not fire. If those matter, iterate with ->each->exclude() or dispatch a job.

Relationships. The global scope leaks into eager loads and whereHas in ways that surprise people. Article::with('comments') will silently drop excluded comments; Article::with(['comments' => fn ($q) => $q->withExcluded()]) opts back in. Decide per-relation and document it.

Route-model binding. Route::get('/articles/{article}', ...) will 404 for excluded rows, which is usually the desired behaviour and gives you a free "gone" response. If you need a reversible public URL (e.g. an unpublished article preview for editors), resolve with withExcluded() inside a policy-gated controller action rather than disabling the scope globally.

Filtering pipelines / repositories. If you build query objects, expose a includeExcluded(bool) flag and centralise the scope toggle there, so no controller reaches into withoutGlobalScope directly. This keeps the exclusion semantic in one place when the column or trait changes.

Testing. RefreshDatabase plus factory-created excluded records is the cleanest way to assert both sides:

Article::factory()->excluded()->create();
$this->assertSame(0, Article::count());
$this->assertSame(1, Article::withExcluded()->count());

Add a factory state that sets excluded => true; otherwise test authors forget the flag and write assertions that pass for the wrong reason.

Laravel 11 upgrade. 4.1.0 is the version that unblocks Laravel 11. Bump Laravel and this package together, confirm the service provider is still auto-discovered (Laravel 11's slimmer skeleton changes nothing here, but check bootstrap/providers.php if you registered the provider manually), and run the suite against the new skeleton. No migration or API change is expected.

Gotchas and Tips

  • The scope is silent. Forgetting the trait on a new model means exclusions quietly do nothing — the column exists, the row is flagged, and it still shows up. Add a small test per excludable model asserting Article::count() ignores flagged rows; it catches missing traits and misnamed columns.
  • Column name is configurable. If the trait reads a configurable column, changing it post-deploy is a data migration, not a config edit. Pin the name in the migration and leave the config default alone unless you have a reason.
  • exclude() fires model events; raw updates do not. saved/updated observers, audit logs, and search-index sync will behave differently between the two paths. Pick one convention per codebase.
  • Counts and aggregates diverge. Article::count(), pagination totals, and any DB::table('articles') query builder calls will disagree, because the raw builder has no global scope. Anything using DB::table must add where('excluded', false) manually.
  • Don't stack withExcluded() and onlyExcluded() and expect a sensible result — they are mutually exclusive intent; the last-applied scope wins depending on implementation order.
  • Uniqueness and foreign keys. Excluding a row does not free its unique keys. If "excluding" is meant to allow a replacement record with the same slug/email, you need a partial unique index or a composite with the flag — the trait alone won't do it.
  • Debugging. When a row "disappears", check in this order: toSql() on the query (is the scope there?), withExcluded()->find($id) (does the row exist?), then the column value. Model::withoutGlobalScopes() is the fastest way to confirm the scope is the culprit.
  • Extension points. Override getExcludedColumn() (or the equivalent accessor) in the model to point at a differently named column, and register additional global scopes only after the exclusion scope so ordering stays predictable.
  • Version pin. If Composer resolves to 4.0.x on a Laravel 11 app, you have a stale lock or a conflicting constraint elsewhere; composer why-not maize-tech/laravel-excludable 4.1.0 will name the offender.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core