skip to content

Eloquent Models

Eloquent's model layer: table conventions, relations and eager loading, casts, scopes and lifecycle events. Interviewers go straight at lazy loading and N+1, which Eloquent makes effortless to cause.

on this pageshow

explore

questions

page 1 of 2

In a Laravel 13 Eloquent model, what does casts() declare, how does it relate to $casts, and what do common built-in casts return?

level: juniorimportance: must knowfreq 62%

answer

  1. column name to type map
  2. method can call static helpers
  3. casts() wins over $casts on conflict
  4. datetime:format only shapes output
  5. decimal:2 returns a string

basics

~20 s

casts() returns a map of attribute names to cast types such as boolean, datetime, decimal:2, enum classes and cast classes, applied on every read and write. The older $casts property still works; casts() is merged over it and can call static helpers.

solid answer

~40 s

In Laravel 13 a model declares `protected function casts(): array` returning, for example, `['paid' => 'boolean', 'amount' => 'decimal:2', 'issued_at' => 'immutable_datetime', 'status' => InvoiceStatus::class]`. Eloquent converts each value when you read the attribute, and converts dates, JSON, enums and cast classes back to a storable form when you set them. The older `protected $casts = [...]` property still works; at model initialisation the two are merged with `array_merge($this->casts, $this->casts())`, so the method wins on a conflict. The method exists mainly because a property default cannot call code, while the method can return `AsCollection::using(InvoiceLines::class)`. Two built-ins surprise people: `decimal:2` returns a **string** rounded to two places, not a float, and a format such as `datetime:Y-m-d` changes only the array and JSON output, not the Carbon object you read.

code

php · 20 lines
php
<?php

namespace App\Models;

use App\Enums\InvoiceStatus;
use Illuminate\Database\Eloquent\Model;

class Invoice extends Model
{
    protected function casts(): array
    {
        return [
            'paid' => 'boolean',
            'amount' => 'decimal:2',            // '19.90' (string)
            'issued_at' => 'immutable_datetime',
            'due_at' => 'datetime:Y-m-d',        // Carbon; JSON prints 2026-10-01
            'status' => InvoiceStatus::class,
        ];
    }
}

go deeper

for a junior

Recall that casts() maps attributes to types such as boolean, datetime and decimal, and that casts convert both on read and on write.

for a middle

Explain the merge between $casts and casts(), why decimal returns a string, and that a date format only affects serialization.

for a senior

Catch float money, mutable date aliasing and format-versus-storage confusion in review, and pick immutable_datetime by default.

for a principal

Set conventions for money, time zones and date serialization across services so API consumers never parse ambiguous values.

