This series follows one invoice in a multi-tenant app. A team owns invoices. An invoice has lines. That is enough to show relationships, scopes and the queries that get expensive.
Each lesson is a page, with a short video and the code the video is about. Mark a lesson as read when you have tried the idea, then use Next. The last lesson points at the next series, and this overview stays one link away.
It is for a developer who already has a team and its invoices, and wants that model to be safe to query. You build the invoice, its lines and its tags, then a team-scoped list in two queries, then an API that shows only the fields you chose.
01 The model the lessons share
class Invoice extends Model
{
public function team(): BelongsTo
{
return $this->belongsTo(Team::class);
}
public function lines(): HasMany
{
return $this->hasMany(InvoiceLine::class);
}
}Create the invoice from the team, so the foreign key is set by the relationship rather than by the request. The later lessons come back to this model: how a column is stored, how a list is queried, and what an API is allowed to show.
02 The lessons
- Models that match the domain. Name the model and its methods for the words the team already uses, and leave the team id off the fillable list.
- Migrations you can still read next year. Constrain the foreign key in the same migration, and keep an invoice number unique to its team.
- Casts, enums and custom casters. Bring dates back as dates and a status back as an enum, so a typo never lands in the column.
- One to many, without the surprises. Create an invoice's lines through the relationship, so the foreign key is set for you.
- Belongs-to and
whereBelongsTo. Query a team's invoices through the relationship instead of a hand-written team id. - Many to many and pivot data. Let a tag sit on many invoices, and keep the extra pivot column when you need it.
- Has many through. Reach a team's invoice lines without loading every invoice in PHP first.
- Local and global scopes. Name the constraints you mean, and keep a team filter local until a missed one has actually shipped a bug.
- Eager loading on purpose. Load the relationships a page will touch, and leave the rest alone.
- Catching N+1 queries. Turn a forgotten eager load into an exception outside production.
- Indexes, select and chunking. Match the index to the list you actually run, and walk a large table without loading it.
- API resources without leaking attributes. Decide the fields a client may see, so a new column does not become public by accident.