An Eloquent model is a PHP class that represents one database table, where each object you create from it is one row.
๐๏ธA table is a filing cabinet, and the model is the trained clerk who works it. You ask the clerk "give me post 7" or "file this new post", and they handle the drawers, labels and paperwork for you.
Talk to tables like they're objects
Rows become objects
Instead of juggling raw arrays, you write $post->title and $post->save(). Your editor can autocomplete it.
Safety built in
$fillable stops attackers from sneaking extra fields like is_admin into your forms.
Data comes out ready to use
Casts turn "1" into true and date strings into Carbon objects automatically.
Conventions connect the dots
| Model class | Table Eloquent expects | Foreign key elsewhere |
|---|---|---|
Post | posts | post_id |
User | users | user_id |
Category | categories | category_id |
Comment | comments | comment_id |
BlogPost | blog_posts | blog_post_id |
Eloquent also assumes a primary key called id and two timestamp columns, created_at and updated_at. All of these can be overridden, but you rarely need to.
-
Generate the model
Models live in
app/Models/. Add flags to create related files at the same time.php artisan make:model Post # just the model php artisan make:model Category -m # + migration php artisan make:model Comment -mfc # + migration, factory, controller<?php namespace App\Models; use Illuminate\Database\Eloquent\Model; class Post extends Model { // }That empty class already works.
Post::all()would return every row inposts. -
Override conventions only when you must
If you're stuck with a legacy table, set a property to tell Eloquent the real names.
class Post extends Model { protected $table = 'blog_articles'; // table isn't "posts" protected $primaryKey = 'article_id'; // key isn't "id" public $timestamps = false; // no created_at / updated_at }For a fresh project, skip all three and follow the conventions.
-
Guard against mass assignment
Mass assignment means filling many attributes at once from an array, like
Post::create($request->all()). That's convenient but risky: a sneaky user could postuser_id=1and claim someone else's post.$fillableis an allow-list of columns that may be mass-assigned.class Post extends Model { protected $fillable = [ 'title', 'slug', 'body', 'category_id', 'published_at', ]; }The opposite is
$guarded, a block-list.protected $guarded = ['id'];allows everything exceptid. Pick one style per model.$fillableis the safer default for beginners because new columns are blocked until you opt in.โน๏ธ Notice what's missinguser_idisn't fillable. You'll set it from the logged-in user instead, e.g.$request->user()->posts()->create(...). More in SQL Injection & Mass Assignment. -
Cast columns to useful PHP types
Databases return most values as strings. The
casts()method tells Eloquent how to convert each column on the way out (and back on the way in).protected function casts(): array { return [ 'published_at' => 'datetime', // Carbon date object 'is_featured' => 'boolean', // true / false 'views' => 'integer', 'meta' => 'array', // JSON column โ PHP array ]; }$post->published_at->diffForHumans(); // "3 days ago" if ($post->is_featured) { /* real boolean */ } -
Shape values with accessors and mutators
An accessor changes a value when you read it; a mutator changes it when you write it. In modern Laravel, both are one method returning an
Attribute.use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Support\Str; // Accessor + mutator on a real column protected function title(): Attribute { return Attribute::make( get: fn (string $value) => Str::headline($value), set: fn (string $value) => trim($value), ); } // A "virtual" attribute with no column behind it protected function readingTime(): Attribute { return Attribute::make( get: fn () => max(1, (int) ceil(str_word_count($this->body) / 200)) . ' min read', ); }Use them as normal properties:
$post->titleand$post->reading_time(the method name in camelCase becomes a snake_case property). -
Let timestamps work for you
When a model is created, Eloquent fills
created_atandupdated_at. On every save it refreshesupdated_at. Both come back as Carbon dates automatically.$post = Post::create(['title' => 'Hello', 'slug' => 'hello', 'body' => '...']); $post->created_at; // 2025-03-10 12:00:00 (Carbon) $post->created_at->format('M j, Y'); // "Mar 10, 2025" $post->touch(); // bump updated_at only
Avoid this, do that
protected $guarded = [];
Post::create($request->all());
An empty $guarded plus all() lets users set any column, including ones you never put in the form.
protected $fillable = ['title', 'slug', 'body'];
Post::create($request->validated());
An explicit allow-list plus validated input means only the fields you intended can ever be written.
{{-- In every Blade view... --}}
{{ \Carbon\Carbon::parse($post->published_at)->format('M j') }}
Parsing the same column by hand everywhere is repetitive and easy to forget.
{{-- casts(): 'published_at' => 'datetime' --}}
{{ $post->published_at->format('M j') }}
Cast it once in the model and every part of your app gets a real date object.
php artisan tinker opens an interactive PHP shell with your app loaded. Try Post::first() or new Post(['title' => 'hi']) to see exactly what a model holds.
- Create
Category,CommentandTagmodels withphp artisan make:model. - Give
Categorya$fillableofnameandslug. - Add a
casts()method toPostthat castspublished_attodatetimeandis_featuredtoboolean. - Add an
excerptaccessor that returns the first 120 characters ofbodyusingStr::limit(). - Open Tinker and check that
Post::first()->excerptworks.
Key takeaways
- One model per table:
Postโposts,Categoryโcategories. $fillableis an allow-list for mass assignment;$guardedis a block-list. Prefer$fillable.casts()converts columns into dates, booleans, integers and arrays automatically.Attribute::make(get:, set:)defines accessors and mutators in one method.created_atandupdated_atare managed for you.