gigerit/laravel-cascade-delete
gigerit/laravel-cascade-delete brings trait-based cascade deletes to Laravel Eloquent models, automatically removing related records when a parent is deleted. Prevent orphaned rows without fragile DB foreign keys and keep your data consistent and relationships tidy.
composer require gigerit/laravel-cascade-delete and publish the package config (php artisan vendor:publish --tag="cascade-delete-config"). Read the config file before wiring anything up — it controls how recursive deletes are executed and is the first place to look when behaviour surprises you.CascadeDeletes) to the model that owns the relationship tree, then declare the relations that should follow it down:use GigerIT\LaravelCascadeDelete\Traits\CascadeDeletes;
class User extends Model
{
use CascadeDeletes;
protected $cascadeDeletes = ['posts', 'profile'];
}
User whose posts each have comments, where you want a single $user->delete() to clean the whole graph without hand-writing deleting observers or relying on database-level ON DELETE CASCADE. Declare $cascadeDeletes on User for posts and on Post for comments, then delete the user and assert the children are gone.src/Traits/...) to see the exact deletion entry points, the published config, and the package's own test suite for the soft-delete and forceDelete cases — those define the contract you're building on.^1.3.4 in composer.json. Earlier 1.3.x releases used non-public soft-delete internals; see Gotchas.$cascadeDeletes entries are resolved as relationship methods on the model. They must exist and return a Relation; passing an attribute name or a pre-loaded collection silently does nothing.Post without the trait stops the chain at comments.delete() on a soft-deletable parent soft-deletes the children; forceDelete() hard-deletes them. Keep the two paths explicit in your application code (an admin "purge" action should call forceDelete() deliberately, not fall out of a generic delete controller).DB::transaction(...) so a failing child doesn't leave a half-deleted graph.deleting/deleted events fire. For big subtrees, consider chunking the cascade at the top level or moving the operation to a queued job.morphMany/morphToMany work as cascade targets, but belongsToMany rows need the pivot handled — verify pivot rows are removed or detached as you expect in a test rather than assuming.onDelete('cascade') on the same relations, you get two mechanisms doing the same job — usually harmless, occasionally noisy in query logs and confusing when debugging. Pick one per relationship and be consistent.RefreshDatabase and assert with assertSoftDeleted($child) / assertDatabaseMissing($child) on both the soft and force paths. A regression test around soft-delete vs force-delete is the cheapest guard for the v1.3.4 class of bug.isForceDeleting() / trashed() / forceDelete() semantics). Practical impact: if you use Laravel's stock SoftDeletes you were probably fine, but if you have a custom soft-delete trait, an overridden performDeleteOnModel, a forceDeleting property hack, or a package that swaps out SoftDeletes, earlier versions could hard-delete models you expected to be trashed (or skip them entirely). Upgrade to ≥1.3.4 and re-run your delete/force-delete tests if any of those apply. If you wrote a workaround around this bug, remove it.$cascadeDeletes (or a relation that doesn't exist) typically doesn't throw — it just doesn't cascade. Confirm by adding a temporary deleting listener or asserting child rows are gone.deleting returning false halts the chain for that model but may leave already-deleted siblings behind. Keep observers side-effect-light and don't use them as authorization gates — use policies before calling delete().Model::where(...)->delete() on the query builder skips model events and therefore the cascade. Route through model instances, or handle those relations explicitly.parent/children on the same table, or A→B→A) risk infinite recursion. Guard with depth limits or exclude the back-reference from $cascadeDeletes.forceDelete() is irreversible — including children. No deleted rows remain to restore from. Gate it behind a policy or explicit confirmation, and never let it be reachable from a generic resource controller destroy method.php artisan config:cache. A stale cached config is a frequent "it works locally" cause.static::deleting(fn ($m) => logger()->info(class($m).' deleting', ['force' => $m->isForceDeleting()])) — the isForceDeleting() flag tells you immediately which path the package took on this release.use ParentTrait::method as parentMethod; to add filtering (e.g. skip archived children) without forking the package. Observers registered on child models are the cleanest place for audit logging.How can I help you explore Laravel packages today?