## What a cast is Database drivers hand PHP mostly strings and integers: a `paid` flag arrives as `0` or `1`, a timestamp as `'2026-09-29 10:15:00'`, a JSON column as text. A **cast** tells Eloquent to convert an attribute to a richer PHP type when you read it and back to a storable value when you set it, so the rest of the code works with booleans, dates, enums and value objects instead of raw strings. ## Declaring casts: the method and the property The Laravel 13 skeleton's `User` model declares casts with a method: ```php protected function casts(): array { return [ 'email_verified_at' => 'datetime', 'password' => 'hashed', ]; } ``` The older form, `protected $casts = [...]`, still works. When the model initialises, Eloquent merges both with `array_merge($this->casts, $this->casts())`, so an entry in `casts()` **overrides** the same key in `$casts`. The method form matters because a property's default value must be a constant expression, while a method can call helpers such as `AsCollection::using(InvoiceLines::class)` or `AsEnumCollection::of(Feature::class)`. At runtime, `mergeCasts()` adds casts to one instance, and `withCasts()` adds them to one query. ## The built-in casts you meet most | Cast | Read value | Notes | |---|---|---| | `boolean`, `integer`, `float`, `string` | PHP scalar | `null` stays `null` | | `datetime` | `Illuminate\Support\Carbon` | mutable instance | | `immutable_datetime` | `CarbonImmutable` | `addDays()` returns a new object | | `date` / `immutable_date` | Carbon at 00:00:00 | time is dropped | | `datetime:Y-m-d` | Carbon | the format applies only to `toArray()` and JSON | | `decimal:2` | **string**, e.g. `'19.90'` | rounded half up to the given scale | | `array`, `json`, `object`, `collection` | decoded JSON | whole-value replacement only | | `encrypted`, `encrypted:array` … | decrypted value | stored as ciphertext | | `hashed` | stored hash | hashes plain text on assignment | | `InvoiceStatus::class` | enum case | backed or pure enum | ## Traps interviewers probe 1. **`decimal:2` is a string.** The framework formats the value with an arbitrary-precision decimal library and returns a string, so `'19.90'` survives without float rounding. Comparing it with `===` against a float fails; doing arithmetic on it silently converts to float. 2. **A date format does not format the object.** `'due_at' => 'datetime:Y-m-d'` still gives a full Carbon on `$invoice->due_at`; only `toArray()` and `toJson()` print `2026-10-01`. Without a format, dates serialize as UTC ISO-8601 (`2026-10-01T00:00:00.000000Z`). The storage format is a separate setting on the model. 3. **Mutable dates are shared objects.** With `datetime`, `$due = $invoice->due_at; $reminder = $due->subDays(3);` also moves `$due`, because Carbon's mutators change and return the same instance; `immutable_datetime` returns a new object and makes that bug impossible. Each read of the attribute re-parses the stored value, so neither form writes a change back unless you assign it. 4. **Not every cast converts on write.** Dates, JSON, encrypted, hashed, enum and class casts convert the value when you assign it. Scalar casts such as `boolean` and `integer` convert only on read, so `$invoice->paid = 'yes'` writes the raw `'yes'`; validate input before assigning it. ## How casts interact with dirty checking Casts also decide whether a model is **dirty**. Eloquent compares the new raw value with the original raw value, applying cast-aware comparison, so: - assigning `'1'` to a `boolean` attribute whose stored value is `1` is not treated as a change; - assigning a Carbon for the same instant to a `datetime` attribute is not a change either; - assigning `'19.9'` to a `decimal:2` attribute holding `'19.90'` compares the scaled values, not the raw strings. This keeps `save()` from issuing pointless updates when a form re-submits the same values. ## A checklist for a new model - Every flag column gets `boolean`, every timestamp gets `datetime` or `immutable_datetime`. - Money never uses `float`: use `decimal:<scale>`, or integer cents behind a value-object cast. - Status columns use an enum class instead of strings. - JSON columns choose between `array` and the object casts depending on whether code edits keys in place. - Secrets use `encrypted`, passwords use `hashed`. ## Where custom behaviour goes When the built-ins are not enough, the same map accepts a **cast class** (implementing `CastsAttributes`) or an enum class. Computed values that do not map to one column belong in an accessor instead. Both are covered by their own questions; the principle is the same: the model converts at the boundary, so controllers and views never see raw column text.

  • How do you cast a raw value selected in one query, such as MAX(paid_at)?
    Call `withCasts(['last_paid_at' => 'datetime'])` on the query builder. The cast applies to the models that query returns, so the aggregated column comes back as a Carbon instance instead of a string, without changing the model's permanent `casts()` map.
  • Why store money as decimal:2 rather than float?
    A float cannot represent most decimal fractions exactly, so sums drift by fractions of a cent. `decimal:2` returns a string rounded half up to two places by an arbitrary-precision library, which keeps the exact value from the column. Many teams go further and store integer cents behind a custom cast that returns a money value object.

saying these in an interview costs you the question

  • Saying decimal:2 returns a float rounded to two places
  • Believing datetime:Y-m-d changes the Carbon object you read
  • Claiming the $casts property no longer works in Laravel 13
  • Thinking $casts wins when both declare the same attribute
  • Treating $invoice->due_at as a plain string after a datetime cast
