The cast stores a line's amount as pence. hasMany is how those lines belong to this invoice. The foreign key lives on the lines table. Create lines through the relationship so invoice_id is set for you. A line created any other way is a row you hoped belonged to this invoice.
Eloquent, properly · Lesson 2
One to many, without the surprises
Video
01 The parent owns the collection
The method sits on Invoice, and the name is the word the team uses. lines() returns the query for this invoice's children. It does not load them until you ask.
class Example
{
public function lines(): HasMany
{
return $this->hasMany(InvoiceLine::class);
}
}Saving through the relationship is the difference between a line that belongs to this invoice and a line whose invoice_id you hoped you set. create() fills the foreign key and inserts the row. sum() on the relationship asks the database, so the total includes the line you just wrote.
$invoice->lines()->create([
'description' => 'Hosting',
'amount' => 2400,
]);
$total = $invoice->lines()->sum('amount');orderBy on the relationship is fine when every caller wants the same order. A scope is better when only one screen wants lines sorted by amount. Do not add a default order that forces a filesort on a report that never displays that order. The report pays for the sort on every query, including the ones that only sum the amount.
Deleting the invoice should decide what happens to the lines. If the migration cascades, the database does it. If it does not, delete the lines in the same transaction as the invoice, or you will leave rows that point at nothing. Eloquent does not delete children because a hasMany method exists. The foreign key, or a transaction you wrote, does that work.
02 The loaded collection misses a line you just created
lines() and lines are not the same thing. The method returns a query. The property returns the collection already loaded on the model, or lazy-loads it once. create() writes a row and does not push that row onto a collection you loaded earlier. A total taken from the property can ignore the line you just added.
$invoice->load('lines');
$invoice->lines()->create([
'description' => 'Support',
'amount' => 800,
]);
$fromMemory = $invoice->lines->sum('amount');
$fromQuery = $invoice->lines()->sum('amount');$fromQuery includes Support. $fromMemory does not, until you refresh the relation. unsetRelation('lines') drops the stale collection so the next read of the property loads again. refresh() reloads the invoice and its loaded relations, which is the heavier tool when you also need columns that changed in the database.
createMany() has the same gap. It inserts each line with the foreign key set, and the in-memory collection stays where it was. Use it when a form posts several lines and you want one foreign key for all of them. Then read the total from lines(), or reload, before you show the invoice back to the team.
A view that received the invoice after load('lines') is showing that snapshot. If the action creates a line and returns the same instance, the view is the stale one. Reload before you render, or build the response from the query. The relationship write is still the right insert. The property is only a cache of an earlier read.
Recap
You can create an invoice's lines through the relationship so invoice_id is set for you, and you can avoid totalling a collection that does not have the new row yet.
hasManylives on the invoice. The foreign key lives on the lines.- A default order on the relationship costs every query, including reports that do not display it.
- Deleting the invoice has to decide what happens to the lines.
- A loaded lines collection does not gain rows that
lines()->create()just inserted.
Check yourself · 3 questions
Question 1 of 3A line is created with InvoiceLine::create() and no invoice_id. What do you have?