maize-tech/laravel-excludable
Laravel Excludable adds a trait that lets Eloquent models be hidden from queries by default, so you can exclude records globally while still including them on demand via scopes.
composer require maize-tech/laravel-excludable (4.1.0 adds Laravel 11 support; no breaking changes from 4.0.x, so an in-place composer update is safe).php artisan vendor:publish --tag="excludable-config". Read config/excludable.php first — it tells you which column is used and how the scope is applied.$table->boolean('excluded')->default(false)->index();. Index it: the global scope adds a WHERE clause to every query on that model.use Excludable;), add the column to $fillable if you plan to mass-assign it, and cast it to bool.Product::withExcluded() in admin screens and Product::onlyExcluded() in a "trash"/archive view.Where to look first: the trait (src/Concerns/Excludable.php), the global scope class, and the config file. Those three define the entire surface area.
Model::withExcluded(). In public controllers, do nothing — the safe default is already applied.$model->exclude(), $model->include()) rather than update(['excluded' => true]) in controllers; it keeps intent readable and pairs naturally with events/observers. Check state with isExcluded() in policies instead of comparing the raw column.update()/delete() too, so Model::query()->where(...)->delete() leaves excluded rows untouched. To reach them, prefix with withExcluded() explicitly — this is usually what you want, but be deliberate about it.$post->load(['comments' => fn ($q) => $q->withExcluded()]), or define a dedicated commentsWithExcluded() relation for admin contexts.onlyTrashed() does not also restore excluded rows). For an admin "everything" view, chain explicitly: Model::withExcluded()->withTrashed().if ($request->user()?->can('viewExcluded', Product::class)) { $query->withExcluded(); }. Treat the scope as a UX default, not a security boundary.Rule::unique('products', 'sku') will ignore excluded rows and let duplicates through. Add ->withoutGlobalScope(...) (or build the rule from a withExcluded() query) when uniqueness must span the whole table.Model::withExcluded() (or withoutGlobalScopes()) so fixtures aren't silently invisible to assertions and assertDatabaseHas counts.exists(), pagination totals and find() will not see excluded records. If a number "looks low", check the scope before checking the query.php artisan config:cache after editing excludable.php; a stale cache is the usual culprit when a column rename "does nothing". Remember to config:clear in tests.0/1; always cast to bool so isExcluded() and JSON serialisation behave across drivers (SQLite in tests vs. MySQL in prod).->toSql() shows the injected where "excluded" = ? — if it's absent, the trait isn't on the model or someone called withoutGlobalScopes() (which nukes SoftDeletes too). Prefer removing the single scope over removing all of them.excluded column with different semantics (nullable, enum, timestamp) will break in confusing ways — repoint the column in config or rename.$model->update($request->all()) can let a client un-exclude a record if the column is fillable. Keep it out of $fillable and mutate it through explicit methods.excluded_for_tenant_id) rather than forking the package. withoutGlobalScope(SpecificScope::class) is the surgical escape hatch when you need one model/one query to bypass.where clauses.How can I help you explore Laravel packages today?