A pivot row with behavior deserves a class. It extends Relations\Pivot (MorphPivot for polymorphic pivots), both definitions add ->using(OrderItem::class), and accessors, casts and model events then work on pivot rows:
class OrderItem extends Pivot
{
protected $table = 'order_items';
protected function lineTotal(): Attribute
{
return Attribute::get(fn () => bcmul($this->unit_price, (string) $this->quantity, 2));
}
protected static function booted(): void
{
static::creating(fn (OrderItem $item) =>
$item->unit_price ??= Product::findOrFail($item->product_id)->price);
}
}use App\Models\Order;
$order = Order::find(3);
$order->products()->attach(7, ['quantity' => 2]);
foreach ($order->products()->get() as $p) {
echo "{$p->sku}: {$p->pivot->quantity} x {$p->pivot->unit_price} = ",
$p->pivot->line_total, "\n";
}Output
-- select * from `orders` where `orders`.`id` = 3 limit 1
-- select * from `products` where `products`.`id` = 7 limit 1
-- insert into `order_items` (`product_id`, `order_id`, `quantity`, `unit_price`) values (7, 3,
2, '12.50')
...
BK-LAR-01: 1 x 49.00 = 49.00
AC-MUG-01: 2 x 12.50 = 25.00With using(), attach() saves each row through the class, so creating fired and froze the mug's current price into the order. $table is needed because a pivot class guesses a singular name. Pivots are non-incrementing; one with its own id declares #[Table(incrementing: true)]. bcmul() keeps money in decimal strings (BCMath).