Model::delete
Removes the model's row from the database, or sets deleted_at when the model uses SoftDeletes.
$model->delete(): bool|nullReturn value
bool|nullTrue when the row was deleted, false when a deleting event cancelled it, and null when the model does not exist in the database.
Errors
Throws LogicException with the message 'No primary key defined on model.' when the model has no key name, since the delete cannot be constrained to one row.
Called on a model instance, delete issues a DELETE constrained to that model's primary key, touches any parents listed in $touches, and fires the deleting and deleted events.
When to reach for it
Use the instance method when you have the model in hand and want its events and observers to run — cleaning up files, cascading to related records, writing an audit entry. When you only have ids, destroy(1, 2, 3) loads each model and calls delete on it, so events still fire.
What trips people up
Adding the SoftDeletes trait changes what this method does without changing how it is called: the row stays and deleted_at is stamped instead. Soft-deleted rows then disappear from every ordinary query, which reads like a hard delete until someone opens the table. withTrashed() brings them back, restore() clears the column, and forceDelete() removes the row for real.
The other trap is the query-builder form. Flight::where('active', 0)->delete() returns the number of affected rows, not a boolean, and fires no model events at all — the models are never loaded, so observers never run.
Example
<?php
use App\Models\Flight;
$flight = Flight::find(1);
var_dump($flight->delete()); // bool(true)
var_dump($flight->exists); // bool(false)
// A model that was never persisted returns null, not false.
$unsaved = new Flight(['name' => 'Nowhere']);
var_dump($unsaved->delete()); // NULL
// Delete by key without retrieving first (still fires events).
Flight::destroy(2);
Flight::destroy(3, 4, 5);
Flight::destroy([6, 7]);
echo Flight::count(); // 0When to use it
- Delete a record from a controller's destroy action and let an observer clean up its uploaded files.
- Soft delete a user account so it can be restored during a grace period.
- Purge rows older than a retention window from a scheduled command.
- Permanently remove a soft-deleted record once its retention period has passed.
Frequently asked questions
Why does delete() not remove the row from my table?
The model almost certainly uses the SoftDeletes trait, which stamps deleted_at instead of issuing a DELETE. Query with withTrashed() to see the row, and call forceDelete() to remove it permanently.
Why do my model events not fire when deleting many records?
Because Flight::where(...)->delete() deletes at the query level without instantiating any models, so deleting and deleted never fire. Iterate with chunkById and call delete() on each model when observers need to run.
What does Model::delete() return in Laravel?
True when the row was deleted, false when a deleting event returned false, and null when the model does not exist in the database. The query-builder form returns the number of affected rows instead.
More examples
The same call, soft deleted
Nothing about the delete() call changed — only the trait on the model. The row survives with deleted_at populated, and a global scope hides it from every query that does not opt in. This is why a "deleted" record can still collide with a unique index: the database still holds it.
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class Flight extends Model
{
use SoftDeletes;
}
// Elsewhere...
$flight = Flight::find(1);
$flight->delete();
var_dump($flight->trashed()); // bool(true)
echo $flight->deleted_at; // 2024-01-01 09:00:00
echo Flight::count(); // 0 — hidden from normal queries
echo Flight::withTrashed()->count(); // 1 — the row is still there
echo Flight::onlyTrashed()->count(); // 1
$flight->restore();
echo Flight::count(); // 1
$flight->forceDelete(); // gone for good
echo Flight::withTrashed()->count(); // 0Mass delete returns a count and fires no events
The two forms differ in both return type and side effects. Because the mass delete never instantiates the models, observers, deleting events and any file cleanup they perform are skipped silently — which is how orphaned uploads accumulate after a bulk purge. Use chunkById with per-model deletes when the events matter.
<?php
use App\Models\Flight;
// Instance delete: returns bool, fires deleting/deleted per model.
$one = Flight::find(1)->delete();
var_dump($one); // bool(true)
// Query delete: returns the number of affected rows, fires nothing.
$many = Flight::where('active', 0)->delete();
var_dump($many); // int(37)
// Everything in the table.
$all = Flight::query()->delete();
var_dump($all); // int(1463)Cleaning up in an observer
deleting runs before the row disappears, which is the only point at which related records and file paths are still reachable. Returning false from this handler would cancel the delete and make delete() return false — useful for guarding records that are still referenced elsewhere.
<?php
namespace App\Observers;
use App\Models\Flight;
use Illuminate\Support\Facades\Storage;
class FlightObserver
{
public function deleting(Flight $flight): void
{
Storage::disk('public')->delete($flight->manifest_path);
$flight->passengers()->delete();
}
}
// In a controller...
$flight = Flight::findOrFail(1);
if ($flight->delete()) {
echo "deleted\n";
}
Discussion