Behaviour that belongs to a set rather than to a single record — the total of a group of order lines, whether any of them is backordered — ends up as a helper function or a static method, because there is nowhere obvious to put it.
class OrderLineCollection extends Collection
{
public function total(): Money
{
return $this->reduce(function ($carry, $line) {
return $carry->add($line->subtotal());
}, Money::zero());
}
}
class OrderLine extends Model
{
public function newCollection(array $models = [])
{
return new OrderLineCollection($models);
}
}
Every query returning these models now returns the richer collection, including through relations, so $order->lines->total() works without anything at the call site knowing. The restraint required is to keep it to set behaviour: a method that only ever touches one element belongs on the model, and a collection class accumulating those becomes the same junk drawer it was meant to replace.