laravelmastery
SnippetsArticlesSeriesNews

Eloquent, properly · Lesson 1 of 3

Eloquent, properly · Lesson 1

Casts, enums and custom casters

Shape the dataEloquentQueries

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.

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.

InvoiceStatus.phpPHP
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.

PHP
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.

InvoiceLine.phpPHP
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?

The invoice now comes back as dates and an enum. Next, the lines that belong to it.

← PreviousMigrations you can still read next year

On this page

  • Keep the status closed
  • Store the line amount as pence
  • Recap
  • Check yourself

Eloquent, properly

  • 1 Overview

Shape the data

  • 2 Models that match the domain
  • 3 Migrations you can still read next year
  • 4 Casts, enums and custom casters

Connect it

  • 5 One to many, without the surprises
  • 6 Belongs-to and whereBelongsTo
  • 7 Many to many and pivot data
  • 8 Has many through

Query it safely and fast

  • 9 Local and global scopes
  • 10 Eager loading on purpose
  • 11 Catching N+1 queries
  • 12 Indexes, select and chunking
  • 13 API resources without leaking attributes

Eloquent, properly

Lessons

  1. 01Overview

Shape the data

  1. 02Models that match the domain
  2. 03Migrations you can still read next year
  3. 04Casts, enums and custom casters

Connect it

  1. 05One to many, without the surprises
  2. 06Belongs-to and whereBelongsTo
  3. 07Many to many and pivot data
  4. 08Has many through

Query it safely and fast

  1. 09Local and global scopes
  2. 10Eager loading on purpose
  3. 11Catching N+1 queries
  4. 12Indexes, select and chunking
  5. 13API resources without leaking attributes

Get the Friday email

One snippet and the week’s Laravel news, in your inbox every Friday morning. Unsubscribe whenever you like.

laravelmastery

Practical Laravel learning for working developers, made in the UK.

Learn

  • Snippets
  • Articles
  • Series

Stay current

  • News
  • Friday email
  • RSS feed

Company

  • About
  • Privacy
  • Terms
© 2026 Laravel Mastery. Independent and not affiliated with Laravel Holdings Inc.Designed and built by Half Shell Studios

We remember the videos you watch, where you left off, and which series lessons you mark as read, against this connection’s address. Nothing is stored until you agree.