php
59 lines · 7 steps
Calculating a cart total with a Money type
A cart calculator sums line items, applies discounts, shipping rules, and tax using precise integer-cent Money objects instead of floats.
Explained by
highlit
1<?php
2
3namespace App\Services;
4
5use App\Models\Cart;
6use Money\Money;
7
8final class CartTotalCalculator
9{
10 private const FREE_SHIPPING_THRESHOLD = 7500;
11 private const FLAT_SHIPPING = 995;
12 private const TAX_RATE = 0.0825;
13
14 public function calculate(Cart $cart): array
15 {
16 $subtotal = Money::USD(0);
17 $discountTotal = Money::USD(0);
18
19 foreach ($cart->lines as $line) {
20 $gross = Money::USD($line->unit_price)->multiply($line->quantity);
21 $discount = $this->lineDiscount($gross, $line->discount_percent);
22
23 $subtotal = $subtotal->add($gross);
24 $discountTotal = $discountTotal->add($discount);
25 }
26
27 $discountedSubtotal = $subtotal->subtract($discountTotal);
28 $shipping = $this->shipping($discountedSubtotal, $cart->requires_shipping);
29 $tax = $discountedSubtotal->multiply((string) self::TAX_RATE, Money::ROUND_HALF_UP);
30
31 return [
32 'subtotal' => $subtotal,
33 'discount' => $discountTotal,
34 'shipping' => $shipping,
35 'tax' => $tax,
36 'total' => $discountedSubtotal->add($shipping)->add($tax),
37 ];
38 }
39
40 private function lineDiscount(Money $gross, float $percent): Money
41 {
42 if ($percent <= 0) {
43 return Money::USD(0);
44 }
45
46 return $gross->multiply((string) ($percent / 100), Money::ROUND_HALF_UP);
47 }
48
49 private function shipping(Money $discountedSubtotal, bool $requiresShipping): Money
50 {
51 if (! $requiresShipping) {
52 return Money::USD(0);
53 }
54
55 return $discountedSubtotal->greaterThanOrEqual(Money::USD(self::FREE_SHIPPING_THRESHOLD))
56 ? Money::USD(0)
57 : Money::USD(self::FLAT_SHIPPING);
58 }
59}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Representing money as integer cents in a Money value object avoids floating-point errors that plague price arithmetic.
- 2Encoding business rules like free-shipping thresholds and tax rates as constants keeps pricing logic centralized and readable.
- 3Immutable value objects return new instances on every operation, so accumulation happens by reassignment rather than mutation.
Related explainers
php
<?php class NameParser {
Parsing a full name into components in PHP
string-parsing
arrays
normalization
Intermediate
8 steps
php
<?php namespace App\Services\Checkout;
Validating coupons with Laravel's Pipeline
pipeline
chain of responsibility
transactions
Intermediate
7 steps
php
<?php namespace App\Services;
How a password strength validator works in PHP
validation
regular-expressions
data-driven
Intermediate
8 steps
php
<?php namespace App\Services;
Building a cached daily leaderboard in Laravel
caching
aggregation
eager-loading
Intermediate
9 steps
go
package logging import ( "context"
Deduplicating log attributes in Go's slog
decorator-pattern
structured-logging
immutability
Intermediate
8 steps
php
<?php final class RotatingFileLogger {
How a rotating file logger works in PHP
logging
file-rotation
io
Intermediate
9 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/calculating-a-cart-total-with-a-money-type-explained-php-1445/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.