workdoneright/laravel-deletion-guard
Composer 安装命令:
composer require workdoneright/laravel-deletion-guard
包简介
Config-driven deletion guard for Laravel models
README 文档
README
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
- 🛡️ 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
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
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
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 table below for detailed feature parity.
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)
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
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 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 <message>- 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
Both modes support nearly all features, with one exception for security reasons:
| Feature | Explicit Mode | Docblock Mode | Example |
|---|---|---|---|
| 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?
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:
- Parsing and eval()ing PHP code from docblocks - Major security vulnerability
- 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
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
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
protected function deletionDependencies(): array { return [ 'posts' => [ 'withTrashed' => true, // Check including soft-deleted posts 'message' => 'Cannot delete user with posts (including deleted ones).', ], ]; }
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
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
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
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
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
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()- Returnsfalseif deletion blockers existforceDelete()- Checks if user is admin ($user->is_admin)
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
- The trait boots during model initialization and registers a
deletingevent listener - Before deletion, it checks all configured dependencies
- For each dependency, it queries the relationship to see if related records exist
- If any blockers are found, it throws a
DeletionBlockedException - If
allow_force_deleteis enabled andforceDelete()is called, guards are bypassed
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
- PHP 8.2 or higher
- Laravel 11.0 or 12.0
Testing
composer test
Changelog
Please see CHANGELOG for more information on what has changed recently.
Contributing
Please see CONTRIBUTING for details.
Security Vulnerabilities
Please review our security policy on how to report security vulnerabilities.
Credits
License
The MIT License (MIT). Please see License File for more information.
workdoneright/laravel-deletion-guard 适用场景与选型建议
workdoneright/laravel-deletion-guard 是一款 基于 PHP 开发的 Composer 扩展包,目前已累计 38 次下载、GitHub Stars 达 0, 最近一次更新时间为 2025 年 12 月 26 日, 在 PHP 生态内属于活跃度较高的组件。
它主要适用于以下技术方向: 「laravel」 「eloquent」 「delete-models」 「laravel-deletion-guard」 等业务场景。在实际项目中,围绕这些方向常见需要落地的问题包括:接口对接、性能调优、并发安全、与既有框架(Laravel / ThinkPHP / Yii / Webman 等)的兼容适配,以及生产环境的日志埋点与稳定性保障。
我们在过去多个企业项目中使用过 workdoneright/laravel-deletion-guard 或与其功能相近的方案,如果你在选型或落地过程中遇到问题,例如 版本兼容、二次改造、私有化封装、与内部系统对接、生产 BUG 排查,欢迎联系我们协助评估。
基于 workdoneright/laravel-deletion-guard 在你已有业务上做功能扩展、字段裁剪、UI 适配、与内部账号 / 权限 / 日志系统的深度对接。
线上偶发问题、内存泄漏、慢查询、并发异常等排查修复;针对高流量场景做缓存、队列、索引层面的调优。
承接完整的项目从需求 → 设计 → 开发 → 上线 → 长期运维;也可按月提供技术保姆服务。
与 workdoneright/laravel-deletion-guard 相关的其它包
同方向 / 同关键字的高下载量 PHP Composer 包推荐,方便对比选型:
A Laravel Eloquent model trait for translatable resource
A package for automatically encrypting and decrypting Eloquent attributes in Laravel 5.5+, based on configuration settings.
Help to manage meta data on Laravel Eloquent model
A package to cast json fields, each sub-keys is castable
Alfabank REST API integration
Auto-generate dummy data with realistic relationships per Model-like config classes
统计信息
- 总下载量: 38
- 月度下载量: 0
- 日度下载量: 0
- 收藏数: 0
- 点击次数: 21
- 依赖项目数: 0
- 推荐数: 0
其他信息
- 授权协议: MIT
- 更新时间: 2025-12-26