Class Structure
The plugin uses a custom autoloader rooted at the MCP_ class-name prefix and the includes/ directory.
| Class | File | Responsibility |
|---|---|---|
| MCP_Core | class-mcp-core.php | Bootstrap, component init, activation / deactivation lifecycle |
| MCP_Admin | class-mcp-admin.php | Product meta box, WC Settings tab (General / Tools / System Status) |
| MCP_Frontend | class-mcp-frontend.php | Product page selection UI, asset enqueueing, AJAX endpoints for live total |
| MCP_Cart | class-mcp-cart.php | Cart item data, quantity matching, required vs extra split, removal cascade |
| MCP_Order | class-mcp-order.php | Order line item meta (_mcp_*), admin order detail relationship labels |
| MCP_Requirements | class-mcp-requirements.php | Reads per-product companion config, cached at 1-hour TTL |
| MCP_Cache | class-mcp-cache.php | Requirements cache layer + invalidation hooks |
| MCP_Export_Import | class-mcp-export-import.php | SKU-keyed JSON config import / export |
Constants
MCP_VERSION // '1.0.0'
MCP_PLUGIN_FILE // __FILE__ from the main plugin
MCP_PLUGIN_DIR // plugin_dir_path(__FILE__)
MCP_PLUGIN_URL // plugin_dir_url(__FILE__)
MCP_PLUGIN_BASENAME // plugin_basename(__FILE__)Action Hooks
`mcp_before_add_required_product`
Fires immediately before a required companion is added to the cart.
do_action('mcp_before_add_required_product', $product_id, $selected_product_id, $quantity, $cart_item_key);| Param | Type | Description |
|---|---|---|
| $product_id | int | Parent product ID |
| $selected_product_id | int | Companion product ID being added |
| $quantity | int | Quantity being added |
| $cart_item_key | string | Parent cart item key |
`mcp_after_add_required_product`
Fires after a required companion is added (or merged into an existing line).
do_action('mcp_after_add_required_product', $cart_item_key, $required_cart_key, $product_id, $selected_product_id, $quantity);`mcp_order_item_meta_added`
Fires after companion relationship meta has been written to an order line item.
do_action('mcp_order_item_meta_added', $item, $values, $parent_product_id, $group_id);$group_id is the literal string 'required' or 'optional'.
`mcp_after_clear_all_cache`
Fires after the requirements cache is fully flushed (Tools → Clear Cache).
do_action('mcp_after_clear_all_cache');Filter Hooks
`mcp_validate_add_to_cart`
Filter the validation result before a parent product is added to the cart.
add_filter('mcp_validate_add_to_cart', function($passed, $product_id, $first_selected_id, $quantity) {
// Return false to block add-to-cart with a wc_add_notice() already shown,
// or true to allow.
return $passed;
}, 10, 4);For backward compatibility, only the first selected companion ID is passed (the field is single-select for most customers).
`mcp_required_cart_data`
Filter the cart_item_data array used when adding a required companion.
add_filter('mcp_required_cart_data', function($cart_item_data, $product_id, $selected_product_id, $quantity) {
$cart_item_data['my_custom_flag'] = 'yes';
return $cart_item_data;
}, 10, 4);`mcp_optional_cart_data`
Same as above for add-on companions:
add_filter('mcp_optional_cart_data', function($cart_item_data, $product_id, $optional_product_id, $quantity) {
return $cart_item_data;
}, 10, 4);Cart Item Data Keys
Runtime keys set on cart line items:
| Key | Type | Set on |
|---|---|---|
| mcp_is_required | bool | required companions |
| mcp_is_optional | bool | add-on companions |
| mcp_is_standalone | bool | the same product when also bought independently |
| mcp_required_qty | int | required companions — the driven-by-parent portion |
| mcp_extra_qty | int | the customer-added-on-top portion |
| mcp_parent_product_id | int | parent product ID |
| mcp_parent_cart_key | string | parent cart item key |
These are not persisted to options; they live for the lifetime of the cart session and are promoted to order item meta at checkout.
Order Line Item Meta
Persisted on woocommerce_order_item_meta (or the HPOS equivalent):
| Meta key | Value | Notes |
|---|---|---|
| _mcp_is_required | 'yes' | leading underscore — hidden from order edit UI by default |
| _mcp_is_optional | 'yes' | |
| _mcp_group_id | 'required' or 'optional' | |
| _mcp_parent_product_id | parent product ID | used to render the "Required for [parent]" / "Add-on for [parent]" label |
Use $item->get_meta('_mcp_parent_product_id') to recover the parent from a line item.
SKU-Keyed Config Schema (Tools → Export)
{
"version": "1.0.0",
"exported_at": "2026-05-28T16:42:11Z",
"global": {
"multiselect_roles": ["wholesale", "dealer"]
},
"products": {
"PARENT-SKU-001": {
"required": {
"enabled": true,
"label": "Select Required Product",
"display": "radio",
"items": [
{ "sku": "REQ-SKU-A", "oos": "block" },
{ "sku": "REQ-SKU-B", "oos": "backorder" }
]
},
"optional": {
"enabled": true,
"label": "Add-ons",
"items": [
{ "sku": "ADD-SKU-X", "conflict_key": "voltage" },
{ "sku": "ADD-SKU-Y", "conflict_key": "voltage" }
]
},
"pricing_display": "both"
}
}
}SKUs are the join key on import — products on the destination site match by SKU and are skipped (with an admin notice line) if the SKU doesn't resolve.
HPOS Declaration
Declared on the before_woocommerce_init action:
AutomatticWooCommerceUtilitiesFeaturesUtil::declare_compatibility(
'custom_order_tables',
__FILE__,
true
);All order reads and writes go through WooCommerce APIs (wc_get_orders(), $order->get_meta(), $item->add_meta_data()), never get_post_meta() against the order ID.