Eloquent Extras: Casts, Scopes & Soft Deletes
Everyday Eloquent features that keep models clean — automatic type conversion, reusable query filters, and a recoverable “trash bin” for deletes.
What you will learn
- Cast attributes to real PHP types
- Write a reusable query scope
- Use soft deletes to recover records
Three features you will reach for constantly
These are small, high-value Eloquent tools that paid courses teach because they appear in almost every project. We will take them one at a time.
Casts — get the right type back automatically
Databases store everything as text or numbers, so a true/false column comes back as 1 or 0, and a JSON column comes back as a plain string. Casts tell Eloquent to convert these into proper PHP types for you. You list them in a casts() method on the model:
// in the Product model
protected function casts(): array
{
return [
'in_stock' => 'boolean',
'released_at' => 'datetime',
'options' => 'array',
];
}Now $product->in_stock is a real true/false (not 1), $product->released_at is a Carbon date object you can format with ->format('d M Y'), and $product->options is a real PHP array you can loop over — even though the database stores it as a JSON string. The conversion happens both ways: set the array and Eloquent saves it back as JSON.
Note: Output:
Without casts: $product->in_stock → 1 (a number).
With the boolean cast: $product->in_stock → true (a real boolean you can use in @if).
Query scopes — name a filter once, reuse it everywhere
If you keep writing ->where('in_stock', true) all over your code, give it a name. A scope is a method on the model, prefixed with scope, that adds a condition to a query:
// in the Product model
public function scopeInStock($query)
{
return $query->where('in_stock', true);
}You define scopeInStock but call it without the scope prefix, as inStock():
$available = Product::inStock()->get();
$cheapAvailable = Product::inStock()->where('price', '<', 500)->get();The first line reads almost like English — "get the products that are in stock". The second chains an extra where onto the same scope. The filter lives in one place, so if "in stock" ever changes meaning you fix it once.
Soft deletes — a recoverable trash bin
By default delete() removes a row forever. Soft deletes instead set a deleted_at timestamp, so the row stays in the table but is hidden from normal queries — like moving a file to the trash instead of shredding it. To turn it on, add a deleted_at column in the migration and use the trait on the model:
// migration: $table->softDeletes(); // adds the deleted_at column
// in the model
use Illuminate\Database\Eloquent\SoftDeletes;
class Product extends Model
{
use SoftDeletes;
}Now deleting and recovering behave like a trash bin:
$product->delete(); // sets deleted_at — row is hidden, not gone
Product::all(); // does NOT include soft-deleted rows
Product::withTrashed()->get(); // include them
$product->restore(); // bring it back
$product->forceDelete(); // really delete it foreverStep by step: delete() now just stamps deleted_at, so Product::all() quietly skips it. withTrashed() shows the hidden ones too, restore() clears the timestamp to bring a record back, and forceDelete() is the only thing that truly erases it. This saves you from accidental permanent data loss.
Note: Output:
After $product->delete(): the row still exists in the database with a filled deleted_at, but Product::all() no longer returns it. restore() makes it reappear.
Tip: Casts, scopes and soft deletes all live on the model, so every query in your app benefits automatically — this is the “fat model, skinny controller” idea that keeps controllers clean.
Q. What does enabling soft deletes change about delete()?
✍️ Practice
- Add a boolean and a datetime cast to a model and confirm the types in a Blade view.
- Write a query scope and enable soft deletes, then test
withTrashed()andrestore().
🏠 Homework
- Add soft deletes to a CRUD resource and build a simple “trash” page that lists soft-deleted records with a restore button.