# Laravel Migration Rollbacks Fail to Trigger Model Deletion Events
## Summary
- Model deletion events (`deleting`/`deleted`) are not triggered during Laravel migration rollbacks when using `Schema::drop()` or `Schema::dropIfExists()`.
- This occurs because table dropping bypasses Eloquent model hooks and executes SQL-level `DROP TABLE` commands.
- External API cleanup logic attached to model events fails to execute during migration reversals.
## Root Cause
- Migrations operate at the database schema layer, not the Eloquent ORM layer
- `Schema::drop()` executes a direct `DROP TABLE` SQL statement:
- No individual records are loaded as Eloquent models
- Model lifecycle events never fire
- Eloquent events only trigger during model instance operations (e.g., `delete()` called on a model instance)
## Why This Happens in Real Systems
- Synchronization requirements with external services naturally evolve over time
- Developers attach side-effects to model events for simplicity
- Migrations are primarily viewed as schema-management tools, not data-cleanup pipelines
- Rollback scenarios are often secondary considerations after initial deployment
## Real-World Impact
- Orphaned records in external APIs when rolling back deployments
- Data inconsistency between local DBs and third-party services
- Hidden technical debt requiring manual API cleanup
- Integrity failures during repeated migration refreshes (local/test environments)
## Example Code
⚠️ **Problematic Implementation:**
```php
// Migration down() method
public function down()
{
Schema::drop('items'); // Bypasses model events
}
// Model event hook
class Item extends Model
{
protected static function booted()
{
static::deleting(function ($item) {
API::delete($item->id); // Never called during rollback
});
}
}
✅ Solution Migration:
public function down()
{
// Delete models using Eloquent before dropping table
DB::transaction(function () {
Item::withTrashed()->ifUsingSoftDeletes()->each(function ($item) {
$item->forceDelete(); // Triggers deleting/deleted events
});
});
Schema::drop('items');
}
How Senior Engineers Fix It
-
Explicit cleanup in migrations: Use Eloquent queries in
down()to force event triggering -
Extract API interactions: Move API sync logic to a dedicated service class:
class ApiSynchronizer { public static function deleteItem(Item $item) { API::delete($item->id); } } // Use synchronizer in both places: Item::deleting(fn($item) => ApiSynchronizer::deleteItem($item)); Migration::down() => Api