spatie/tax-calculator
Interfaces and helpers to simplify tax calculations in PHP. Use TaxCalculation with plain numbers or items implementing HasTax to compute base, tax, and total prices, and combine calculations (e.g., cart items + delivery) on the fly.
Installation:
composer require spatie/tax-calculator
No additional configuration is required—just start using the package.
First Use Case:
HasTax on your model (e.g., CartItem):
use Spatie\TaxCalculator\HasTax;
class CartItem implements HasTax
{
public function getTaxablePrice(): float
{
return $this->price;
}
public function getTaxRate(): float
{
return 0.21; // 21% tax rate
}
}
$taxCalculation = TaxCalculation::fromCollection($cartItems);
$basePrice = $taxCalculation->basePrice(); // Sum of all pre-tax prices
$taxedPrice = $taxCalculation->taxedPrice(); // Sum + tax
Key Classes to Explore:
TaxCalculation: Core class for calculations.HasTax: Interface for taxable objects.TaxCalculationException: Handle edge cases (e.g., invalid tax rates).Cart/Checkout Calculations:
$subtotal = TaxCalculation::fromCollection($cartItems)->basePrice();
$taxAmount = TaxCalculation::fromCollection($cartItems)->taxPrice();
$total = $subtotal + $taxAmount;
$shippingTax = TaxCalculation::fromTaxedPrice($shippingCost, $taxRate);
$totalWithShipping = $taxCalculation->add($shippingTax)->taxedPrice();
Dynamic Tax Rates:
getTaxRate() per item (e.g., for exempt products):
public function getTaxRate(): float
{
return $this->isTaxExempt() ? 0.0 : 0.21;
}
Testing:
HasTax implementations or use TaxCalculation directly:
$this->assertEquals(12.10, TaxCalculation::fromTaxedPrice(10.00, 0.21)->taxedPrice());
TaxCalculation to a service container for dependency injection:
$this->app->bind(TaxCalculation::class, function () {
return new TaxCalculation();
});
return response()->json([
'subtotal' => $taxCalculation->basePrice(),
'tax' => $taxCalculation->taxPrice(),
'total' => $taxCalculation->taxedPrice(),
]);
tax_rate, tax_amount) alongside orders for reporting.Floating-Point Precision:
$taxedPrice = round($taxCalculation->taxedPrice(), 2);
bcmath or gmp for high-precision needs (not built into the package).Negative Tax Rates:
getTaxRate() returns >= 0:
public function getTaxRate(): float
{
return max(0.0, $this->rate); // Clamp to 0
}
Empty Collections:
fromCollection([]) returns 0 for all methods. Handle edge cases explicitly:
$price = $taxCalculation->basePrice() ?? 0.0;
Immutable Operations:
add() return new TaxCalculation instances. Chain operations carefully:
// Correct: Chained operations
$total = TaxCalculation::fromCollection($items)
->add($shipping)
->taxedPrice();
// Avoid: Overwriting variables
$calc = TaxCalculation::fromCollection($items);
$calc = $calc->add($shipping); // New instance!
$calc = TaxCalculation::fromCollection($items);
logger()->debug([
'base' => $calc->basePrice(),
'tax' => $calc->taxPrice(),
'items' => $items->map(fn ($item) => [
'price' => $item->getTaxablePrice(),
'rate' => $item->getTaxRate(),
]),
]);
HasTax Implementation:
Use instanceof to verify:
if (!$item instanceof HasTax) {
throw new \InvalidArgumentException('Item must implement HasTax');
}
TaxCalculation for complex logic (e.g., tiered taxes):
class CustomTaxCalculation extends TaxCalculation
{
public function applyTieredTax(float $threshold, float $rate): self
{
// Custom logic here
return $this;
}
}
class TaxRateProvider
{
public function getRateForItem($item): float
{
return $item->category === 'exempt' ? 0.0 : config('tax.default_rate');
}
}
Then update getTaxRate() to delegate:
public function getTaxRate(): float
{
return app(TaxRateProvider::class)->getRateForItem($this);
}
$formattedTax = number_format($taxCalculation->taxPrice(), 2, ',', ' ');
How can I help you explore Laravel packages today?