The migration decided what a column stores. A cast decides what it comes back as. Dates should be Carbon. Money in pence should be an integer, not a float. A status that has three legal values should be an enum. The mapping lives on the model, so a controller does not parse strings on the way in and again on the way out.
Eloquent, properly · Lesson 1
Casts, enums and custom casters
Video
01 Keep the status closed
A backed enum stored as a string means a typo never lands in the column. InvoiceStatus::Paid is the only paid value the rest of the code should hold. The database still stores paid. The cast is the only place that mapping lives, which is why it sits on the model and not in a helper each controller remembers to call.
enum InvoiceStatus: string
{
case Draft = 'draft';
case Issued = 'issued';
case Paid = 'paid';
}
class Invoice extends Model
{
protected function casts(): array
{
return [
'issued_on' => 'date',
'status' => InvoiceStatus::class,
];
}
}Reading the status is then a comparison, not a string you have to remember. Updating it passes the case, and Eloquent stores the backed value. A form request can accept the string draft and hand InvoiceStatus::from($data['status']) to the model, so the string comparison stays at the edge.
if ($invoice->status === InvoiceStatus::Paid) {
return;
}
$invoice->update(['status' => InvoiceStatus::Issued]);Add a custom caster only when the enum and the built-in casts are not enough. A caster that hides a join, or that queries, is a relationship pretending to be a cast. The status column does not need one. The date column does not need one. The built-in date cast already returns a Carbon instance for issued_on, and toDateString() is there when a resource wants the calendar day.
02 Store the line amount as pence
A line's amount is money, and money in this app is an integer number of pence. A float column will not hold 0.10 exactly, and a decimal you then cast to float brings the same problem back in PHP. unsignedInteger on the migration and the integer cast on the model keep every sum as a whole number until a view divides by 100 to show pounds.
class InvoiceLine extends Model
{
protected function casts(): array
{
return [
'amount' => 'integer',
];
}
}
$invoice->lines()->create([
'description' => 'Hosting',
'amount' => 2400,
]);2400 is £24.00. The column never sees 24.00. Summing lines is a sum of integers, which is the number you can compare in a test without a tolerance. Divide by 100 at the edge that renders a price, and do not store the result. A custom caster that returns a Money value object is justified only when several screens need the pounds and the pence together. Until then the integer is the cast. A caster that loads the invoice to decide the currency is a query hiding in an attribute, and it belongs on a relationship or a method you call on purpose.
The status enum and the amount integer solve different problems. One closes a set of strings. The other stops a float from rounding the bill. Both are declared in casts(), so the next person does not discover the rule by reading a controller.
Recap
You can bring a status back as an enum, a date back as a date, and a line amount back as pence.
- A backed enum stops a typo reaching the column.
- The cast lives on the model, not in every controller.
- A caster that queries is a relationship in disguise.
- amount is an integer of pence. Divide by 100 only when you render pounds.
Check yourself · 3 questions
Question 1 of 3A controller compares $invoice->status === 'paid' after the enum cast. What is wrong?