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

dcat/laravel-softdeletes

dcat/laravel-softdeletes adds enhanced soft delete support to Laravel Eloquent models. Cascade deletes and restores across related records, keep data consistent, and avoid unique constraint conflicts from trashed rows, all through simple traits with minimal configuration.

View on GitHub
Deep Wiki
Context7

Getting Started

The package swaps Laravel's deleted_at-column soft deletes for an archive-table approach: when you delete a model, the row is copied into a shared soft_deletes table and physically removed from its own table. The original table stays clean, which is the whole point — you can put unique indexes on email, phone, slug, etc. without deleted rows blocking re-registration, and the hot table doesn't carry bloat plus a deleted_at index.

Install and scaffold

composer require dcat/laravel-softdeletes

# publishes the archive-table migration + config
php artisan vendor:publish --provider="Dcat\LaravelSoftDeletes\SoftDeletesServiceProvider"
php artisan migrate

Adopt it on one model first — do not convert the whole app at once.

namespace App\Models;

use Dcat\LaravelSoftDeletes\SoftDeletes;   // NOT Illuminate\Database\Eloquent\SoftDeletes
use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    use SoftDeletes;
}

Then drop the now-unused column and add the unique index you actually wanted:

Schema::table('users', function (Blueprint $table) {
    $table->dropSoftDeletes();          // removes deleted_at
    $table->unique('email');
});

First use case to prove it out: a users table where a deleted account's email currently blocks re-registration. Delete a user, confirm the row vanished from users and appeared in soft_deletes, then register the same email again. If that works, the integration is sound.

Where to look first, in order:

  1. src/SoftDeletes.php — the trait. Confirm exactly which methods it overrides (delete, forceDelete, restore, runSoftDelete) and which query scopes it does or does not provide. Fifteen minutes here saves a day of surprises.
  2. The published migration — column names in the archive table (model_type, model_id, and the payload column) drive every query you'll write against it.
  3. The config file — table name and any retention/connection settings.

Implementation Patterns

Default queries need no changes. Because the row is gone from the main table, User::all() naturally returns only live users — there is no global scope to fight. That's the nicest property of this design.

Trashed data lives in a second table, so treat it as one. Build an explicit archive model or repository over the soft_deletes table rather than expecting withTrashed() semantics:

// A "recycle bin" listing — polymorphic, so filter by type
$archived = DB::table('soft_deletes')
    ->where('model_type', User::class)
    ->orderByDesc('id')
    ->paginate(50);

For an admin UI (Dcat Admin, Nova, Filament) this is actually cleaner than Laravel's built-in trash filter: one dedicated "Deleted Records" page, one resource, restore and purge buttons per row. Since the same author maintains Dcat Admin, the trait slots into a Dcat grid without ceremony — you just point the CRUD at the archive table instead of the model.

Restoration is a re-insert, so treat it as a business operation, not a toggle. Wrap it and handle the collision case explicitly:

DB::transaction(function () use ($archived) {
    // guard: has someone taken the unique key since?
    abort_if(User::where('email', $archived->data['email'])->exists(), 409);

    User::create(json_decode($archived->data, true));
    DB::table('soft_deletes')->where('id', $archived->id)->delete();
});

Cascade by hand in the deleting event. There is no cascade from the database once the parent row is physically gone — and if you do have ON DELETE CASCADE foreign keys, soft-deleting a parent will hard-delete its children. Pick one:

protected static function booted()
{
    static::deleting(function (self $user) {
        // archive children with the parent, in the same transaction
        $user->posts()->get()->each->delete();
    });
}

Use the archive table as an audit log for free. Add deleted_by (or a causer column) to the published migration. You now have "who deleted what, when" with zero extra tables — a genuine argument for this package over native soft deletes.

Prune on a schedule. The archive is append-only and nothing cleans it up:

// App\Console\Kernel
$schedule->call(fn () => DB::table('soft_deletes')
    ->where('created_at', '<', now()->subDays(90))->delete())
    ->daily();

Migrate incrementally. Convert high-churn, unique-constrained tables first (users, orders, subscriptions). Leave low-volume tables on native soft deletes — mixing the two approaches across models is fine, since the mechanism is per-model.

Gotchas and Tips

  • Laravel 9+ is untested territory. Last tag is January 2021, so the composer constraints target PHP 7.x / Laravel 5.5–8.x. Expect composer require to refuse on modern Laravel; budget for a fork or a --ignore-platform-reqs trial, and re-test the trait against the current Eloquent internals before trusting it.
  • query()->delete() vs destroy(). Eloquent's mass-delete path issues raw SQL and bypasses model events. Under native soft deletes the global scope rewrites it; here it is very likely a hard delete. Read the trait, then standardise on Model::destroy() / ->each->delete() everywhere, and grep your codebase for ->delete() on builders before converting a model.
  • withTrashed() / onlyTrashed() / assertSoftDeleted() are casualties. Anything assuming one table — joins to the parent, ->trashed(), assertSoftDeleted($user) — needs rewriting against the archive table. Write a small assertArchived() test helper instead.
  • Restores fail silently on schema drift. If you added a NOT NULL column without a default after the record was archived, the re-insert throws. Keep restore windows short and give new columns defaults.
  • Don't try to search archived rows by arbitrary fields. The payload is a JSON blob; MySQL can't index it usefully. If you need "find the deleted user with email X", add a small set of promoted columns (model_id, plus maybe deleted_by) to the archive table and live with the rest.
  • Wrap delete and restore in transactions. Both are two-statement operations (insert-then-delete / insert-then-delete). A crash between them either loses the row or duplicates it.
  • Purge the deleted_at residue. After conversion, clean up $dates/$casts, any whereNotNull('deleted_at') filters, validation rules, and model factories that set it.
  • Never stack both traits. One model, one soft-delete strategy; importing Illuminate\...\SoftDeletes alongside will produce duplicate global scopes and confusing behaviour.
  • Debugging: DB::listen() and watch for the insert/delete pair on a single delete(). If you only see the DELETE, the trait isn't hooked — usually a forgotten use statement or a stale cached trait map (composer dump-autoload).
  • Extension points: the config table name, the migration itself (add your columns), and a thin repository layer over the archive table are your levers. Prefer forking the trait over patching vendor/ — the package is effectively unmaintained, so plan for ownership.
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.
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle