Price Fieldtype API
This guide explains how to make a custom ExpressionEngine fieldtype work as a CartThrob product price field. It assumes basic PHP knowledge and familiarity with ExpressionEngine fieldtypes.
If you need the ExpressionEngine side of fieldtype development first, start here:
CartThrob already ships with working price fieldtypes. Start by copying one of those and changing it, rather than writing everything from scratch.
Good starter examples:
ft.cartthrob_price_simple.phpfor a single stored numberft.cartthrob_price_quantity_thresholds.phpwhen price depends on cart quantityft.cartthrob_price_by_member_group.phpwhen price depends on the member role
This is not the same as tax or shipping plugins. You do not put a class in cartthrob/plugins/. You build a normal EE fieldtype and add one CartThrob method to it.
Start Here
A CartThrob price fieldtype is an ExpressionEngine fieldtype that:
- Lives where EE fieldtypes live, usually in an add-on folder as
ft.your_field.php - Extends
EE_Fieldtypeor another EE fieldtype base class - Can store and display product price data like any other fieldtype
- Optionally implements
cartthrob_price($data, $item = null)so CartThrob can resolve complex stored data into one number
If your field already stores a plain numeric price, CartThrob can often use the raw field value and you do not need cartthrob_price().
If your field stores rows, thresholds, serialized data, or anything that is not already the final price, implement cartthrob_price() and return a number.
Then map that field as the product channel Price field in CartThrob settings.
How This Differs From Tax and Shipping Plugins
| Tax / Shipping plugins | Price fieldtypes |
|---|---|
Live in cartthrob/plugins/tax/ or shipping/ |
Live as normal EE fieldtypes |
Extend TaxPlugin / ShippingPlugin |
Extend EE_Fieldtype or similar |
| Selected in CartThrob tax/shipping settings | Mapped as the product channel Price field |
| Calculate tax or shipping for the cart | Resolve one product’s price |
There is a legacy CartThrob\Plugins\Price\PricePlugin class and a plugins/price/ path in CartThrob, but the practical price API for custom product pricing is the fieldtype method documented here.
File Location and Naming
Create a normal ExpressionEngine fieldtype. The EE CLI is the easiest way to scaffold one:
php system/ee/eecli.php make:addon
php system/ee/eecli.php make:fieldtype
make:addon creates the add-on. make:fieldtype adds an ft.*.php file to that add-on. See the Adding Fieldtypes docs and the make:addon CLI command for details.
For a third-party add-on, the fieldtype usually looks like:
system/user/addons/your_addon/ft.your_price_field.php
Class naming follows EE conventions:
Filename: ft.your_price_field.php
Class: Your_price_field_ft
CartThrob’s bundled examples live in the CartThrob add-on root:
system/user/addons/cartthrob/ft.cartthrob_price_simple.php
system/user/addons/cartthrob/ft.cartthrob_price_quantity_thresholds.php
system/user/addons/cartthrob/ft.cartthrob_price_by_member_group.php
After installing the fieldtype:
- Add the field to your products channel
- In CartThrob Products settings, map that field as the channel Price field
- Add a product to the cart and confirm the calculated price
Minimum Working Fieldtype Method
This is the smallest CartThrob-specific piece: return a numeric price from the field data.
/**
* @param mixed $data The field data from the product entry
* @param Cartthrob_item|null $item The cart item when CartThrob is pricing a cart row
* @return float|int
*/
public function cartthrob_price($data, $item = null)
{
return (float) $data;
}
What this does:
- Accepts the stored field value as
$data - Optionally receives the current cart item as
$item - Returns one number CartThrob can use as the product price
Important:
- Return the price only, not a formatted string like
$10.00 - Return a number. Non-numeric returns become
0in the cart pricing path - Always accept
$item = null, because some call sites pass only$data
When You Need cartthrob_price()
You usually do not need it when
- The field stores one plain number
- You are building something like Price - Simple
Cartthrob_price_simple_ft does not implement cartthrob_price(). CartThrob can use the stored numeric value directly.
You do need it when
- The field stores multiple rows and must choose one price
- Price depends on quantity, member role, package contents, or other runtime context
- Stored data is serialized or otherwise not already a final number
Best references:
| Fieldtype | Why copy it |
|---|---|
ft.cartthrob_price_quantity_thresholds.php |
Uses $item->quantity() |
ft.cartthrob_price_by_member_group.php |
Resolves complex stored rows to one price |
ft.cartthrob_package.php |
Advanced package pricing |
ft.cartthrob_price_simple.php |
Shows a price field without cartthrob_price() |
Price Modifiers and Price Modifiers Configurator are related product option fieldtypes. They adjust option prices; they are not the main channel Price field API documented here.
Method Reference
cartthrob_price($data, $item = null)
Optional for simple numeric fields. Required for complex price fieldtypes.
Parameters:
$data: the product entry’s field data for the mapped Price field$item: usually aCartthrob_itemwhen CartThrob is calculating cart item price; may be omitted for base-price lookups
Returns: float|int — the resolved product price
public function cartthrob_price($data, $item = null)
{
// Resolve $data into one number and return it
return 19.99;
}
CartThrob checks for this method through EE’s channel fields API:
ee()->api_channel_fields->check_method_exists('cartthrob_price');
ee()->api_channel_fields->apply('cartthrob_price', [$field_data, $item]);
If the method exists and returns a non-numeric value, cart pricing treats the price as 0.
How CartThrob Calls Your Fieldtype
Understanding the call sites helps you write the right method.
Cart item pricing
When a product item needs a price, CartThrob runs the product_price hook path and may call:
$price = ee()->api_channel_fields->apply(
'cartthrob_price',
[$data['field_id_' . $field_id], $item]
);
That is the path where $item is available, so quantity-based pricing can work.
Base price lookup
product_model->get_base_price() may call:
return $this->api_channel_fields->apply(
'cartthrob_price',
[$data['field_id_' . $field_id]]
);
Here $item is not passed. Your method should still work and return a sensible default price.
Global price short-circuit
If the product channel has a Global Price set in CartThrob Products settings, CartThrob returns that value and does not call your fieldtype method.
If your custom pricing never seems to run, check Global Price first.
Fallback without cartthrob_price()
If the mapped Price fieldtype does not implement cartthrob_price(), CartThrob falls back to using the stored field value as a normal product price field.
Using the Cart Item
When $item is present, it is a Cartthrob_item object. Common uses:
if ($item instanceof Cartthrob_item) {
$quantity = $item->quantity();
$options = $item->item_options();
$product_id = $item->product_id();
}
Quantity thresholds use that pattern:
public function cartthrob_price($data, $item = null)
{
if (!is_array($data)) {
$data = _unserialize($data, true);
}
foreach ($data as $row) {
if (
$item instanceof Cartthrob_item
&& $item->quantity() >= $row['from_quantity']
&& $item->quantity() <= $row['up_to_quantity']
) {
return $row['price'];
}
}
return 0;
}
Always guard with instanceof Cartthrob_item before calling item methods.
Useful Helpers
Field data shape
$data depends on your fieldtype:
- Simple fields: usually a string or number
- Matrix-style CartThrob price fields: often an array of rows, or a serialized string that must be unserialized first
Bundled complex fieldtypes commonly do:
if (!is_array($data)) {
$data = _unserialize($data, true);
}
Entry row data
In the cart pricing path, CartThrob may set the fieldtype’s $this->row to the product entry data before calling cartthrob_price(). That can be useful if you need channel or entry context.
Number cleanup
Prefer clean numeric returns:
use CartThrob\Math\Number;
return abs(Number::sanitize($price));
ExpressionEngine access
Use ee():
$role_id = ee()->session->userdata('role_id');
Do not use $this->EE =& get_instance(); in new fieldtypes.
Optional Template Methods
These are not required for cart pricing. Bundled price fieldtypes often add them so templates can display formatted prices.
Common patterns:
public function replace_tag($data, $params = '', $tagdata = '')
{
// Return a formatted price for {your_price_field}
}
public function replace_numeric($data, $params = '', $tagdata = '')
{
// Return the raw number for {your_price_field:numeric}
}
public function replace_plus_tax($data, $params = '', $tagdata = '')
{
// Return price + tax for {your_price_field:plus_tax}
}
Cart pricing still depends on cartthrob_price() or the raw stored number. Template replace_* methods only affect front-end display tags.
Examples
Example 1: Return the stored number
Useful when your field stores one price value and you still want an explicit CartThrob method.
public function cartthrob_price($data, $item = null)
{
if ($data === '' || $data === null) {
return 0;
}
return (float) $data;
}
Example 2: Quantity-based price
Same idea as Cartthrob_price_quantity_thresholds_ft.
public function cartthrob_price($data, $item = null)
{
if (!is_array($data)) {
$data = _unserialize($data, true);
}
if (!is_array($data)) {
return 0;
}
foreach ($data as $row) {
$from = $row['from_quantity'] ?? 0;
$to = $row['up_to_quantity'] ?? 0;
$price = $row['price'] ?? 0;
if (
$item instanceof Cartthrob_item
&& $item->quantity() >= $from
&& $item->quantity() <= $to
) {
return $price;
}
}
// Fall back to the last configured row if present
$last = end($data);
return is_array($last) && isset($last['price']) ? $last['price'] : 0;
}
Example 3: Member role price
Same idea as Cartthrob_price_by_member_group_ft.
public function cartthrob_price($data, $item = null)
{
if (!is_array($data)) {
$data = _unserialize($data, true);
}
$price = null;
$default_price = null;
$role_id = ee()->session->userdata('role_id');
foreach ($data as $row) {
if (is_null($price) && !empty($row['member_group']) && $role_id == $row['member_group']) {
$price = $row['price'];
}
if (is_null($default_price) && empty($row['member_group'])) {
$default_price = $row['price'];
}
}
if (is_null($price) && !is_null($default_price)) {
$price = $default_price;
}
return is_null($price) ? 0 : $price;
}
Checklist
Before testing your fieldtype:
- Fieldtype file and classname follow EE naming rules
- Fieldtype is installed and assigned to the products channel
- Field is mapped as the CartThrob product channel Price field
- Global Price is empty unless you intentionally want it to override everything
- If stored data is not a plain number,
cartthrob_price()exists cartthrob_price()always accepts$item = nullcartthrob_price()returns a number, not a formatted currency string- Any use of
$itemis guarded withinstanceof Cartthrob_item - Test with a product in the cart, including quantity changes if your logic depends on quantity
If the field never appears as a Price option, check that the fieldtype is installed on the products channel. If cart price is always 0, check for a non-numeric return from cartthrob_price(). If your custom logic never runs, check Global Price and confirm the field is mapped as the channel Price field.