Install the Package
composer require ejosterberg/opensalestax
Requires PHP 8.2+.
Set Up the Engine
Deploy the OpenSalesTax engine (e.g., via Docker) and ensure it’s reachable at a baseUrl (e.g., http://localhost:8080).
First Use Case: Calculate Tax for a Transaction
use OpenSalesTax\Client;
use OpenSalesTax\Address;
use OpenSalesTax\LineItem;
$client = new Client(baseUrl: 'http://localhost:8080');
$result = $client->calculate(
address: new Address(zip5: '55401'),
lineItems: [
new LineItem(amount: '100.00', category: 'general'),
]
);
echo "Tax Total: {$result->taxTotal}"; // Output: "Tax Total: 8.025"
Client Class: Core entry point for all API calls.Address, LineItem, CalculateResponse): Data structures for requests/responses.OpenSalesTaxApiException or OpenSalesTaxNetworkException.Basic Tax Calculation
$client = new Client(baseUrl: 'http://your-engine:8080');
$result = $client->calculate(
address: new Address(zip5: '90210'),
lineItems: [new LineItem(amount: '50.00', category: 'clothing')]
);
result->taxTotal for customer-facing totals.result->lines for jurisdiction-level breakdowns (e.g., state/county splits).Shipping Tax (Engine v0.59.0+)
$result = $client->calculate(
address: new Address(zip5: '55401'),
lineItems: [new LineItem(amount: '100.00', category: 'general')],
shipping: new Shipping(amount: '12.50', method: 'standard')
);
$result->shipping for shipping tax details.$client->capabilities()->features->shippingFirstClass.Rate Lookup
$rates = $client->rates(zip5: '90210');
echo "Combined Rate: {$rates->combinedRatePct}%";
Health Checks
$health = $client->health();
if (!$health->databaseConnected) {
throw new RuntimeException("Engine database unavailable");
}
rates or calculate responses. The SDK does not include caching logic.try {
$result = $client->calculate(...);
} catch (OpenSalesTaxApiException $e) {
logError("Engine error: {$e->statusCode}");
} catch (OpenSalesTaxNetworkException $e) {
logError("Network failure: " . $e->getMessage());
}
"100.00") to avoid floating-point precision issues. Convert cents to strings manually:
$cents = 10000;
$amount = number_format($cents / 100, 2, '.', '');
capabilities() to gate features (e.g., shipping) based on engine version:
if ($client->capabilities()->features->shippingFirstClass) {
// Enable shipping tax logic
}
Bulk Calculations Loop over line items or addresses and batch requests if the engine supports it (not natively supported in this SDK).
Tax Category Mapping
Map your product categories to OpenSalesTax categories (e.g., general, clothing, groceries). Example:
$categoryMap = [
'electronics' => 'general',
'apparel' => 'clothing',
];
Jurisdiction Breakdowns
Use result->lines[*]->jurisdictions for accounting splits:
foreach ($result->lines as $line) {
foreach ($line->jurisdictions as $jurisdiction) {
echo "{$jurisdiction->type}: {$jurisdiction->tax}\n";
}
}
Testing
Client or HTTP client for isolated tests (e.g., with Mockery or PHPUnit’s createMock).Engine Version Mismatch
shipping field.ejosterberg/opensalestax: ^0.3
opensalestax engine: v0.59.0+
Decimal String Precision
"100.00").100.00 → 10000) loses precision. Always use number_format():
// Wrong:
$amount = 10000 / 100; // Float: 100.00000000000001
// Right:
$amount = number_format(10000 / 100, 2, '.', ''); // String: "100.00"
ZIP Code Validation
"123") throw OpenSalesTaxValidationException.try {
$result = $client->calculate(...);
} catch (OpenSalesTaxValidationException $e) {
echo "Invalid ZIP: {$e->getMessage()}";
}
Shipping Field Ignored in Older Engines
shipping field in calculate() is silently ignored.if (!$client->capabilities()->features->shippingFirstClass) {
throw new RuntimeException("Shipping tax requires engine v0.59.0+");
}
No Built-in Caching
$cacheKey = "tax_{$zip}_{$subtotal}";
$cachedResult = cache()->get($cacheKey);
if (!$cachedResult) {
$result = $client->calculate(...);
cache()->put($cacheKey, $result, now()->addHours(1));
}
Tax Category Quirks
note field in CalculateResponse for hints:
if ($line->note) {
echo "Note: {$line->note}"; // e.g., "Clothing is non-taxable in Minnesota..."
}
Network Timeouts
timeoutSeconds constructor argument:
$client = new Client(baseUrl: 'http://engine:8080', timeoutSeconds: 30.0);
$client = new Client(
baseUrl: 'http://engine:8080',
httpClient: new \GuzzleHttp\Client([
'handler' => \GuzzleHttp\HandlerStack::create([
new \GuzzleHttp\
How can I help you explore Laravel packages today?