Tax Plugins
This guide explains how to build a custom CartThrob tax plugin using the CartThrob 9.x plugin API. It assumes basic PHP knowledge: classes, methods, arrays, and returning values.
If you need ExpressionEngine add-on basics first, see the ExpressionEngine Docs and the Add-on Development Overview.
CartThrob ships with several working tax plugins. Start by copying one of those and changing it, rather than writing everything from scratch.
Good starter examples:
Cartthrob_tax_standard.phpfor location-based rates plus a default percentCartthrob_tax_default.phpfor a settings matrix of location ratesCartthrob_tax_by_class.phpfor advanced per-item tax classes
Unlike shipping plugins, tax plugins return numeric amounts (float / int), not Money objects.
Start Here
A tax plugin is a PHP class that:
- Lives in CartThrob’s tax plugins folder
- Extends
CartThrob\Plugins\Tax\TaxPlugin - Declares a
$title - Implements
get_tax($price)and returns the tax amount for that price
At a minimum, CartThrob needs that get_tax($price) method so it can calculate tax on items, and sometimes on shipping or discounts.
In practice you should also add:
tax_rate()so CartThrob can display and reuse the current ratetax_name()for the tax labeltax_shipping()so CartThrob knows whether shipping is taxable
File Location and Naming
Store tax plugins here:
system/user/addons/cartthrob/cartthrob/plugins/tax/
Rules:
- The filename and classname must match.
- The classname must begin with
Cartthrob_tax. - The file must begin with
Cartthrob_tax. - Do not start your own PHP session. CartThrob already manages session and cart state.
Example:
Filename: Cartthrob_tax_my_rate.php
Class: Cartthrob_tax_my_rate
Minimum Working Plugin
This is the smallest useful tax plugin: one percent rate from a settings field.
<?php
use CartThrob\Math\Number;
use CartThrob\Plugins\Tax\TaxPlugin;
if (!defined('CARTTHROB_PATH')) {
Cartthrob_core::core_error('No direct script access allowed');
}
class Cartthrob_tax_my_rate extends TaxPlugin
{
public $title = 'My Flat Tax';
public $settings = [
[
'name' => 'Tax percent',
'short_name' => 'default_tax',
'type' => 'text',
'default' => '8',
'note' => 'Enter 8 for 8%, not 0.08.',
],
];
protected array $rules = [
'default_tax' => 'required|numeric',
];
public function get_tax($price)
{
return $price * $this->tax_rate();
}
public function tax_rate()
{
return abs(Number::sanitize($this->plugin_settings('default_tax')) / 100);
}
public function tax_name()
{
return 'Sales Tax';
}
public function tax_shipping()
{
return false;
}
}
What this does:
- Shows a Tax percent field in CartThrob tax settings
- Reads that setting with
plugin_settings('default_tax') - Converts
8into a rate of0.08 - Returns
$price * 0.08as the tax amount
After adding the file, select the plugin in CartThrob’s tax settings and save.
Plugin Information
These public properties describe the plugin in the control panel.
public $title = 'My Flat Tax';
public $note = 'Optional note shown with the plugin settings.';
public $overview = 'Optional longer overview text.';
$titleis required.$noteand$overvieware optional.- You can use plain text, or a language key that CartThrob will look up.
Language Files
Language keys keep settings labels cleaner and make text easier to reuse.
Built-in CartThrob tax plugins store many labels in:
system/user/addons/cartthrob/language/english/cartthrob_lang.php
You can also place a plugin-specific language file next to the plugin, named like:
cartthrob_tax_my_rate_lang.php
Example language entries:
<?php
$lang = [
'my_flat_tax_title' => 'My Flat Tax',
'my_flat_tax_note' => 'Charges one percent rate for the whole order.',
'default_tax' => 'Tax Percent',
'default_tax_note' => 'Enter 8 for 8%, not 0.08.',
];
Then reference those keys in the plugin:
public $title = 'my_flat_tax_title';
public $note = 'my_flat_tax_note';
public $settings = [
[
'name' => 'default_tax',
'note' => 'default_tax_note',
'short_name' => 'default_tax',
'type' => 'text',
'default' => '8',
],
];
If CartThrob finds the language key, it shows the translated text. If not, it shows the key itself.
A separate language file is optional for a basic plugin. Plain-text $title and setting labels work fine while you are learning.
Plugin Settings
Each plugin can define its own settings array. Every setting needs at least:
nametypeshort_name
Optional keys:
defaultoptionsnotesettingsfor matrix rowsattributesfor extra HTML attributes on the field
Setting Keys
name
Required. Label shown in the control panel. Use plain text or a language key.
type
Required. Supported types:
texttextarearadioselectcheckboxmatrixheader
text, textarea, and header store string-like values. The others usually store arrays or selected option values.
Unchecked checkboxes typically return no value.
short_name
Required. The key you use in PHP with plugin_settings('short_name').
Use letters, numbers, underscores, and hyphens only.
default
Optional. Default value for the setting.
Matrix fields can have defaults on individual columns. You generally cannot preset a full matrix row with one top-level default.
options
Usually required for radio, select, and checkbox.
'options' => [
'yes' => 'Yes',
'no' => 'No',
],
The array key is the stored value. The array value is the label shown to the admin.
note
Optional help text shown with the setting.
settings
Required for matrix settings. Contains the column definitions for each matrix row.
CartThrob tax plugins do not support a matrix inside another matrix.
Settings Example
public $settings = [
[
'name' => 'Default tax',
'note' => 'Used when no location match is found. Enter 8 for 8%.',
'short_name' => 'default_tax',
'type' => 'text',
'default' => '8',
],
[
'name' => 'Tax by location',
'short_name' => 'tax_settings',
'type' => 'matrix',
'settings' => [
[
'name' => 'Name',
'short_name' => 'name',
'type' => 'text',
],
[
'name' => 'Tax percent',
'short_name' => 'rate',
'type' => 'text',
],
[
'name' => 'State / Country',
'short_name' => 'state',
'type' => 'select',
'attributes' => [
'class' => 'states_and_countries',
],
'options' => [],
],
[
'name' => 'Zip / Region',
'short_name' => 'zip',
'type' => 'text',
],
[
'name' => 'Tax shipping',
'short_name' => 'tax_shipping',
'type' => 'checkbox',
],
],
],
];
Read settings in your plugin like this:
$default_tax = $this->plugin_settings('default_tax');
$tax_settings = $this->plugin_settings('tax_settings', []);
Required and Expected Methods
get_tax($price)
Required for every tax plugin. This is the only abstract method on TaxPlugin.
Parameters:
$price(required): the amount being taxed- A second argument is often passed by CartThrob, even though the abstract signature only shows
$price:- an item object for item tax
'shipping'for shipping tax'discount'for discount tax
Returns: number (float or int) — the tax amount for that price, not the price plus tax
public function get_tax($price)
{
return $price * $this->tax_rate();
}
Important:
- Return the tax portion only. If
$priceis100and the rate is8%, return8, not108. - Return a number. Do not return a
Moneyobject. - Settings are usually entered as percents like
8. Convert them with/ 100before multiplying. - Use
CartThrob\Math\Number::sanitize(...)to clean admin input before math.
If you need the optional second argument:
public function get_tax($price, $context = null)
{
// $context may be an item object, 'shipping', or 'discount'
return $price * $this->tax_rate();
}
tax_rate()
Expected. CartThrob calls this for display and rate lookups.
Parameters: none
Returns: float rate as a decimal, such as 0.08 for 8%
public function tax_rate()
{
return abs(Number::sanitize($this->plugin_settings('default_tax')) / 100);
}
tax_name()
Expected. Used for tax labels in the cart and templates.
Parameters: none
Returns: string
public function tax_name()
{
return 'Sales Tax';
}
tax_shipping()
Expected. CartThrob checks this before taxing shipping.
Parameters: none
Returns: boolean
public function tax_shipping()
{
return false;
}
If this returns true, CartThrob calls get_tax($shipping_amount, 'shipping').
initialize()
Optional.
Use this when you want to prepare plugin-wide values once.
Parameters: $params = [], $defaults = []
Returns: usually $this or void
protected $default_rate = 0;
public function initialize($params = [], $defaults = [])
{
if ($this->plugin_settings('default_tax')) {
$this->default_rate = Number::sanitize($this->plugin_settings('default_tax')) / 100;
}
return $this;
}
tax_data()
Optional helper pattern used by the included location plugins. Not required by the base class.
Use it when you want one place to resolve the current tax row (name, percent, shipping taxable flag) and then read keys from it:
$this->tax_data('percent');
$this->tax_data('tax_name');
$this->tax_data('shipping_is_taxable');
See Cartthrob_tax_standard.php and Cartthrob_tax_default.php for full examples.
How CartThrob Calls Your Plugin
Understanding the call sites helps you write the right methods.
Item tax
For each taxable item, CartThrob roughly does:
$tax = $plugin->get_tax($cost, $item);
Your plugin should return the tax for that item cost.
Shipping tax
If your plugin’s tax_shipping() returns true:
$amount = $plugin->get_tax($shipping, 'shipping');
Discount tax
When discounts are taxable, CartThrob may call:
$amount = $plugin->get_tax($discount, 'discount');
Rate and name display
Store and template variables use:
$plugin->tax_rate();
$plugin->tax_name();
So even though only get_tax($price) is abstract, a useful plugin almost always implements the companion methods too.
Useful Cart Helpers
These are the helpers you will use most often inside tax plugins.
Taxable cart total
$this->core->cart->taxable_subtotal();
Returns the total of taxable items in the cart.
Customer address data
$this->core->cart->customer_info();
$this->core->cart->customer_info('zip');
$this->core->cart->customer_info('state');
$this->core->cart->customer_info('country_code');
$this->core->cart->customer_info('region');
If CartThrob is configured to tax based on the shipping address:
$prefix = $this->core->store->config('tax_use_shipping_address') ? 'shipping_' : '';
$zip = $this->core->cart->customer_info($prefix . 'zip');
$state = $this->core->cart->customer_info($prefix . 'state');
$country = $this->core->cart->customer_info($prefix . 'country_code');
Plugin settings
$this->plugin_settings('default_tax');
$this->plugin_settings('tax_settings', []);
The second argument is the fallback if the setting is missing.
Tax rates from the database
If rates are stored in CartThrob’s tax table rather than only in plugin settings:
$locations = [
'zip' => $this->core->cart->customer_info($prefix . 'zip'),
'special' => $this->core->cart->customer_info($prefix . 'region'),
'state' => $this->core->cart->customer_info($prefix . 'state'),
'country' => $this->core->cart->customer_info($prefix . 'country_code'),
];
$tax_settings = $this->core->get_tax_rates($locations);
Cartthrob_tax_standard.php is the best reference for this pattern.
Cart items
foreach ($this->core->cart->items() as $item) {
$item->quantity();
$item->price();
$item->product_id();
}
Rounding and number cleanup
use CartThrob\Math\Number;
$clean = Number::sanitize($this->plugin_settings('default_tax'));
$rounded = $this->core->round('11.203'); // 11.20
Prefer Number::sanitize() for settings input. Use $this->core->round() when you need money-style rounding inside the plugin.
ExpressionEngine helpers are available through ee(), for example:
ee()->lang->line('some_key');
Do not use $this->EE =& get_instance(); in new plugins.
Location-Based Rates
Most real tax plugins need more than one flat percent. The usual pattern is:
- Read the customer location from billing or shipping fields
- Find a matching rate row
- Fall back to a default percent
- Expose that row through
tax_rate(),tax_name(), andtax_shipping()
Which included plugin to copy
| Plugin | Best for |
|---|---|
Cartthrob_tax_standard.php |
Default percent + tax table / location lookup |
Cartthrob_tax_default.php |
Admin-managed matrix of location rates in plugin settings |
Cartthrob_tax_default_plus_quebec.php |
Special regional handling on top of the default pattern |
Cartthrob_tax_by_class.php |
Different rates by product/tax class |
For a first custom plugin, copy standard if you want a default percent and location table. Copy default if you want admins to manage rows in a settings matrix.
Rate format reminder
Admins usually enter 8 meaning 8 percent. Your code should convert that:
return abs(Number::sanitize($percent) / 100); // 0.08
Then calculate tax as:
return $price * $this->tax_rate();
Validation Rules
Many current tax plugins validate settings with a $rules array:
protected array $rules = [
'default_tax' => 'required|numeric',
];
This helps catch empty or non-numeric admin settings before they cause bad tax calculations.
Examples
Example 1: Flat percent rate
A beginner-friendly plugin with one setting.
<?php
use CartThrob\Math\Number;
use CartThrob\Plugins\Tax\TaxPlugin;
if (!defined('CARTTHROB_PATH')) {
Cartthrob_core::core_error('No direct script access allowed');
}
class Cartthrob_tax_my_rate extends TaxPlugin
{
public $title = 'My Flat Tax';
public $settings = [
[
'name' => 'Tax percent',
'short_name' => 'default_tax',
'type' => 'text',
'default' => '8',
],
];
protected array $rules = [
'default_tax' => 'required|numeric',
];
public function get_tax($price)
{
return $price * $this->tax_rate();
}
public function tax_rate()
{
return abs(Number::sanitize($this->plugin_settings('default_tax')) / 100);
}
public function tax_name()
{
return 'Sales Tax';
}
public function tax_shipping()
{
return true;
}
}
Example 2: Same calculation pattern as Standard
Cartthrob_tax_standard.php keeps the math tiny and puts the hard work in tax_data():
public function get_tax($price)
{
return $price * $this->tax_rate();
}
public function tax_rate()
{
return abs(Number::sanitize($this->tax_data('percent')) / 100);
}
public function tax_name()
{
return $this->tax_data('tax_name');
}
public function tax_shipping()
{
return (bool) $this->tax_data('shipping_is_taxable');
}
Copy that file when you need location matching and a fallback default percent.
Example 3: Settings matrix of locations
Use Cartthrob_tax_default.php when admins should manage name / percent / state / zip / tax-shipping rows in the plugin settings matrix.
That plugin:
- Reads
plugin_settings('tax_settings') - Matches the customer location
- Maps matrix fields onto
percent,tax_name, andshipping_is_taxable - Uses the same
$price * tax_rate()calculation
Checklist
Before testing your plugin:
- File is in
system/user/addons/cartthrob/cartthrob/plugins/tax/ - Filename and classname match
- Classname starts with
Cartthrob_tax - Class extends
CartThrob\Plugins\Tax\TaxPlugin get_tax($price)exists and returns a numbertax_rate(),tax_name(), andtax_shipping()are implemented- Percent settings are converted with
/ 100before multiplying - Settings use valid
name,type, andshort_namekeys - Plugin is selected and saved in CartThrob tax settings
- Test with a taxable item, a non-taxable item if you support that, and shipping tax on/off
If something does not appear in the control panel, check the classname prefix and file location first. If tax calculates as zero or looks like a full price instead of a tax amount, check that get_tax() returns $price * rate and that the rate is a decimal like 0.08.