CartThrob 9 documentation for ExpressionEngine commerce teams Visit CartThrob.com
CartThrob.com

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.php for a single stored number
  • ft.cartthrob_price_quantity_thresholds.php when price depends on cart quantity
  • ft.cartthrob_price_by_member_group.php when 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:

  1. Lives where EE fieldtypes live, usually in an add-on folder as ft.your_field.php
  2. Extends EE_Fieldtype or another EE fieldtype base class
  3. Can store and display product price data like any other fieldtype
  4. 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:

  1. Add the field to your products channel
  2. In CartThrob Products settings, map that field as the channel Price field
  3. 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 0 in 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 a Cartthrob_item when 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:

  1. Fieldtype file and classname follow EE naming rules
  2. Fieldtype is installed and assigned to the products channel
  3. Field is mapped as the CartThrob product channel Price field
  4. Global Price is empty unless you intentionally want it to override everything
  5. If stored data is not a plain number, cartthrob_price() exists
  6. cartthrob_price() always accepts $item = null
  7. cartthrob_price() returns a number, not a formatted currency string
  8. Any use of $item is guarded with instanceof Cartthrob_item
  9. 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.

ExpressionEngine References