In one sentence

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.

Why it matters

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.

The big picture

Conventions connect the dots

๐Ÿ—„๏ธposts tableplural, snake_case
๐ŸงฑPost modelsingular, StudlyCase
๐Ÿ“ฆ$post objectone row, as PHP
Name things the Laravel way and Eloquent finds the right table with zero configuration.
Model classTable Eloquent expectsForeign key elsewhere
Postpostspost_id
Userusersuser_id
Categorycategoriescategory_id
Commentcommentscomment_id
BlogPostblog_postsblog_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.

Step by step

Build the Post model

We'll assume the posts table from the Migrations lesson exists.

  1. 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 in posts.

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

  3. 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 post user_id=1 and claim someone else's post. $fillable is 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 except id. Pick one style per model. $fillable is the safer default for beginners because new columns are blocked until you opt in.

    โ„น๏ธ Notice what's missing

    user_id isn't fillable. You'll set it from the logged-in user instead, e.g. $request->user()->posts()->create(...). More in SQL Injection & Mass Assignment.

  4. 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 */ }
            
  5. 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->title and $post->reading_time (the method name in camelCase becomes a snake_case property).

  6. Let timestamps work for you

    When a model is created, Eloquent fills created_at and updated_at. On every save it refreshes updated_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
            
Common mistakes

Avoid this, do that

โŒ Avoid

          protected $guarded = [];

          Post::create($request->all());
        

An empty $guarded plus all() lets users set any column, including ones you never put in the form.

โœ… Do

          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.

โŒ Avoid

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

โœ… Do

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

๐Ÿ’ก Pro tip: poke your models in Tinker

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.

๐Ÿ‹๏ธ Try it yourself
  1. Create Category, Comment and Tag models with php artisan make:model.
  2. Give Category a $fillable of name and slug.
  3. Add a casts() method to Post that casts published_at to datetime and is_featured to boolean.
  4. Add an excerpt accessor that returns the first 120 characters of body using Str::limit().
  5. Open Tinker and check that Post::first()->excerpt works.
Recap

Key takeaways

Check yourself

Mini quiz