open as a page

In an Eloquent model, how do $hidden, $visible and $appends shape toArray() and JSON output, and what is Laravel 13's attribute form?

level: juniorimportance: must knowfreq 60%

basics

~20 s

$hidden removes attributes and relations from toArray() and JSON, $visible keeps only those listed, and $appends adds accessor values that have no column. Laravel 13 also offers #[Hidden], #[Visible] and #[Appends] class attributes; none of them changes what is queried.

open as a page

In Laravel's Eloquent, which table, primary key and timestamp columns does a model assume by convention, and how do you override them?

level: juniorimportance: must knowfreq 70%

basics

~20 s

Eloquent assumes the snake_case plural of the class name as the table, an auto-incrementing integer id key, and managed created_at and updated_at columns. Override them with the $table, $primaryKey, $keyType, $incrementing and $timestamps properties, or Laravel 13's #[Table] attribute.

open as a page

In Eloquent, what do find(), findOrFail(), first() and firstWhere() return when no matching row exists?

level: juniorimportance: must knowfreq 72%

basics

~10 s

find(), first() and firstWhere() return null when nothing matches, while findOrFail() and firstOrFail() throw ModelNotFoundException. Choose the null-returning form when absence is normal and the OrFail form when absence is an error.

open as a page

In a Laravel blog index, how does Post::with('author') stop a Blade loop printing $post->author->name from running one query per post?

level: juniorimportance: must knowfreq 85%

basics

~20 s

Post::with('author') runs the posts query, then one extra query that fetches every needed author by id and attaches each to its post. The Blade loop then reads already-loaded authors: two queries instead of one per post.

open as a page

In Laravel 13, how do you generate an Eloquent observer class and register it so its methods run on model events?

level: juniorimportance: must knowfreq 60%

basics

~10 s

Run php artisan make:observer ListingObserver --model=Listing, which writes app/Observers/ListingObserver.php with methods named after events, then register it with #[ObservedBy([ListingObserver::class])] on the model or Listing::observe() in AppServiceProvider::boot().

open as a page

In an Eloquent model, what is the difference between $photo->tags and $photo->tags(), and when does each run a query?

level: juniorimportance: must knowfreq 66%

basics

~20 s

$photo->tags() returns the relation object, a query builder that runs a fresh query on each terminal call. $photo->tags returns the loaded results, querying once on first access and then reusing the collection cached on the model.

open as a page

In Eloquent, how do hasOne, hasMany and belongsTo differ, and which model's table holds the foreign key in each?

level: juniorimportance: must knowfreq 80%

basics

~20 s

hasOne and hasMany sit on the parent and expect the foreign key on the related table; belongsTo sits on the model whose own table holds that key. hasMany returns a Collection, hasOne and belongsTo a single model or null.

open as a page

In Laravel, how do you define and call a local Eloquent scope with #[Scope] or the scope prefix, including one that takes parameters?

level: juniorimportance: must knowfreq 58%

basics

~20 s

Mark a protected model method with #[Scope] (or name it scopeOpen) whose first parameter is the Eloquent Builder; extra parameters follow it. Call it as Matter::open()->assignedTo($lawyer)->get(), without any prefix, and chain it like any query method.

open as a page

With Eloquent's SoftDeletes trait, what do delete(), withTrashed(), onlyTrashed(), restore() and forceDelete() do to a matter and its queries?

level: juniorimportance: must knowfreq 62%

basics

~20 s

With SoftDeletes, delete() sets deleted_at instead of removing the row, and a global scope hides trashed rows from queries. withTrashed() includes them, onlyTrashed() returns only them, restore() clears deleted_at, and forceDelete() really deletes the row.

open as a page

In Laravel, how do you define an Eloquent accessor and mutator with Attribute::make(), and when do you write a CastsAttributes class instead?

level: middleimportance: must knowfreq 56%

basics

~20 s

