Discount Plugins
This guide explains how to build a custom CartThrob discount plugin using the CartThrob 9.x plugin API. It assumes basic PHP knowledge: classes, methods, arrays, and returning values.
CartThrob ships with several working discount plugins. Start by copying one of those and changing it, rather than writing everything from scratch.
Good starter examples:
Cartthrob_discount_percentage_off.phpfor a simple percent off the cart subtotalCartthrob_discount_amount_off.phpfor a flat amount offCartthrob_discount_percentage_off_product.phpwhen the discount applies only to specific productsCartthrob_discount_free_shipping.phpwhen the discount sets shipping to zero
Discount plugins return numeric amounts (float / int), not Money objects.
Discount plugins power both:
- Automatic discounts
- Coupon / voucher codes
Both are managed through ExpressionEngine channels and CartThrob discount settings. See the ExpressionEngine Docs if you need a refresher on channels, fields, and entries.
Start Here
A discount plugin is a PHP class that:
- Lives in CartThrob’s discount plugins folder
- Extends
CartThrob\Plugins\Discount\DiscountPlugin - Declares a
$title - Implements
get_discount()and returns the discount amount
At a minimum, CartThrob needs that get_discount() method so it can calculate the cart discount.
If the discount should only apply when the cart meets certain conditions, also:
implements CartThrob\Plugins\Discount\ValidateCartInterface- Add
validateCart(): bool - Call
set_error(...)when validation fails
File Location and Naming
Store discount plugins here:
system/user/addons/cartthrob/cartthrob/plugins/discount/
Rules:
- The filename and classname must match.
- The classname must begin with
Cartthrob_discount. - The file must begin with
Cartthrob_discount. - Do not start your own PHP session. CartThrob already manages session and cart state.
Example:
Filename: Cartthrob_discount_my_percent.php
Class: Cartthrob_discount_my_percent
Minimum Working Plugin
This is the smallest useful discount plugin: a percent off the cart subtotal.
<?php
use CartThrob\Math\Number;
use CartThrob\Plugins\Discount\DiscountPlugin;
if (!defined('CARTTHROB_PATH')) {
Cartthrob_core::core_error('No direct script access allowed');
}
class Cartthrob_discount_my_percent extends DiscountPlugin
{
public $title = 'My Percentage Off';
public $settings = [
[
'name' => 'Percentage off',
'short_name' => 'percentage_off',
'type' => 'text',
'default' => '10',
'note' => 'Enter 10 for 10%, not 0.10.',
],
];
public function get_discount()
{
return $this->core->cart->subtotal()
* abs(Number::sanitize($this->plugin_settings('percentage_off')) / 100);
}
}
What this does:
- Shows a Percentage off field in the discount/coupon field settings
- Reads that setting with
plugin_settings('percentage_off') - Converts
10into0.10 - Returns
subtotal * 0.10as the discount amount
After adding the file, create or edit a discount/coupon entry, choose this plugin type, save the settings, and test in the cart.
Plugin Information
These public properties describe the plugin in the control panel.
public $title = 'My Percentage Off';
public $note = 'Optional note shown with the plugin settings.';
$titleis required.$noteis 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 discount 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_discount_my_percent_lang.php
Example language entries:
<?php
$lang = [
'my_percentage_off_title' => 'My Percentage Off',
'percentage_off' => 'Percentage Off',
'percentage_off_note' => 'Enter 10 for 10%, not 0.10.',
];
Then reference those keys in the plugin:
public $title = 'my_percentage_off_title';
public $settings = [
[
'name' => 'percentage_off',
'note' => 'percentage_off_note',
'short_name' => 'percentage_off',
'type' => 'text',
],
];
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.
Inside the plugin, common CartThrob language lines are available through:
$this->core->lang('coupon_not_valid_for_items');
For ExpressionEngine language file conventions, see the EE language files docs.
Plugin Settings
Each plugin can define its own settings array. Every setting needs at least:
nametypeshort_name
Optional keys:
defaultoptionsnotesize
Setting Keys
name
Required. Label shown in the control panel. Use plain text or a language key.
type
Required. Supported types commonly used by discount plugins:
texttextarearadioselectcheckboxheader
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.
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. HTML is allowed.
Settings Example
public $settings = [
[
'name' => 'Percentage off',
'short_name' => 'percentage_off',
'type' => 'text',
'default' => '10',
'note' => 'Enter 10 for 10%.',
],
[
'name' => 'Product entry IDs',
'short_name' => 'entry_ids',
'type' => 'text',
'note' => 'Separate multiple entry IDs with commas or pipes.',
],
];
Read settings in your plugin like this:
$percent = $this->plugin_settings('percentage_off');
$entry_ids = $this->plugin_settings('entry_ids', '');
The second argument is the fallback if the setting is missing.
Global Discount Settings
DiscountPlugin already defines shared global settings used across discount/coupon entries:
used_byper_user_limitdiscount_limitmember_groupsmember_ids
You usually do not need to redefine those in your plugin. CartThrob applies them when validating coupons and discounts.
Your $settings array should focus on the calculation-specific fields for your plugin.
Required and Optional Methods
get_discount()
Required for every discount plugin. This is the only abstract method on DiscountPlugin.
Parameters: none
Returns: number (float or int) — the discount amount, not the discounted total
public function get_discount()
{
return $this->core->cart->subtotal()
* abs(Number::sanitize($this->plugin_settings('percentage_off')) / 100);
}
Important:
- Return the discount portion only. If the subtotal is
100and the discount is10%, return10, not90. - Return a number. Do not return a formatted currency string.
- Percent settings are usually entered like
10. Convert them with/ 100before multiplying. - Use
CartThrob\Math\Number::sanitize(...)to clean admin input before math.
Free-shipping style plugins may return 0 and change shipping instead:
public function get_discount()
{
$this->core->cart->set_shipping(0);
return 0;
}
validateCart(): bool
Optional, but needed when the discount should only apply if the cart qualifies.
Implement ValidateCartInterface and return true or false.
Parameters: none
Returns: boolean
use CartThrob\Plugins\Discount\ValidateCartInterface;
class Cartthrob_discount_my_product extends DiscountPlugin implements ValidateCartInterface
{
public function validateCart(): bool
{
// return true if the cart qualifies
// return false and set_error(...) if it does not
}
}
For coupon codes, a failed validation can show an error message.
For automatic discounts, failed validation usually means the discount is simply skipped.
initialize($plugin_settings = [], $defaults = [])
Optional.
Use this when you need to prepare plugin-wide values once, such as adjusting settings options dynamically.
Parameters: $plugin_settings = [], $defaults = []
Returns: usually $this
public function initialize($plugin_settings = [], $defaults = [])
{
parent::initialize($plugin_settings, $defaults);
// optional setup here
return $this;
}
set_error($error) / error()
Use set_error() inside validateCart() when a coupon should explain why it is invalid.
$this->set_error($this->core->lang('coupon_not_valid_for_items'));
toString($data)
Optional. Many bundled plugins implement this for Order Manager / human-readable discount summaries. It is not required for cart discount calculation.
How CartThrob Uses Discount Plugins
Understanding the call sites helps you write the right methods.
Coupons
When a coupon code is in the cart and valid, CartThrob:
- Loads the coupon entry data
- Creates the selected discount plugin
- Calls
get_discount() - Adds that amount into the cart discount total
If the plugin implements ValidateCartInterface, CartThrob also calls validateCart() while validating the coupon.
Automatic discounts
CartThrob also loops configured automatic discount entries, creates each plugin, and calls get_discount().
Discount vs shipping override
Most plugins return a money amount that reduces the order.
Some plugins, like free shipping, return 0 and call:
$this->core->cart->set_shipping(0);
You can also force a cart total in advanced cases:
$this->core->cart->set_total(0); // free order
Use those carefully. They override normal cart calculations.
Useful Cart Helpers
These are the helpers you will use most often inside discount plugins.
Plugin settings
$this->plugin_settings('percentage_off');
$this->plugin_settings('entry_ids', '');
Cart totals
$this->core->cart->subtotal();
$this->core->cart->shipping();
$this->core->cart->count();
Cart items
foreach ($this->core->cart->items() as $item) {
$item->quantity();
$item->price();
$item->product_id();
$item->weight();
}
Product-specific discounts often check $item->product_id() against a list of entry IDs.
Product IDs currently in the cart
$this->core->cart->product_ids();
Useful for validateCart() checks.
Customer info
$this->core->cart->customer_info();
$this->core->cart->customer_info('email_address');
Number cleanup
use CartThrob\Math\Number;
$clean = Number::sanitize($this->plugin_settings('amount_off'));
Prefer Number::sanitize() for settings input.
ExpressionEngine access
Use ee():
ee()->session->userdata('member_id');
Do not use $this->EE =& get_instance(); in new plugins.
If you are new to EE add-on development, start with the ExpressionEngine Docs and the Add-on Development Overview.
Validating the Cart
Use ValidateCartInterface when the discount depends on cart contents.
Example pattern from Percentage Off Product:
public function validateCart(): bool
{
$valid = false;
if ($this->plugin_settings('entry_ids') && $entry_ids = preg_split(
'#\s*[,|]\s*#',
trim($this->plugin_settings('entry_ids'))
)) {
$valid = count(array_intersect($this->core->cart->product_ids(), $entry_ids)) > 0;
}
if (!$valid) {
$this->set_error($this->core->lang('coupon_not_valid_for_items'));
}
return $valid;
}
Then in get_discount(), calculate only against the eligible items.
You do not need validateCart() for simple whole-cart discounts like percentage off or amount off.
Examples
Example 1: Percentage off subtotal
Same pattern as Cartthrob_discount_percentage_off.php.
<?php
use CartThrob\Math\Number;
use CartThrob\Plugins\Discount\DiscountPlugin;
if (!defined('CARTTHROB_PATH')) {
Cartthrob_core::core_error('No direct script access allowed');
}
class Cartthrob_discount_percentage_off extends DiscountPlugin
{
public $title = 'percentage_off';
public $settings = [
[
'name' => 'percentage_off',
'short_name' => 'percentage_off',
'note' => 'percentage_off_note',
'type' => 'text',
],
];
public function get_discount()
{
return $this->core->cart->subtotal()
* abs(Number::sanitize($this->plugin_settings('percentage_off')) / 100);
}
}
Example 2: Flat amount off
Same pattern as Cartthrob_discount_amount_off.php.
public function get_discount()
{
return abs(Number::sanitize($this->plugin_settings('amount_off')));
}
Example 3: Percentage off specific products
Use Cartthrob_discount_percentage_off_product.php as your model when you need:
ValidateCartInterface- Entry ID matching
- Per-item discount accumulation
Key pieces:
use CartThrob\Plugins\Discount\DiscountPlugin;
use CartThrob\Plugins\Discount\ValidateCartInterface;
class Cartthrob_discount_percentage_off_product extends DiscountPlugin implements ValidateCartInterface
{
public function get_discount()
{
// Loop cart items, discount matching product IDs, return total discount
}
public function validateCart(): bool
{
// Return true only if at least one eligible product is in the cart
}
}
Example 4: Free shipping
Same pattern as Cartthrob_discount_free_shipping.php.
public function get_discount()
{
$this->core->cart->set_shipping(0);
return 0;
}
Checklist
Before testing your plugin:
- File is in
system/user/addons/cartthrob/cartthrob/plugins/discount/ - Filename and classname match
- Classname starts with
Cartthrob_discount - Class extends
CartThrob\Plugins\Discount\DiscountPlugin get_discount()exists and returns a number- Percent settings are converted with
/ 100before multiplying - Settings use valid
name,type, andshort_namekeys - If the discount depends on cart contents, the class implements
ValidateCartInterfaceand providesvalidateCart() - A discount or coupon entry uses your plugin type and is saved
- Test with a qualifying cart, a non-qualifying cart, and coupon error messaging if relevant
If the plugin does not appear as a discount type, check the classname prefix and file location first. If the discount calculates as zero or looks like a final total instead of a discount amount, check that get_discount() returns the discount portion only and that percent values are decimals like 0.10.