A cast converts a column between its stored form and a PHP type in both directions, declared in a casts() method (the $casts property still works). Product::casts() returns ['price' => 'decimal:2', 'stock' => 'integer', 'attributes' => AsCollection::class], and Order::casts() maps status to the string-backed enum OrderStatus:
use App\Casts\AsMoney;
use App\Models\{Order, Product};
$p = Product::find(7);
echo json_encode([$p->price, $p->stock, get_class($p->attributes)]), "\n";
$o = Order::find(3);
echo $o->status->name, ' ', json_encode($o->status->canShip()), "\n";
$p->mergeCasts(['price' => AsMoney::class]);
echo $p->price, ' x 3 = ', $p->price->times(3), "\n";
$o->status = 'lost';... ["12.50",60,"Illuminate\\Support\\Collection"] ... Paid true USD 12.50 x 3 = USD 37.50 ValueError "lost" is not a valid backing value for enum App\Enums\OrderStatus.
decimal:2 returns a string on purpose, so money never passes through a binary float. AsCollection decodes the JSON column and encodes it on save. The name attributes works from outside, but inside the model $this->attributes is Eloquent's raw-value array, so avoid it in new schemas. The enum cast gave OrderStatus::Paid with its methods, and a bad value threw before any SQL. Other casts include boolean, datetime:Y-m-d, hashed and encrypted.
A custom cast implements CastsAttributes (make:cast). AsMoney::get() returns new Money((int) round($value * 100)), a readonly object of integer cents, and set() turns a Money back into "12.50". mergeCasts() applied it to one instance; in casts() it applies everywhere.