Declare a camelCase method with a declared Attribute return type that returns Attribute::make(get: ..., set: ...). The get closure transforms reads and set can return several columns. Write a CastsAttributes class when the same conversion is reused across attributes or models.

open as a page

In an Eloquent model, what is the difference between $fillable and $guarded, and what happens to a key neither allows?

level: middleimportance: must knowfreq 82%

basics

~20 s

$fillable is an allow-list of keys fill() and create() may set; $guarded is a deny-list. A default model is totally guarded and throws MassAssignmentException, while a model with a list silently drops disallowed keys by default.

open as a page

In Eloquent, how do firstOrNew(), firstOrCreate() and updateOrCreate() differ, and which of them write to the database?

level: middleimportance: must knowfreq 65%

basics

~10 s

firstOrNew() returns the found row or a new unsaved model; firstOrCreate() inserts when nothing matches; updateOrCreate() inserts when missing or fills and saves the found row. Only the last two write.

open as a page

In Eloquent many-to-many relations, how do attach(), detach(), sync(), syncWithoutDetaching() and toggle() differ, and what does sync() delete?

level: middleimportance: must knowfreq 63%

basics

~20 s

All five write pivot rows, never Tag rows. attach() inserts rows without checking for duplicates, detach() deletes them (every row when called without IDs), sync() makes the pivot match the given list exactly, syncWithoutDetaching() only adds, and toggle() flips each ID.

open as a page

With Eloquent, how do whereHas(), whereRelation(), doesntHave() and whereBelongsTo() filter photos by related rows, and what SQL do they produce?

level: middleimportance: must knowfreq 60%

basics

~20 s

They filter parents by related rows without loading them. whereHas() adds a WHERE EXISTS subquery (COUNT when given a count), doesntHave() adds NOT EXISTS, whereRelation() is whereHas() with one where, and whereBelongsTo() adds a foreign-key WHERE IN.

open as a page

In Eloquent, how do you declare a many-to-many relation whose pivot table stores a grade, and when do you add a custom pivot with using()?

level: middleimportance: must knowfreq 70%

basics

~20 s

Declare belongsToMany() on both models, backed by an alphabetically named pivot table such as course_student, and add withPivot('grade') to read the extra column. Add using() with a Pivot subclass when the pivot row needs casts or behaviour.

open as a page

In Laravel, how does an Eloquent global scope such as a firm filter reach every model query, and how is it registered and bypassed?

level: middleimportance: must knowfreq 55%

basics

~20 s

A global scope is a class implementing the Eloquent Scope interface, or a named closure, registered with #[ScopedBy] or addGlobalScope() in booted(). Eloquent adds its constraint to every query of the model; withoutGlobalScope() removes it for one query.

open as a page

In Laravel, what does Model::preventLazyLoading() do, how should you enable it, and which lazy loads does it deliberately let through?

level: seniorimportance: must knowfreq 58%

basics

~20 s

Model::preventLazyLoading() makes Eloquent throw LazyLoadingViolationException when an unloaded relation is read as a property on a model that came from a multi-row result. Enable it outside production in AppServiceProvider::boot, or register a handler that logs.

open as a page

An Eloquent observer syncs listings to a search index and audit log, yet Listing::where('status', 'pending')->update([...]) triggered neither; why, and how do you fix it?

level: seniorimportance: must knowfreq 58%

basics

~20 s

Builder::update() runs one UPDATE and returns the affected-row count; no Listing models are loaded, so saving, updating, updated and saved never fire. Load and save each model, or keep the bulk write and reindex and audit the affected IDs explicitly.

open as a page

In Eloquent, what does associate() do on a belongsTo relation such as $photo->album(), and why must you still call save()?

level: juniorimportance: should knowfreq 44%

basics

~20 s

associate() sets the child's foreign key (photos.album_id) to the parent's key and puts the parent in the child's loaded relations, all in memory. It runs no SQL, so the change reaches the database only when save() is called on the child.

open as a page

