The pivot joined a tag to many invoices. A team still needs every line it has billed. hasManyThrough() answers that without an intermediate collection in PHP. You do not load the invoices, loop, and load again. The relationship is one query.
Eloquent, properly · Lesson 2
Has many through
Video
01 Walk two foreign keys
The first class is the far model, the one you want. The second is the model in the middle. Laravel follows team_id on invoices, then invoice_id on lines. The team is $this, so it is not an argument. Swap the two classes and the query looks for a key that is not there.
class Team extends Model
{
public function invoiceLines(): HasManyThrough
{
return $this->hasManyThrough(InvoiceLine::class, Invoice::class);
}
}The query is still a join. Constrain it when you do not want every line the team has ever written. The status column lives on invoices, so the where names that table. A where on amount needs no prefix while amount lives only on the lines. The moment both tables share a column name, qualify it.
$lines = $team->invoiceLines()
->where('invoices.status', 'issued')
->get();If you need columns from the invoice as well as the line, a join you write yourself, or an eager load of invoices.lines, is clearer than stretching the through relationship. hasManyThrough() is for the far collection, not for a custom report. The line model that comes back is an InvoiceLine. The invoice number is not an attribute of that line unless you selected it, and selecting it in here is how the relationship turns into a report you will not recognise next month.
02 Count and sum without loading the lines
A team index that shows how many lines each team has billed does not need the lines. withCount() adds invoice_lines_count to each team and leaves the far rows in the database. The name is the relationship with _count on the end. Read that attribute in the view. Do not call invoiceLines and count the collection, or you have loaded every line to print an integer.
$teams = Team::query()
->withCount('invoiceLines')
->get();
$total = $team->invoiceLines()
->where('invoices.status', 'issued')
->sum('invoice_lines.amount');sum() is the same join with an aggregate. Qualify invoice_lines.amount so a later amount column on invoices cannot make the total ambiguous. The result is a number of pence, matching the integer cast on the line. It is not a collection, and it does not give you descriptions. When the screen needs the rows, go back to get(). When it needs a total for issued invoices only, keep the where on invoices.status that the list already uses.
withCount() accepts a constraint when the badge should ignore drafts. A closure on the relationship applies the same invoices.status filter without a second query per team. Skipping the constraint counts every line the team has ever had, including drafts. That may be the badge you want. It is not the same number as the sum of issued lines, so do not label them as if they were.
The through relationship stays a poor place to reshape the line into an invoice. If the page is really a list of invoices with their lines nested, eager load invoices.lines and let the invoice resource, or the view, walk that tree. hasManyThrough() has done its job when the question was about the lines and the invoices were only the path.
Recap
You can reach a team's invoice lines without loading every invoice in PHP first, and you can count those lines without loading them either.
- The first class is the far model. The second is the one in the middle.
- The query follows
team_idon invoices, theninvoice_idon lines. - A report that needs invoice columns is clearer as an eager load or a join you write.
withCount('invoiceLines')andsum()answer the index without hydrating the lines.
Check yourself · 3 questions
Question 1 of 3You want every line a team has billed. What do you call?