PHPackages                             workdoneright/laravel-deletion-guard - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Database &amp; ORM](/categories/database)
4. /
5. workdoneright/laravel-deletion-guard

ActiveLibrary[Database &amp; ORM](/categories/database)

workdoneright/laravel-deletion-guard
====================================

Config-driven deletion guard for Laravel models

v1.0.0(3mo ago)026[2 PRs](https://github.com/Work-Done-Right/laravel-deletion-guard/pulls)MITPHPPHP ^8.2CI passing

Since Dec 26Pushed 1mo agoCompare

[ Source](https://github.com/Work-Done-Right/laravel-deletion-guard)[ Packagist](https://packagist.org/packages/workdoneright/laravel-deletion-guard)[ Docs](https://github.com/1639302-abishekrsrikaanth/laravel-deletion-guard)[ GitHub Sponsors](https://github.com/abishekrsrikaanth)[ RSS](/packages/workdoneright-laravel-deletion-guard/feed)WikiDiscussions main Synced 1mo ago

READMEChangelog (2)Dependencies (12)Versions (5)Used By (0)

Laravel Deletion Guard
======================

[](#laravel-deletion-guard)

[![Latest Version on Packagist](https://camo.githubusercontent.com/0c487bfe6c841a12c805b4c0641b19b3bbff4cf5362c0ef4328d7bb87d0d90c6/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f776f726b646f6e6572696768742f6c61726176656c2d64656c6574696f6e2d67756172642e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/workdoneright/laravel-deletion-guard)[![Total Downloads](https://camo.githubusercontent.com/94dbe04a41ab0b6d87e8fd2d5fa885f3cd31aee1141828284c323590d34bb0ed/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f776f726b646f6e6572696768742f6c61726176656c2d64656c6574696f6e2d67756172642e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/workdoneright/laravel-deletion-guard)

A Laravel package that prevents Eloquent models from being deleted when they have dependent relationships with existing data. Stop accidental data loss by automatically blocking deletions when related records exist.

Features
--------

[](#features)

- 🛡️ **Automatic Protection** - Prevents deletion of models with dependent relationships
- 🎯 **Two Modes** - Choose between explicit method-based or docblock annotation-based configuration
- 🚀 **Performance Optimized** - Built-in caching for relation discovery
- 🔧 **Flexible** - Support for force delete, soft deletes, and custom error messages
- 📊 **Record Counts** - Automatically include counts in error messages (e.g., "47 posts, 123 comments")
- 🗑️ **Cascade Delete** - Optionally auto-delete related records instead of blocking
- 🎚️ **Conditional Blocking** - Block deletion only when specific conditions are met
- 📝 **Laravel Gates Compatible** - Includes a policy class for authorization
- 🧪 **Audit Command** - Test deletion blockers before attempting deletion

Installation
------------

[](#installation)

Install the package via composer:

```
composer require workdoneright/laravel-deletion-guard
```

Publish the config file:

```
php artisan vendor:publish --tag="laravel-deletion-guard-config"
```

Configuration
-------------

[](#configuration)

After publishing, the config file will be available at `config/deletion-guard.php`:

```
return [
    /*
    |--------------------------------------------------------------------------
    | Dependency Discovery Mode
    |--------------------------------------------------------------------------
    | Options:
    | - explicit   → Require explicit opt-in via deletionDependencies() method
    | - docblock   → Auto-discover via @deleteBlocker docblock annotations
    */
    'mode' => env('DELETE_DEPENDENCY_MODE', 'explicit'),

    /*
    |--------------------------------------------------------------------------
    | Cache
    |--------------------------------------------------------------------------
    | Cache relation discovery to improve performance
    */
    'cache' => [
        'enabled' => true,
        'store' => null, // uses default cache store
        'ttl' => 3600,
        'prefix' => 'delete_guard:',
    ],

    /*
    |--------------------------------------------------------------------------
    | Force Delete Override
    |--------------------------------------------------------------------------
    | Allow force delete to bypass deletion guards
    */
    'allow_force_delete' => true,

    /*
    |--------------------------------------------------------------------------
    | Include Count in Messages
    |--------------------------------------------------------------------------
    | When enabled, error messages will include the count of related records.
    | Example: "Cannot delete due to related posts. (5 records)"
    */
    'include_count' => true,
];
```

Usage
-----

[](#usage)

### Choosing a Mode

[](#choosing-a-mode)

The package supports two configuration modes:

**Explicit Mode (Recommended)**

- ✅ Full feature support including conditional blocking
- ✅ Type-safe with IDE autocomplete
- ✅ More control and flexibility
- ✅ Best for complex business logic

**Docblock Mode**

- ✅ Clean, annotation-driven syntax
- ✅ Less code, more readable
- ✅ Great for simple blocking rules
- ❌ No conditional blocking (security limitation)

See the [Mode Comparison](#mode-comparison-feature-parity) table below for detailed feature parity.

### Basic Setup

[](#basic-setup)

Add the `PreventsDeletionWithDependencies` trait to your Eloquent models:

```
use Illuminate\Database\Eloquent\Model;
use WorkDoneRight\DeletionGuard\Concerns\PreventsDeletionWithDependencies;

class User extends Model
{
    use PreventsDeletionWithDependencies;

    public function posts()
    {
        return $this->hasMany(Post::class);
    }

    public function comments()
    {
        return $this->hasMany(Comment::class);
    }
}
```

### Explicit Mode (Recommended)

[](#explicit-mode-recommended)

Define which relationships should block deletion by implementing the `deletionDependencies()` method:

```
class User extends Model
{
    use PreventsDeletionWithDependencies;

    public function posts()
    {
        return $this->hasMany(Post::class);
    }

    public function comments()
    {
        return $this->hasMany(Comment::class);
    }

    /**
     * Define relationships that prevent deletion
     */
    protected function deletionDependencies(): array
    {
        return [
            'posts',    // Simple: just list the relation name
            'comments' => [
                'message' => 'Cannot delete user because they have comments.',
            ],
        ];
    }
}
```

Now when you try to delete a user with posts or comments:

```
$user = User::find(1);
$user->delete(); // Throws DeletionBlockedException if posts or comments exist
```

### Docblock Mode

[](#docblock-mode)

Set `mode` to `'docblock'` in your config, then use annotations:

```
class User extends Model
{
    use PreventsDeletionWithDependencies;

    /**
     * @deleteBlocker
     * @deleteMessage Cannot delete user with existing posts
     */
    public function posts()
    {
        return $this->hasMany(Post::class);
    }

    /**
     * @deleteBlocker
     */
    public function comments()
    {
        return $this->hasMany(Comment::class);
    }
}
```

#### Docblock Mode Advanced Features

[](#docblock-mode-advanced-features)

Docblock mode supports most features available in explicit mode:

```
class User extends Model
{
    use PreventsDeletionWithDependencies;

    /**
     * @deleteBlocker
     * @cascade
     * Auto-delete all posts when user is deleted
     */
    public function posts()
    {
        return $this->hasMany(Post::class);
    }

    /**
     * @deleteBlocker
     * @withTrashed
     * Include soft-deleted comments in the check
     */
    public function comments()
    {
        return $this->hasMany(Comment::class);
    }

    /**
     * @deleteBlocker
     * @noCount
     * @deleteMessage Cannot delete user with subscriptions
     * Disable automatic count in error message
     */
    public function subscriptions()
    {
        return $this->hasMany(Subscription::class);
    }
}
```

**Available Docblock Annotations:**

- `@deleteBlocker` - Marks relation as a deletion blocker (required)
- `@deleteMessage ` - Custom error message
- `@cascade` - Auto-delete related records
- `@withTrashed` - Include soft-deleted records in checks
- `@noCount` - Disable count in error message for this relation

**Note:** Conditional blocking (`condition` callback) is not available in docblock mode for security reasons (would require eval()).

### Mode Comparison: Feature Parity

[](#mode-comparison-feature-parity)

Both modes support nearly all features, with one exception for security reasons:

FeatureExplicit ModeDocblock ModeExample**Basic Blocking**✅✅Prevent deletion when relations exist**Custom Messages**✅✅`'message' => '...'` vs `@deleteMessage`**Cascade Delete**✅✅`'cascade' => true` vs `@cascade`**Include Soft Deleted**✅✅`'withTrashed' => true` vs `@withTrashed`**Disable Count**✅✅`'includeCount' => false` vs `@noCount`**Count Placeholder**✅✅Use `:count` in message**Force Delete Bypass**✅✅Both modes respect config**Conditional Blocking**✅❌`'condition' => fn($q) => ...` only#### Why No Conditional Blocking in Docblock Mode?

[](#why-no-conditional-blocking-in-docblock-mode)

Conditional blocking requires executing PHP closures/callbacks that filter the query before checking for records:

```
// Explicit Mode - SAFE (closure defined in code)
'posts' => [
    'condition' => fn($query) => $query->where('published', true)
]
```

To support this in docblock mode would require either:

1. **Parsing and eval()ing PHP code from docblocks** - Major security vulnerability
2. **A limited DSL for conditions** - Complex to implement and restrictive

Since conditional blocking is an advanced feature and explicit mode provides a safe implementation, we've chosen to keep docblock mode simple and secure.

**Recommendation:** Use explicit mode if you need conditional blocking. Use docblock mode for simpler, annotation-driven configuration.

### Advanced Options

[](#advanced-options)

#### Custom Error Messages

[](#custom-error-messages)

```
protected function deletionDependencies(): array
{
    return [
        'posts' => [
            'message' => 'This user has written posts. Please reassign or delete them first.',
        ],
        'subscriptions' => [
            'message' => 'Cannot delete user with active subscriptions.',
        ],
    ];
}
```

#### Including Record Counts

[](#including-record-counts)

By default, error messages include the count of related records. You can customize this:

```
protected function deletionDependencies(): array
{
    return [
        'posts' => [
            'message' => 'Cannot delete user with :count posts',
            // Use :count placeholder to control where count appears
        ],
        'comments' => [
            'message' => 'User has comments',
            'includeCount' => false, // Disable count for this relation
        ],
    ];
}
```

Output examples:

- With placeholder: `"Cannot delete user with 47 posts"`
- Auto-appended: `"User has comments (12 records)"`
- Disabled: `"User has comments"`

You can also disable counts globally in the config file:

```
// config/deletion-guard.php
'include_count' => false,
```

#### Including Soft Deleted Records

[](#including-soft-deleted-records)

```
protected function deletionDependencies(): array
{
    return [
        'posts' => [
            'withTrashed' => true, // Check including soft-deleted posts
            'message' => 'Cannot delete user with posts (including deleted ones).',
        ],
    ];
}
```

#### Cascade Delete

[](#cascade-delete)

Automatically delete related records instead of blocking:

```
protected function deletionDependencies(): array
{
    return [
        'posts' => [
            'cascade' => true, // Auto-delete all posts when user is deleted
        ],
        'comments' => [
            'cascade' => true,
            // No need for a message since deletion won't be blocked
        ],
        'subscriptions' => [
            // This will still block if subscriptions exist
            'message' => 'Cannot delete user with active subscriptions.',
        ],
    ];
}
```

**Note:** Cascade deletes are executed before checking blockers, so you can mix cascade and blocking rules.

#### Conditional Blocking

[](#conditional-blocking)

Block deletion only when specific conditions are met:

```
protected function deletionDependencies(): array
{
    return [
        'posts' => [
            'condition' => fn($query) => $query->where('published', true),
            'message' => 'Cannot delete user with published posts.',
        ],
        'subscriptions' => [
            'condition' => fn($query) => $query->where('status', 'active'),
            'message' => 'Cannot delete user with active subscriptions.',
        ],
        'orders' => [
            'condition' => fn($query) => $query->where('created_at', '>', now()->subDays(30)),
            'message' => 'Cannot delete user with recent orders.',
        ],
    ];
}
```

The `condition` callback receives the relationship query builder, allowing you to apply any filters before checking if records exist.

### Exception Handling

[](#exception-handling)

The package throws a `DeletionBlockedException` (extends `ValidationException`) when deletion is blocked:

```
use WorkDoneRight\DeletionGuard\Exceptions\DeletionBlockedException;

try {
    $user->delete();
} catch (DeletionBlockedException $e) {
    // Get all blocker messages
    $messages = $e->errors()['delete'];

    // Example: ["Cannot delete due to related posts.", "Cannot delete due to related comments."]
    return response()->json(['errors' => $messages], 422);
}
```

### Force Delete

[](#force-delete)

When `allow_force_delete` is enabled in config, you can bypass guards using force delete:

```
// For soft-deletable models
$user->forceDelete(); // Bypasses deletion guard

// For regular models, force delete works the same as normal delete
// but will bypass the guard if allow_force_delete is true
```

### Audit Command

[](#audit-command)

Check what would block a deletion before attempting it:

```
php artisan deletion-guard:audit "App\\Models\\User" 1
```

Output:

```
❌ Cannot delete due to related posts.
❌ Cannot delete due to related comments.

```

Or if deletion is allowed:

```
✅ No blockers found.

```

### Authorization with Policies

[](#authorization-with-policies)

The package includes a `DeletionPolicy` class for use with Laravel Gates:

```
use WorkDoneRight\DeletionGuard\Policies\DeletionPolicy;

class AuthServiceProvider extends ServiceProvider
{
    protected $policies = [
        User::class => DeletionPolicy::class,
    ];
}
```

The policy provides:

- `delete()` - Returns `false` if deletion blockers exist
- `forceDelete()` - Checks if user is admin (`$user->is_admin`)

### Checking Blockers Programmatically

[](#checking-blockers-programmatically)

```
$user = User::find(1);

// Get all current blockers
$blockers = $user->deletionBlockers();

// Returns array like:
// [
//     ['relation' => 'posts', 'message' => 'Cannot delete due to related posts.'],
//     ['relation' => 'comments', 'message' => 'Cannot delete due to related comments.'],
// ]

// Check if deletion is safe
if (empty($blockers)) {
    $user->delete();
} else {
    // Handle blockers
    foreach ($blockers as $blocker) {
        echo $blocker['message'];
    }
}
```

How It Works
------------

[](#how-it-works)

1. The trait boots during model initialization and registers a `deleting` event listener
2. Before deletion, it checks all configured dependencies
3. For each dependency, it queries the relationship to see if related records exist
4. If any blockers are found, it throws a `DeletionBlockedException`
5. If `allow_force_delete` is enabled and `forceDelete()` is called, guards are bypassed

Performance Considerations
--------------------------

[](#performance-considerations)

- **Caching**: Relation discovery is cached (when in docblock mode) to avoid repeated reflection
- **Query Optimization**: Only checks for existence, doesn't load full records
- **Lazy Evaluation**: Only checks relationships when deletion is attempted

Requirements
------------

[](#requirements)

- PHP 8.2 or higher
- Laravel 11.0 or 12.0

Testing
-------

[](#testing)

```
composer test
```

Changelog
---------

[](#changelog)

Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently.

Contributing
------------

[](#contributing)

Please see [CONTRIBUTING](CONTRIBUTING.md) for details.

Security Vulnerabilities
------------------------

[](#security-vulnerabilities)

Please review [our security policy](../../security/policy) on how to report security vulnerabilities.

Credits
-------

[](#credits)

- [Work Done Right](https://github.com/abishekrsrikaanth)
- [All Contributors](../../contributors)

License
-------

[](#license)

The MIT License (MIT). Please see [License File](LICENSE.md) for more information.

###  Health Score

40

—

FairBetter than 88% of packages

Maintenance86

Actively maintained with recent releases

Popularity8

Limited adoption so far

Community9

Small or concentrated contributor base

Maturity50

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 66.7% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~36 days

Total

2

Last Release

101d ago

Major Versions

v0.0.1 → v1.0.02026-01-31

### Community

Maintainers

![](https://www.gravatar.com/avatar/679089de897a6d3f79d14628be0e3a38b3d77ccfa7c1f85ca7de06ae7564b166?d=identicon)[abishekrsrikaanth](/maintainers/abishekrsrikaanth)

---

Top Contributors

[![abishekrsrikaanth](https://avatars.githubusercontent.com/u/1639302?v=4)](https://github.com/abishekrsrikaanth "abishekrsrikaanth (6 commits)")[![dependabot[bot]](https://avatars.githubusercontent.com/in/29110?v=4)](https://github.com/dependabot[bot] "dependabot[bot] (2 commits)")[![github-actions[bot]](https://avatars.githubusercontent.com/in/15368?v=4)](https://github.com/github-actions[bot] "github-actions[bot] (1 commits)")

---

Tags

laraveleloquentdelete-modelslaravel-deletion-guard

###  Code Quality

TestsPest

Static AnalysisPHPStan

Code StyleLaravel Pint

### Embed Badge

![Health badge](/badges/workdoneright-laravel-deletion-guard/health.svg)

```
[![Health](https://phpackages.com/badges/workdoneright-laravel-deletion-guard/health.svg)](https://phpackages.com/packages/workdoneright-laravel-deletion-guard)
```

###  Alternatives

[silber/bouncer

Eloquent roles and abilities.

3.6k4.4M25](/packages/silber-bouncer)[dyrynda/laravel-model-uuid

This package allows you to easily work with UUIDs in your Laravel models.

4802.8M8](/packages/dyrynda-laravel-model-uuid)[watson/validating

Eloquent model validating trait.

9723.3M47](/packages/watson-validating)[spatie/laravel-model-flags

Add flags to Eloquent models

4301.1M1](/packages/spatie-laravel-model-flags)[clickbar/laravel-magellan

This package provides functionality for working with the postgis extension in Laravel.

423715.4k1](/packages/clickbar-laravel-magellan)[lacodix/laravel-model-filter

A Laravel package to filter, search and sort models with ease while fetching from database.

17649.9k](/packages/lacodix-laravel-model-filter)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