With Eloquent, how do you cast an invoice's status column to a PHP enum, and what happens with values the enum does not have?

level: middleimportance: should knowfreq 50%

basics

~20 s

Map the column to the enum class in casts(), such as 'status' => InvoiceStatus::class. Reads return an enum case and writes store its backing value; a value the enum lacks raises a ValueError, from the database or from code.

open as a page

With Eloquent, why does $invoice->settings['theme'] = 'dark' fail with an array cast, and how do AsArrayObject and AsCollection fix it?

level: middleimportance: should knowfreq 38%

basics

~20 s

An array cast decodes JSON into a plain array on each read, so writing an offset modifies a temporary copy and PHP raises an 'indirect modification' notice. AsArrayObject and AsCollection return a cached object whose edits Eloquent re-encodes on save.

open as a page

In Laravel 13, how do Eloquent's HasUuids and HasUlids traits change a model's primary key, and when is the key value assigned?

level: middleimportance: should knowfreq 45%

basics

~20 s

HasUuids and HasUlids make the primary key a PHP-generated string, a UUIDv7 or a lowercase ULID, and report the key as a non-incrementing string. The value is assigned when the model is inserted, so a new unsaved instance has no key.

open as a page

In Eloquent, what do isDirty(), wasChanged() and getOriginal() report before and after save(), and how do refresh() and fresh() differ?

level: middleimportance: should knowfreq 45%

basics

~20 s

Before save(), isDirty() flags changed columns and getOriginal() returns the loaded values. After save(), isDirty() is false, wasChanged() reports what was written and getOriginal() returns the new values. refresh() reloads the same instance; fresh() returns a new one.

open as a page

In Eloquent, how does $product->update([...]) on a loaded model differ from Product::where('sku', $sku)->update([...]) on a query?

level: middleimportance: should knowfreq 58%

basics

~20 s

A model update fills and saves one loaded model, firing events, applying casts and writing only changed columns. A query update sends one UPDATE for every matching row, returns the affected count and skips model events, casts and mass-assignment rules.

open as a page

In Eloquent, how do load(), loadMissing() and loadCount() on already-fetched models differ from with(), and when does the $with property backfire?

level: middleimportance: should knowfreq 45%

basics

~20 s

load() eager loads relations onto models you already have and always queries again; loadMissing() loads only relations not yet set; loadCount() adds count attributes. $with eager loads on every query of the model, needed or not.

open as a page

With Eloquent, how do nested, constrained and column-limited with() calls work, and when do you need withWhereHas() instead?

level: middleimportance: should knowfreq 52%

basics

~20 s

Dot syntax loads a chain level by level; a closure constrains the related rows only; 'relation:col,...' trims columns but must keep keys. withWhereHas() also drops parents without a matching child, applying one closure to both.

open as a page

For Eloquent model events, when would you choose booted() closures, an observer class, or a $dispatchesEvents map, and why?

level: middleimportance: should knowfreq 40%

basics

~20 s

Use booted() closures for one or two small hooks that belong to the model, an observer when there are several handlers or they need injected services, and $dispatchesEvents when the model moment should become an event class that other listeners handle.

open as a page

With Eloquent, in what order do a model's lifecycle events fire on insert, update and delete, and how can a handler cancel the write?

level: middleimportance: should knowfreq 46%

basics

~20 s

Insert fires saving, creating, created, saved; update fires saving, updating, updated, saved; delete fires deleting then deleted. Returning false from a before-event such as saving or deleting cancels the write, and save() or delete() returns false.

open as a page

In Eloquent, what do saveQuietly() and Model::withoutEvents() suppress, and what surprises teams that rely on them?

level: middleimportance: should knowfreq 34%

basics

~20 s

saveQuietly() saves one model with Eloquent events muted; Model::withoutEvents() runs a closure with them muted. Observers, booted() closures and $dispatchesEvents are all skipped, timestamps are still written, and the muting covers every model class, not only the one named.

open as a page

showing 1–30 of 44