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_Data | class-mcp-data.php | Versioned data layer for sections, "Display as" overrides, and exclusion pairs |
| MCP_Migration | class-mcp-migration.php | Batched 1.x → 2.0 schema migration (conflict groups → exclusions), legacy data retained for rollback |
| 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 (v2 format) |
Constants
MCP_VERSION // '2.0.0'
MCP_SCHEMA_VERSION // 3 — per-product data schema; drives automatic migration
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);`mcp_addons_heading_label`
Filter the heading shown above the add-on sections on the product page.
add_filter('mcp_addons_heading_label', function($label, $product_id) {
return $label;
}, 10, 2);`mcp_exclusion_message`
Filter the "Not compatible with ..." explanation shown on a dimmed, incompatible option.
add_filter('mcp_exclusion_message', function($message, $product_id, $blocked_by_id) {
return $message;
}, 10, 3);`mcp_required_change_removed_items_notice`
Filter the notice shown when changing the required selection auto-removes add-ons that are no longer compatible.
add_filter('mcp_required_change_removed_items_notice', function($notice, $removed_items) {
return $notice;
}, 10, 2);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)
The export is export_format 2. Each product carries a v2 block with the required section, named add-on sections, and exclusion pairs. References resolve by SKU with a product ID fallback so the file moves cleanly between sites. (A legacy requirement_groups / required_products block is still emitted for backward compatibility.)
{
"plugin": "maxtdesign-companion-products",
"version": "2.0.0",
"export_format": 2,
"export_date": "2026-06-10T16:42:11+00:00",
"global_settings": { "multiselect_roles": ["wholesale", "dealer"] },
"product_requirements": [
{
"product_sku": "PARENT-SKU-001",
"settings": {
"selection_label": "Select Required Product",
"out_of_stock_behavior": "block",
"price_display": { "show_individual": true, "show_combined": true }
},
"v2": {
"required_section": {
"label": "Select Required Product",
"display_style": "radio",
"products": [
{ "sku": "REQ-SKU-A", "label_override": null, "description": null },
{ "sku": "REQ-SKU-B", "label_override": "Short name", "description": null }
]
},
"addon_sections": [
{
"id": "hardware",
"label": "Mounting Hardware",
"mode": "checkbox",
"products": [
{ "sku": "ADD-SKU-X", "label_override": null },
{ "sku": "ADD-SKU-Y", "label_override": null }
]
}
],
"exclusions": [
{ "a": { "sku": "ADD-SKU-X" }, "b": { "sku": "ADD-SKU-Y" } }
],
"thumbnail_display": {}
}
}
]
}SKUs are the join key on import: products on the destination site match by SKU (with a product ID fallback) and are skipped, with an admin notice line, if neither resolves. Files exported from version 1 still import.
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.