order_items also stores quantity and the unit_price paid. Adding ->withPivot('quantity', 'unit_price') to both definitions puts them on $product->pivot; ->withTimestamps() maintains pivot created_at and updated_at, and ->as('line') renames the pivot attribute.
use App\Models\{Order, Product};
foreach (Order::find(2)->products as $book) {
echo "{$book->title}: {$book->pivot->quantity} x {$book->pivot->unit_price}\n";
}
$top = Product::withSum('orders as sold', 'order_items.quantity')
->orderByDesc('sold')->first();
echo "{$top->title}: {$top->sold}\n";Output
-- select * from `orders` where `orders`.`id` = 2 limit 1
-- select `products`.*, `order_items`.`order_id` as `pivot_order_id`,
`order_items`.`product_id` as `pivot_product_id`, `order_items`.`quantity` as
`pivot_quantity`, `order_items`.`unit_price` as `pivot_unit_price` from `products` inner
join `order_items` on `products`.`id` = `order_items`.`product_id` where
`order_items`.`order_id` = 2
SQL Queries That Scale: 1 x 44.50
Indexing Deep Dive: 1 x 29.00
Stack Sticker Pack: 3 x 4.99
-- select `products`.*, (select sum(`order_items`.`quantity`) from `orders` inner join
`order_items` on `orders`.`id` = `order_items`.`order_id` where `products`.`id` =
`order_items`.`product_id`) as `sold` from `products` order by `sold` desc limit 1
Semicolon Coffee Mug: 6Eloquent selects the pivot columns under pivot_ aliases, so they cannot clash with product columns, then moves them into the pivot object. wherePivot(), wherePivotIn(), wherePivotNull() and orderByPivot() filter and sort on them, and withPivotValue() filters and fills a column at once. The withSum() column names the pivot table because the aggregate runs against the join.