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 Cascade Soft Deletes Laravel Package

iatstuti/laravel-cascade-soft-deletes

Laravel Cascade Soft Deletes adds an Eloquent trait that automatically cascades soft deletes, restores, and force deletes to related models, keeping parent and child records consistent without custom model event code.

View on GitHub
Deep Wiki
Context7

Getting Started

composer require iatstuti/laravel-cascade-soft-deletes

Add the trait to the parent model alongside SoftDeletes and declare the relations to cascade:

use Iatstuti\Database\Support\CascadeSoftDeletes;
use Illuminate\Database\Eloquent\SoftDeletes;

class User extends Model
{
    use SoftDeletes, CascadeSoftDeletes;

    protected $cascadeSoftDeletes = ['posts', 'posts.comments'];
}

Now $user->delete() soft-deletes the user and every related post and comment.

Where to look first

  • The repo README.md — it documents the two supported properties ($cascadeSoftDeletes, $restoreCascadeSoftDeletes in newer versions) and the overridable hooks.
  • src/CascadeSoftDeletes.php — small file; read it once so you know exactly when the cascade fires.
  • The CHANGELOG — restore and force-delete behaviour has changed across major versions, so pin a version and check its notes.

First use case: a User with Post children and Comment grandchildren. Verify with a Tinker session ($user->delete(); User::withTrashed()->find(...)->posts()->withTrashed()->get()) before wiring it into a controller.

Implementation Patterns

Recursive cascading — pick one strategy

Either list a nested path ('posts.comments') on the parent, or put the trait on the intermediate model with protected $cascadeSoftDeletes = ['comments'] and only cascade 'posts' from User. Prefer the latter for anything more than two levels: it keeps ownership of each cascade with the model that owns the relation.

Restore

Restoring is not automatic in every version. The safe pattern is to wire it explicitly:

public function restore()
{
    $this->restoreCascadeSoftDeletes();
    parent::restore();
}

Only relations declared in $cascadeSoftDeletes (or $restoreCascadeSoftDeletes if your version has it) are restored.

Wrap the call in a transaction

DB::transaction(fn () => $user->delete());

The trait issues one query per relation. If any relation fails, you get a half-deleted tree otherwise.

Queue big cascades

For a user with 50k comments, a synchronous cascade is one query loading every row into memory. Move the delete into a queued job:

DeleteUserJob::dispatch($user);

and perform the delete() inside the job, so the HTTP request isn't blocked and retries are handled.

Observer hook for auditing

The parent's deleting event fires before the cascade. The children's deleting/deleted events fire individually, so a single AuditObserver on each model captures everything without special-casing.

Test helper

public function test_deletes_cascade(): void
{
    $user = User::factory()->hasPosts(3)->create();
    $user->delete();
    $this->assertSoftDeleted($user);
    $this->assertSoftDeleted($user->posts->first());
}

Gotchas and Tips

  • Query-builder deletes bypass the cascade. User::where(...)->delete() does not fire model events, so nothing is cascaded. Use ->each->delete() (or ->chunkById(...)->each) so the trait runs.
  • BelongsTo relations are dangerous. $cascadeSoftDeletes = ['account'] will soft-delete the parent account when you delete a user. Only put HasMany / HasOne / BelongsToMany entries in there.
  • Children must also use SoftDeletes. If the child model doesn't, delete() hard-deletes it — likely not what you want, and irreversible.
  • Restore is blunt. In versions where restoreCascadeSoftDeletes() exists, it restores rows the cascade originally touched, but it can also restore children that were soft-deleted independently. Don't rely on restore for "undo everything" semantics.
  • deleted_at and relations. Child relations that already contain trashed rows are correctly skipped (the global scope filters them), but a relation defined with withTrashed() will pull already-deleted rows back into the delete set. Avoid withTrashed() in cascade relations.
  • Force delete. $user->forceDelete() cascades a hard delete to children when the trait is present. To keep soft-delete behaviour on children, override forceDeleting() to return false on the parent, or omit those relations from the cascade and clean up separately with a scheduled prune.
  • Relations must be real relation methods. Accessors, hasOne()->where(...) returned as a single model, or ->pluck() chains won't work — the trait calls the relation method and iterates the query builder.
  • Debugging a missing cascade. Temporarily ray($this->getCascadeSoftDeletes()) / dd($model->cascadeSoftDeletes ?? 'not set') inside the deleting listener. The two most common causes are a typo in the relation name and the relation returning null for one parent instance.
  • Extension point. The trait's cascade runs from a booted deleting listener. If you need conditional cascading (e.g. skip cascading for admin-owned records), override cascadeSoftDeletes() in the model rather than editing the trait.
  • Version pin. The restore API and force-delete behaviour have shifted between releases. Pin a specific version in composer.json and read the changelog on upgrade — 0.x is iatstuti, but the package was previously michaeldyrynda/laravel-cascade-soft-deletes on GitHub, so composer/GitHub names diverge.
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.
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox