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 Boolean Softdeletes Laravel Package

tenantcloud/laravel-boolean-softdeletes

Laravel soft deletes backed by a boolean column instead of a nullable timestamp. A drop-in trait keeps the familiar SoftDeletes API while making delete checks and indexes faster and simpler, ideal for high-volume tables where a deleted flag beats deleted_at comparisons.

View on GitHub
Deep Wiki
Context7

Getting Started

composer require tenantcloud/laravel-boolean-softdeletes

v7.0 requires Laravel 12 (see Gotchas — if you're on 10/11, stay on v6). Publish the config so you can see the column name and defaults:

php artisan vendor:publish --tag=boolean-softdeletes-config

Add the flag column to the table (either the package's migration helper if available in your version, or plainly):

$table->boolean('is_deleted')->default(false)->index();

Then swap Laravel's SoftDeletes for the package trait on the model — use one or the other, never both:

use Illuminate\Database\Eloquent\Model;
use TenantCloud\LaravelBooleanSoftdeletes\Traits\BooleanSoftdeletes;

class Invoice extends Model
{
    use BooleanSoftdeletes;
}

First use case to validate: pick one high-write table where deleted_at IS NULL filtering was showing up in slow-query logs. Swap the trait, run your existing test suite, and compare EXPLAIN on your hottest list query. That single check tells you whether the migration is worth the blast radius before you touch other models.

Implementation Patterns

Reads are the same, writes change shape. Model::query() is automatically scoped to is_deleted = false. withTrashed(), onlyTrashed(), trashed(), restore(), and forceDelete() all keep their familiar names, so most call sites need zero edits — the breakage is concentrated in code that touches deleted_at directly.

Bulk operations must be per-model. Model::where(...)->delete() issues a real DELETE and bypasses the trait entirely. For soft deletion of many rows:

Invoice::where('status', 'void')->each(fn ($i) => $i->delete());
// or, when you genuinely don't need events:
Invoice::where('status', 'void')->update(['is_deleted' => true]);

Prefer the first form when observers, queued jobs, or audit logs hang off the deleted event.

Unique constraints need rethinking. A row with is_deleted = true still physically exists, so unique(email) now blocks re-registration. Options, in order of preference:

  • Postgres partial unique index: CREATE UNIQUE INDEX ... ON users (email) WHERE is_deleted = false;
  • Composite unique(email, is_deleted) — works for one deleted copy only, not for repeat deletions
  • Mutate the unique value on delete via a deleting observer

Decide this before the migration, not after the first support ticket.

Index the pair, not the flag. A lone boolean index is low-selectivity and most planners will ignore it. Use composite indexes matching your real queries — (is_deleted, created_at), (tenant_id, is_deleted) — or a partial index on Postgres.

Route model binding and API resources come along for free. Implicit binding resolves through the global scope, so trashed records 404 as expected. If your resources exposed deleted_at in JSON, replace it with an explicit is_deleted boolean rather than letting a null leak.

Testing. Replace assertSoftDeleted usage with database assertions:

$this->assertDatabaseHas('invoices', ['id' => $invoice->id, 'is_deleted' => true]);

Consider a small custom assertion helper in your base TestCase so the migration is a one-line change across the suite.

Retention/pruning. There is no scheduler hook. Pair the trait with a scheduled forceDelete() job for rows past your retention window, otherwise the boolean approach quietly turns into "never actually deleted."

Gotchas and Tips

  • assertSoftDeleted() / assertNotSoftDeleted() will fail. They assert on deleted_at, which no longer exists. This is the single most common source of red tests after the swap.
  • Anything reading $model->deleted_at breaks. Attribute access, Blade templates, API resources, admin filters, CSV exports. Grep for deleted_at repo-wide before merging — including in packages, Nova/Filament resources, and raw query builders.
  • Raw SQL and DB::table() bypass the global scope. If you have reporting queries on the query builder, add ->where('is_deleted', false) by hand. The trait only protects Eloquent.
  • Cast the column. MySQL returns 0/1; without protected $casts = ['is_deleted' => 'boolean'] (or the trait's own casting) your JSON and if checks get surprising.
  • v7.0 changed migration generation. PR #26 reworks migration performance — diff the generated schema against what v6 produced before assuming your existing migrations still match the package's expectations. If you use the package's migration helper for new tables only, existing tables are untouched; if you re-run schema tooling, verify the column default and index survived.
  • v7.0 is a Laravel 12 major. Bumping it on a Laravel 10/11 app will fail resolution. Treat the upgrade as a framework upgrade prerequisite, not a patch.
  • Config is read at boot. If you customize the column name, run php artisan config:cache (and clear it in CI) or you'll get "unknown column" errors that look like a broken migration.
  • forceDelete() is irreversible and doesn't warn. In an admin panel this is one careless click from data loss; add confirmation and authorization gates.
  • Events still fire. deleting/deleted/restoring/restored behave normally, which makes observers your cleanest extension point — use deleting to cascade flags to children, since there's no DB-level cascade for a boolean flip.
  • Debugging tip: when a record "disappears" unexpectedly, add ->withTrashed() to confirm it was the scope rather than a hard delete, then check is_deleted directly with DB::table() to rule out a raw query setting the flag.
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