laravelmastery
SnippetsArticlesSeriesNews

Eloquent, properly · Lesson 1 of 3

Eloquent, properly · Lesson 1

Models that match the domain

Shape the dataEloquentQueries

This series ends with a team's invoices modelled, related and served through an API. Name the model and its methods for the words the team already uses. Invoice, not Data. lines(), not invoiceLineItems(). A later reader should guess the relationship from the method. The table can stay plural and the foreign key can stay team_id. The names a person says out loud belong on the class.

Video

01 Say what an invoice is allowed to hold

$fillable is the list of attributes a request is allowed to write. Leave the team id off it when the controller sets the team from the signed-in user. A mass-assigned team_id is how an invoice moves to another tenant.

Number, issued date and status are the fields the form is allowed to choose. notes is deliberately left off $fillable. Refuse attributes a model will throw away is the case where that column is added.

Invoice.phpPHP
class Invoice extends Model
{
    protected $fillable = [
        'number',
        'issued_on',
        'status',
    ];

    public function team(): BelongsTo
    {
        return $this->belongsTo(Team::class);
    }
}

Create it from the team, so the foreign key is set by the relationship rather than by the request. The same shape creates a line. The invoice you are holding writes invoice_id, and the request never sees that column.

PHP
$invoice = $team->invoices()->create([
    'number' => $data['number'],
    'issued_on' => $data['issued_on'],
    'status' => 'draft',
]);

A reader who opens the class can see both decisions. The relationship is called team(), because an invoice has one team. The fillable list is short, because the form is not the place that chooses a tenant. A factory or a seeder that needs to set team_id can call forceFill() or create through the relationship.

Rule

Never put team_id in $fillable to make a seeder easier — the form inherits that change.

02 Make a discarded team id throw

Eloquent drops a key that is not fillable and carries on. The invoice is stored. team_id stays null, or stays on the old team, and the request looks successful. Outside production, turn that silence into an exception. It sits in the same boot method as the lazy-load guard, and it stays off for customers so a forgotten key degrades instead of failing the request.

PHP
public function boot(): void
{
    Model::preventSilentlyDiscardingAttributes(! app()->isProduction());
}

Post team_id with the rest of the form and the test now fails, naming the attribute the model refused. The fix is not to add team_id to $fillable.

The fix is to stop sending it, and to keep creating the invoice from the team. The guard does not write the foreign key for you. It only stops the column from being thrown away without a trace. In production the same request still discards the key, which is why the relationship, not the guard, is what keeps the tenant.

A job or a command that builds an invoice has the same rule. Load the team, then call invoices()->create(). Copying request input into Invoice::create() is how a queued payload chooses a tenant the signed-in user never belonged to.

Recap

You can name an invoice for the domain, keep team_id off the fillable list, and hear about it when a request tries to set that column anyway.

  • lines() is the relationship a reader should guess.
  • Create the invoice from the team so the foreign key is not in the request.
  • A fillable team_id is how an invoice changes tenant.
  • preventSilentlyDiscardingAttributes() throws outside production when a key is dropped.

Check yourself · 3 questions

Question 1 of 3A form posts team_id and the column is fillable. What can a member of Team A do?

The model names the columns. Next, the migration that stores them.

← PreviousOverview

On this page

  • Say what an invoice is allowed to hold
  • Make a discarded team id throw
  • 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.