Creating Custom Product Types in WooCommerce
WooCommerce's extensible product system allows you to create custom product types that go beyond simple, variable, and grouped products. This guide explores how to develop custom product types for unique business requirements, from subscription boxes to customizable products. Learn the complete development process, including data handling, pricing logic, and admin interface customization.
Whether you're building rental products, bookable services, or complex configurators, custom product types provide the flexibility to model any business logic. We'll cover the technical implementation details and best practices that ensure your custom products integrate seamlessly with WooCommerce.
Table of Contents
- Understanding Product Type Architecture
- Creating Your First Custom Product Type
- Advanced Product Features
- Admin Interface Customization
- Frontend Display and Cart Handling
- Real-World Examples
Understanding Product Type Architecture
WooCommerce Product Class Hierarchy
// Understanding the product class structure
abstract class WC_Product {
protected $id = 0;
protected $data = [
'name' => '',
'slug' => '',
'status' => 'publish',
'featured' => false,
'catalog_visibility' => 'visible',
'description' => '',
'short_description' => '',
'sku' => '',
'price' => '',
'regular_price' => '',
'sale_price' => '',
'tax_status' => 'taxable',
'tax_class' => '',
'manage_stock' => false,
'stock_quantity' => null,
'stock_status' => 'instock',
'backorders' => 'no',
'low_stock_amount' => '',
'sold_individually' => false,
'weight' => '',
'length' => '',
'width' => '',
'height' => '',
'parent_id' => 0,
'reviews_allowed' => true,
'purchase_note' => '',
'attributes' => [],
'default_attributes' => [],
'menu_order' => 0,
'virtual' => false,
'downloadable' => false,
];
/**
* Get product type
*/
public function get_type() {
return 'simple';
}
/**
* Product type specific features
*/
public function supports($feature) {
return in_array($feature, $this->get_supports());
}
/**
* Get supported features
*/
protected function get_supports() {
return [];
}
}
// Custom product type base class
abstract class WC_Product_Custom_Base extends WC_Product {
/**
* Custom data fields
*/
protected function get_custom_data() {
return [
'custom_field_1' => '',
'custom_field_2' => '',
'pricing_type' => 'fixed',
'booking_duration' => 0,
'rental_period' => 'day',
];
}
/**
* Initialize custom product
*/
public function __construct($product = 0) {
// Merge custom data
$this->data = array_merge($this->data, $this->get_custom_data());
// Set default supports
parent::__construct($product);
}
/**
* Custom validation
*/
public function validate_props() {
parent::validate_props();
// Add custom validation logic
if ($this->get_pricing_type() === 'dynamic' && !$this->get_price_calculator()) {
$this->error('dynamic_pricing_requires_calculator',
__('Dynamic pricing requires a price calculator', 'woocommerce'));
}
}
}
Creating Your First Custom Product Type
Rental Product Type Example
// Complete rental product implementation
class WC_Product_Rental extends WC_Product {
/**
* Product type name
*/
protected $product_type = 'rental';
/**
* Get product type
*/
public function get_type() {
return 'rental';
}
/**
* Get supported features
*/
protected function get_supports() {
return [
'ajax_add_to_cart',
'shipping',
'stock',
];
}
/**
* Get rental price per period
*/
public function get_rental_price($period = 1) {
$base_price = $this->get_price();
$price_per_period = $this->get_meta('_rental_price_per_period');
if ($price_per_period) {
return $price_per_period * $period;
}
return $base_price * $period;
}
/**
* Get rental periods
*/
public function get_rental_periods() {
return [
'hour' => __('Hour', 'woocommerce'),
'day' => __('Day', 'woocommerce'),
'week' => __('Week', 'woocommerce'),
'month' => __('Month', 'woocommerce'),
];
}
/**
* Get minimum rental period
*/
public function get_minimum_rental_period() {
return $this->get_meta('_minimum_rental_period') ?: 1;
}
/**
* Get maximum rental period
*/
public function get_maximum_rental_period() {
return $this->get_meta('_maximum_rental_period') ?: 365;
}
/**
* Check availability for date range
*/
public function is_available_for_dates($start_date, $end_date) {
global $wpdb;
// Check existing bookings
$bookings = $wpdb->get_results($wpdb->prepare("
SELECT * FROM {$wpdb->prefix}wc_rental_bookings
WHERE product_id = %d
AND status IN ('confirmed', 'pending')
AND (
(start_date <= %s AND end_date >= %s)
OR (start_date <= %s AND end_date >= %s)
OR (start_date >= %s AND end_date <= %s)
)
", $this->get_id(), $start_date, $start_date, $end_date, $end_date, $start_date, $end_date));
if (!empty($bookings)) {
// Check if stock allows multiple rentals
$stock = $this->get_stock_quantity();
if ($stock !== null && count($bookings) >= $stock) {
return false;
}
}
return true;
}
/**
* Add to cart validation
*/
public function validate_add_to_cart($passed, $product_id, $quantity, $variation_id = 0, $variations = []) {
if (!$passed) {
return false;
}
// Check if rental dates are provided
if (empty($_REQUEST['rental_start_date']) || empty($_REQUEST['rental_end_date'])) {
wc_add_notice(__('Please select rental dates', 'woocommerce'), 'error');
return false;
}
$start_date = sanitize_text_field($_REQUEST['rental_start_date']);
$end_date = sanitize_text_field($_REQUEST['rental_end_date']);
// Validate dates
if (strtotime($start_date) >= strtotime($end_date)) {
wc_add_notice(__('End date must be after start date', 'woocommerce'), 'error');
return false;
}
// Check availability
if (!$this->is_available_for_dates($start_date, $end_date)) {
wc_add_notice(__('Product is not available for selected dates', 'woocommerce'), 'error');
return false;
}
return true;
}
}
// Register rental product type
class WC_Product_Rental_Init {
public function __construct() {
// Register product type
add_filter('product_type_selector', [$this, 'add_product_type']);
add_filter('woocommerce_product_class', [$this, 'product_class'], 10, 2);
// Admin hooks
add_action('woocommerce_product_options_general_product_data', [$this, 'add_rental_fields']);
add_action('woocommerce_process_product_meta_rental', [$this, 'save_rental_fields']);
// Frontend hooks
add_action('woocommerce_rental_add_to_cart', [$this, 'add_to_cart_template']);
add_filter('woocommerce_add_cart_item_data', [$this, 'add_cart_item_data'], 10, 3);
add_filter('woocommerce_cart_item_price', [$this, 'cart_item_price'], 10, 3);
}
/**
* Add rental product type
*/
public function add_product_type($types) {
$types['rental'] = __('Rental product', 'woocommerce');
return $types;
}
/**
* Filter product class
*/
public function product_class($classname, $product_type) {
if ($product_type === 'rental') {
return 'WC_Product_Rental';
}
return $classname;
}
/**
* Add rental fields to admin
*/
public function add_rental_fields() {
global $post;
echo '<div class="options_group show_if_rental clear">';
// Rental price per period
woocommerce_wp_text_input([
'id' => '_rental_price_per_period',
'label' => __('Rental price per period', 'woocommerce'),
'data_type' => 'price',
]);
// Rental period
woocommerce_wp_select([
'id' => '_rental_period',
'label' => __('Rental period', 'woocommerce'),
'options' => [
'hour' => __('Hour', 'woocommerce'),
'day' => __('Day', 'woocommerce'),
'week' => __('Week', 'woocommerce'),
'month' => __('Month', 'woocommerce'),
],
]);
// Minimum rental period
woocommerce_wp_text_input([
'id' => '_minimum_rental_period',
'label' => __('Minimum rental period', 'woocommerce'),
'type' => 'number',
'custom_attributes' => [
'min' => '1',
'step' => '1',
],
]);
// Maximum rental period
woocommerce_wp_text_input([
'id' => '_maximum_rental_period',
'label' => __('Maximum rental period', 'woocommerce'),
'type' => 'number',
'custom_attributes' => [
'min' => '1',
'step' => '1',
],
]);
echo '</div>';
}
}
// Initialize rental product type
new WC_Product_Rental_Init();
Advanced Product Features
Dynamic Pricing and Calculations
// Advanced pricing calculator
class WC_Product_Custom_Calculator extends WC_Product {
protected $product_type = 'custom_calculator';
/**
* Calculate dynamic price
*/
public function calculate_price($args = []) {
$base_price = $this->get_base_price();
$total = $base_price;
// Get pricing rules
$rules = $this->get_pricing_rules();
foreach ($rules as $rule) {
switch ($rule['type']) {
case 'multiply':
$total *= $this->evaluate_rule_value($rule, $args);
break;
case 'add':
$total += $this->evaluate_rule_value($rule, $args);
break;
case 'percentage':
$total += ($total * $this->evaluate_rule_value($rule, $args) / 100);
break;
case 'tiered':
$total = $this->calculate_tiered_price($rule, $args, $total);
break;
}
}
return apply_filters('woocommerce_product_calculated_price', $total, $this, $args);
}
/**
* Get pricing rules
*/
public function get_pricing_rules() {
$rules = $this->get_meta('_pricing_rules');
if (!is_array($rules)) {
$rules = [];
}
return $rules;
}
/**
* Calculate tiered pricing
*/
private function calculate_tiered_price($rule, $args, $base_price) {
$quantity = isset($args['quantity']) ? intval($args['quantity']) : 1;
$tiers = $rule['tiers'];
usort($tiers, function($a, $b) {
return $a['min_quantity'] - $b['min_quantity'];
});
foreach ($tiers as $tier) {
if ($quantity >= $tier['min_quantity']) {
if ($tier['price_type'] === 'fixed') {
return $tier['price'];
} elseif ($tier['price_type'] === 'percentage') {
return $base_price * (1 - $tier['discount'] / 100);
}
}
}
return $base_price;
}
/**
* AJAX price calculation endpoint
*/
public static function ajax_calculate_price() {
check_ajax_referer('wc_custom_calculator_nonce', 'nonce');
$product_id = intval($_POST['product_id']);
$product = wc_get_product($product_id);
if (!$product || !is_a($product, 'WC_Product_Custom_Calculator')) {
wp_send_json_error('Invalid product');
}
$args = $_POST['calculation_args'] ?? [];
$price = $product->calculate_price($args);
wp_send_json_success([
'price' => $price,
'formatted_price' => wc_price($price),
]);
}
}
// Register AJAX handler
add_action('wp_ajax_calculate_custom_price', ['WC_Product_Custom_Calculator', 'ajax_calculate_price']);
add_action('wp_ajax_nopriv_calculate_custom_price', ['WC_Product_Custom_Calculator', 'ajax_calculate_price']);
Composite Product Type
// Composite product with components
class WC_Product_Composite extends WC_Product {
protected $product_type = 'composite';
/**
* Get product components
*/
public function get_components() {
$components = $this->get_meta('_components');
if (!is_array($components)) {
return [];
}
// Enhance component data
foreach ($components as &$component) {
$component['products'] = $this->get_component_products($component);
$component['default_product'] = $this->get_component_default($component);
}
return $components;
}
/**
* Get component products
*/
private function get_component_products($component) {
$products = [];
if (!empty($component['product_ids'])) {
foreach ($component['product_ids'] as $product_id) {
$product = wc_get_product($product_id);
if ($product) {
$products[] = [
'id' => $product->get_id(),
'name' => $product->get_name(),
'price' => $product->get_price(),
'image' => $product->get_image(),
];
}
}
} elseif (!empty($component['category_ids'])) {
$products = wc_get_products([
'category' => $component['category_ids'],
'limit' => -1,
'return' => 'ids',
]);
}
return $products;
}
/**
* Calculate composite price
*/
public function get_composite_price($configuration = []) {
$price = 0;
$components = $this->get_components();
foreach ($components as $component_id => $component) {
if (isset($configuration[$component_id])) {
$product_id = $configuration[$component_id]['product_id'];
$quantity = $configuration[$component_id]['quantity'] ?? 1;
$product = wc_get_product($product_id);
if ($product) {
$component_price = $product->get_price() * $quantity;
// Apply component discount
if (!empty($component['discount'])) {
$component_price *= (1 - $component['discount'] / 100);
}
$price += $component_price;
}
} elseif ($component['required']) {
// Add default product price for required components
$default_product = wc_get_product($component['default_product']);
if ($default_product) {
$price += $default_product->get_price();
}
}
}
return $price;
}
}
Admin Interface Customization
Custom Product Data Panels
// Advanced admin interface for custom products
class WC_Custom_Product_Admin {
public function __construct() {
// Add custom product data tabs
add_filter('woocommerce_product_data_tabs', [$this, 'add_product_data_tabs']);
add_action('woocommerce_product_data_panels', [$this, 'add_product_data_panels']);
// Save custom fields
add_action('woocommerce_process_product_meta', [$this, 'save_custom_fields']);
// Admin scripts
add_action('admin_enqueue_scripts', [$this, 'enqueue_admin_scripts']);
}
/**
* Add custom product data tabs
*/
public function add_product_data_tabs($tabs) {
$tabs['custom_options'] = [
'label' => __('Custom Options', 'woocommerce'),
'target' => 'custom_options_data',
'class' => ['show_if_custom_type'],
'priority' => 21,
];
$tabs['pricing_rules'] = [
'label' => __('Pricing Rules', 'woocommerce'),
'target' => 'pricing_rules_data',
'class' => ['show_if_custom_calculator'],
'priority' => 22,
];
return $tabs;
}
/**
* Add product data panels
*/
public function add_product_data_panels() {
?>
<div id="custom_options_data" class="panel woocommerce_options_panel">
<div class="options_group">
<?php
// Custom fields
woocommerce_wp_text_input([
'id' => '_custom_field_1',
'label' => __('Custom Field 1', 'woocommerce'),
'desc_tip' => true,
'description' => __('Enter custom field value', 'woocommerce'),
]);
woocommerce_wp_select([
'id' => '_custom_option',
'label' => __('Custom Option', 'woocommerce'),
'options' => [
'option1' => __('Option 1', 'woocommerce'),
'option2' => __('Option 2', 'woocommerce'),
'option3' => __('Option 3', 'woocommerce'),
],
]);
?>
</div>
<div class="options_group">
<p class="form-field">
<label><?php esc_html_e('Components', 'woocommerce'); ?></label>
<div id="custom_components_container">
<!-- Dynamic components will be added here -->
</div>
<button type="button" class="button add_component">
<?php esc_html_e('Add Component', 'woocommerce'); ?>
</button>
</p>
</div>
</div>
<div id="pricing_rules_data" class="panel woocommerce_options_panel">
<div class="options_group">
<div id="pricing_rules_container">
<!-- Pricing rules interface -->
</div>
</div>
</div>
<?php
}
/**
* Enqueue admin scripts
*/
public function enqueue_admin_scripts($hook) {
if ('post.php' !== $hook && 'post-new.php' !== $hook) {
return;
}
global $post;
if ('product' !== $post->post_type) {
return;
}
wp_enqueue_script(
'wc-custom-product-admin',
plugins_url('assets/js/admin.js', __FILE__),
['jquery', 'jquery-ui-sortable'],
'1.0.0',
true
);
wp_localize_script('wc-custom-product-admin', 'wcCustomProduct', [
'nonce' => wp_create_nonce('wc_custom_product'),
'i18n' => [
'addComponent' => __('Add Component', 'woocommerce'),
'removeComponent' => __('Remove', 'woocommerce'),
'confirmDelete' => __('Are you sure?', 'woocommerce'),
],
]);
}
}
new WC_Custom_Product_Admin();
Frontend Display and Cart Handling
Custom Add to Cart Template
// Frontend display for custom products
class WC_Custom_Product_Frontend {
public function __construct() {
// Template hooks
add_action('woocommerce_custom_calculator_add_to_cart', [$this, 'calculator_add_to_cart']);
add_action('woocommerce_composite_add_to_cart', [$this, 'composite_add_to_cart']);
// Cart handling
add_filter('woocommerce_add_cart_item_data', [$this, 'add_cart_item_data'], 10, 3);
add_filter('woocommerce_get_cart_item_from_session', [$this, 'get_cart_item_from_session'], 10, 3);
add_filter('woocommerce_cart_item_name', [$this, 'cart_item_name'], 10, 3);
}
/**
* Calculator product add to cart form
*/
public function calculator_add_to_cart() {
global $product;
wc_get_template('single-product/add-to-cart/calculator.php', [
'product' => $product,
'pricing_rules' => $product->get_pricing_rules(),
]);
}
/**
* Add custom data to cart item
*/
public function add_cart_item_data($cart_item_data, $product_id, $variation_id) {
$product = wc_get_product($product_id);
if ($product->is_type('custom_calculator')) {
// Store calculation parameters
if (isset($_POST['calculation_args'])) {
$cart_item_data['calculation_args'] = $_POST['calculation_args'];
$cart_item_data['calculated_price'] = $product->calculate_price($_POST['calculation_args']);
}
} elseif ($product->is_type('composite')) {
// Store component selection
if (isset($_POST['composite_configuration'])) {
$cart_item_data['composite_configuration'] = $_POST['composite_configuration'];
$cart_item_data['composite_price'] = $product->get_composite_price($_POST['composite_configuration']);
}
}
return $cart_item_data;
}
/**
* Display custom cart item data
*/
public function cart_item_name($name, $cart_item, $cart_item_key) {
if (isset($cart_item['calculation_args'])) {
$name .= '<dl class="variation">';
foreach ($cart_item['calculation_args'] as $key => $value) {
$name .= '<dt>' . esc_html($key) . ':</dt>';
$name .= '<dd>' . esc_html($value) . '</dd>';
}
$name .= '</dl>';
}
if (isset($cart_item['composite_configuration'])) {
$name .= '<ul class="composite-components">';
foreach ($cart_item['composite_configuration'] as $component_id => $config) {
$component_product = wc_get_product($config['product_id']);
if ($component_product) {
$name .= '<li>' . esc_html($component_product->get_name());
if ($config['quantity'] > 1) {
$name .= ' × ' . $config['quantity'];
}
$name .= '</li>';
}
}
$name .= '</ul>';
}
return $name;
}
}
new WC_Custom_Product_Frontend();
Real-World Examples
Subscription Box Product
// Subscription box product type
class WC_Product_Subscription_Box extends WC_Product {
protected $product_type = 'subscription_box';
/**
* Get subscription intervals
*/
public function get_subscription_intervals() {
return [
'weekly' => __('Weekly', 'woocommerce'),
'monthly' => __('Monthly', 'woocommerce'),
'quarterly' => __('Quarterly', 'woocommerce'),
];
}
/**
* Get box contents for period
*/
public function get_box_contents($period_number) {
$all_contents = $this->get_meta('_box_contents');
if (isset($all_contents[$period_number])) {
return $all_contents[$period_number];
}
// Return rotating contents
$rotation_size = count($all_contents);
if ($rotation_size > 0) {
$index = $period_number % $rotation_size;
return $all_contents[$index];
}
return [];
}
/**
* Calculate next box date
*/
public function get_next_box_date($last_date = null) {
if (!$last_date) {
$last_date = current_time('Y-m-d');
}
$interval = $this->get_subscription_interval();
switch ($interval) {
case 'weekly':
return date('Y-m-d', strtotime($last_date . ' +1 week'));
case 'monthly':
return date('Y-m-d', strtotime($last_date . ' +1 month'));
case 'quarterly':
return date('Y-m-d', strtotime($last_date . ' +3 months'));
}
return $last_date;
}
}
// Initialize subscription box type
add_action('init', function() {
if (class_exists('WC_Product_Subscription_Box')) {
// Register product type
add_filter('product_type_selector', function($types) {
$types['subscription_box'] = __('Subscription Box', 'woocommerce');
return $types;
});
add_filter('woocommerce_product_class', function($classname, $product_type) {
if ($product_type === 'subscription_box') {
return 'WC_Product_Subscription_Box';
}
return $classname;
}, 10, 2);
}
});
Creating custom product types in WooCommerce opens up endless possibilities for unique e-commerce experiences. By understanding the product architecture and implementing proper admin interfaces, cart handling, and display logic, you can build product types that perfectly match your business requirements.