The data layer: how a configurator's content is read, saved and handed to the browser, how a choice resolves itself, how global configurators are copied and linked, and the security around the frontend endpoints.
These are the hooks to reach for when you want to change what the configurator works with, rather than how it looks.
See the hook reference index for how to register callbacks.
Data handed to the browser
| Hook | Type | Arguments | Purpose |
|---|---|---|---|
mkl_product_configurator_init_data | filter | $initData, $product | The configurator's initial data |
mkl_product_configurator_get_front_end_data | filter | $initData, $product | The same, after frontend-specific data is merged |
mkl_product_configurator_get_front_end_data/product_type | filter | $type, $product | The product type used to build it |
mkl_product_configurator_content_data | filter | $data, $postId | The configurator's content |
mkl_pc_get_configurator_data | filter | $data, $id | The data written to the cached config file |
mkl_pc_get_configurator_data_js_output | filter | $output, $id, $data | The JavaScript that file contains |
mkl_pc_get_configurator_gzip_data_js_output | filter | $gzip | Whether that output is gzipped |
mkl_pc_config_file_name | filter | $filename | Name of the cached config file |
mkl_pc_cache_dir | filter | $dir | Path and URL of the cache directory |
mkl_pc_default_config_url | filter | $url | Fallback URL when no cache file exists |
Reading and saving
| Hook | Type | Arguments | Purpose |
|---|---|---|---|
mkl_pc/db/get | filter | $data, $that, $productId | Any data read out of the configurator store |
mkl_pc_db_fields | filter | $fields | Accepted fields, with their sanitise and escape callbacks |
mkl_product_configurator/data/set/content | filter | $data, $id | Content being saved |
mkl_product_configurator/data/set/layers | filter | $data, $id | Layers being saved |
mkl_product_configurator/data/set/{$component} | filter | $data, $id | Any component being saved |
mkl_product_configurator/data/set/choice | filter | $choice, $id, $rawData, $modifiedChoices | One choice being saved |
mkl_pc_before_save_product_configuration | action | $id, $rawData | Before a configuration is written |
mkl_pc_before_save_product_configuration_{$component} | action | $id, $rawData | Same, per component |
mkl_pc_saved_product_configuration | action | $id | After a configuration is written |
mkl_pc_saved_product_configuration_{$component} | action | $id, $data | Same, per component |
mkl_pc_saved_product_configuration_content | action | $id, $data | After content is written |
mkl_pc_saved_product_configuration_layers | action | $id, $layers | After layers are written |
mkl_pc_saved_configurator_data | action | $id, $refId, $component, $data, $modifiedChoices | After any component is written |
mkl_pc_saved_global_layer | action | $resultId, $layer, $content | After a global layer is written |
mkl_set_configurator_data_sanitize | filter | true | Whether incoming data is sanitised |
mkl_pc_verify_chunked_storage_integrity | filter | $cached, $parentId, $variationId, $force | Chunked-storage integrity result |
mkl_pc_chunked_storage_finalized | action | $parentId, $variationId, $verify | Chunked storage finished writing |
Choices and configurations
| Hook | Type | Arguments | Purpose |
|---|---|---|---|
mkl_pc/choice/init | action | $choice, $layerData | A Choice object was constructed |
mkl_pc/choice/sleep | action | $choice | It is being serialised |
mkl_pc/choice/wakeup | action | $choice | It is being restored |
mkl_pc_choice_sleep_properties | filter | $properties, $choice | Which properties survive serialisation |
mkl_pc_choice_set_selected_choice__choices | filter | $choices, $choice | The layer's choice list during resolution |
mkl_pc_choice_set_selected_choice__choice | filter | $choice, $that | The resolved choice |
mkl_pc_choice_set_selected_choice__images | filter | $images, $choice | Its images |
mkl_pc/choice/source | filter | $source, $choice | Whether a choice counts as stored or live |
mkl_pc/choice/saved_value | filter | $value, $key, $choice | One value read back from a stored configuration |
mkl_pc/choice/image_id | filter | $imageId, $type, $choice | The image ID a choice resolves to |
mkl_pc/choice/verify_saved_image | filter | true, $attachmentId, $choice | Whether to check a saved image still exists |
mkl_pc_item_meta | filter | $meta, $layer, $product, $itemKey, $context | A layer's label/value pair everywhere it is displayed |
mkl_pc_item_meta/wrap_choice_name | filter | true, $layer | Whether the choice name is wrapped in a <span> |
mkl_pc_new_configuration_data | filter | $data | The post array for a new saved configuration |
mkl_pc_configuration_visibility | filter | $visibility, $configuration | Visibility of a saved configuration |
mkl_pc_configuration_saved_message_success | filter | $message | Success message after saving |
mkl_pc/configuration/get_configured_price | filter | $prices, $configuration, $layers, $args | Base, extra and total for a configuration |
Images
| Hook | Type | Arguments | Purpose |
|---|---|---|---|
mkl_pc_images_merge__single_image_src | filter | $image | Source of each image in a merge |
mkl-pc-serve-image-process-layer-image | filter | $path, $layer | Path of a layer image being merged |
mkl_pc_get_image_url_default_empty_image | filter | $url | Placeholder when a layer has no image |
Global configurators
| Hook | Type | Arguments | Purpose |
|---|---|---|---|
mkl_pc/global_configurators/linked | action | $productId, $globalId, $wipeData | A product was linked to a global configurator |
mkl_pc/global_configurators/unlinked | action | $productId, $globalId, $copyData | It was unlinked |
mkl_pc/global_configurators/source_changed | action | $productId, $postedId, $previous | Its source changed |
mkl_pc/global_configurators/created_from_product | action | $newId, $sourceProductId | One was created from a product |
mkl_pc/global_configurators/copy/single_meta_keys | filter | $keys | Meta keys copied wholesale |
mkl_pc/global_configurators/apply_category_ids | filter | $ids, $globalId | Categories a global configurator applies to |
mkl_pc/global_configurators/category_includes_children | filter | true, $globalId | Whether child categories are included |
mkl_pc/global_configurators/block_delete_when_in_use | filter | true, $post, $consumers | Block deleting one that is in use |
Settings and plugin
| Hook | Type | Arguments | Purpose |
|---|---|---|---|
mkl_pc__settings_defaults | filter | $defaults | Default values for every setting |
mkl/pc/setting/{$key} | filter | $default | The default for one setting |
mkl/pc/theme_id | filter | $themeId | The active configurator theme |
mkl_pc_installed_themes | filter | $themes | The list of available themes |
mkl_pc_translatable_settings | filter | $keys | Which settings WPML may translate |
mkl_pc_translatable_settings_defaults | filter | $defaults | Their default values |
mkl_pc_woocommerce_product_types | filter | $types | Product types that can be configured |
mkl_pc_product_post_types | filter | $postTypes | Post types treated as products |
mkl_pc/configuration_meta_mode | filter | $mode, $product | How configuration meta is written to orders |
mkl_pc_allowed_3d_material_properties | filter | $properties | Material properties a 3D choice may set |
mkl_pc_3d_default_hidden_object_names | filter | $defaults | Objects hidden by default in a 3D scene |
mkl_pc/custom_html/allowed_tags | filter | $tags | Tags allowed in custom HTML fields |
mkl_pc_is_loaded | action | (none) | The plugin finished loading |
mkl_pc_before_check_update | action | (none) | Before the update routine runs |
mkl_pc_updated_plugin | action | (none) | After it does |
AJAX, REST and security
| Hook | Type | Arguments | Purpose |
|---|---|---|---|
mkl_pc_frontend_action_token_ttl | filter | 10 * MINUTE_IN_SECONDS | Lifetime of a frontend action token |
mkl_pc_frontend_action_token_purposes | filter | $purposes | Valid token purposes |
mkl_pc_frontend_action_token_skip_captcha | filter | $skip, $purpose, $request | Skip captcha for this request |
mkl_pc_frontend_action_token_require_captcha | filter | false, $purpose, $request | Require captcha for this request |
mkl_pc_frontend_action_token_captcha_provider | filter | 'auto', $purpose, $request | Which captcha provider to use |
mkl_pc_rate_limit_rule | filter | $rule, $action | Requests allowed per window |
mkl_pc_rate_limit_client_key | filter | $key, $ip | How a client is identified for rate limiting |
mkl_product_configurator_get_front_end_data
The single most important hook here. Everything the configurator knows when it opens —
PC.fe.currentProductData — comes through it. Core uses it itself, at priority 20, to re-resolve
image URLs.
add_filter( 'mkl_product_configurator_get_front_end_data', function ( $init_data, $product ) { $init_data['product_info']['my_lead_time'] = get_post_meta( $product->get_id(), '_lead_time', true ); return $init_data;}, 30, 2 );
mkl_product_configurator_init_data is the earlier, shared stage — it also runs for the admin
editor. Filter that one when the change belongs in both, and this one when it is frontend-only.
Note the data is cached to a file per product (see mkl_pc_get_configurator_data below), so a
filter that varies per visitor may not take effect the way you expect.
mkl_pc_get_configurator_data and the cached config file
For performance, a product's configuration is written to a JavaScript file and enqueued directly
rather than fetched over AJAX. mkl_pc_get_configurator_data filters the data going into that
file; mkl_pc_get_configurator_data_js_output filters the JavaScript itself.
add_filter( 'mkl_pc_get_configurator_data', function ( $data, $id ) { return $data;}, 10, 2 );
This is cached. The file is regenerated when the product is modified, so a filter that
depends on the current user, the time of day or the cart will produce whatever was true when the
file was written. For per-visitor changes use
mkl_product_configurator_get_front_end_data with the async data mode, or adjust in JavaScript.
mkl_pc_item_meta
Fired from Product::set_layer_item_meta(), which is the common path for displaying a layer in
the cart, the order, emails and the admin. $context tells you which — 'cart' or 'order'.
add_filter( 'mkl_pc_item_meta', function ( $meta, $layer, $product, $item_key, $context ) { if ( 'order' === $context ) { $meta['value'] .= ' [' . $layer->get_choice( 'sku' ) . ']'; } return $meta;}, 10, 5 );
Several add-ons hook this — Extra Price at 10, core's SKU handling at 20, WPML translation at 2. Pick your priority with that in mind.
mkl_pc_db_fields
Declares every field the configurator will accept, each with a sanitize and an escape
callback. This is the allowlist: a field that is not here is dropped when a configuration is
saved. Adding custom data to a choice means adding it here too.
add_filter( 'mkl_pc_db_fields', function ( $fields ) { $fields['my_reference'] = array( 'sanitize' => 'sanitize_text_field', 'escape' => 'esc_html', ); return $fields;} );
Always supply both callbacks. The data arrives from the browser.
mkl_pc/db/get
Every read from the configurator store passes through here, with the store object and the product id. Broad enough to be powerful and broad enough to be dangerous — it runs on the admin editor as well as the frontend, and often several times per request.
add_filter( 'mkl_pc/db/get', function ( $data, $that, $product_id ) { return $data;}, 10, 3 );
Keep it cheap, and be specific about what you change.
mkl_pc_saved_product_configuration and the save actions
Several fire when a configurator is saved, from most to least specific:
mkl_pc_before_save_product_configuration/…_{$component}— before writingmkl_product_configurator/data/set/{$component}— filters the data being writtenmkl_pc_saved_product_configuration_{$component}— after that componentmkl_pc_saved_configurator_data— after any component, with the modified choicesmkl_pc_saved_product_configuration— once, at the end
{$component} is content, layers, angles and so on.
add_action( 'mkl_pc_saved_product_configuration', function ( $id ) { my_clear_cache_for_product( $id );} );
Use mkl_pc_saved_configurator_data when you need to know which choices changed — it receives
$modified_choices.
mkl_pc/configuration/get_configured_price
The resolved price of a saved configuration, as base, extra and total. With Extra Price
active the figures match cart pricing.
add_filter( 'mkl_pc/configuration/get_configured_price', function ( $prices, $configuration, $layers, $args ) { $prices['total'] = $prices['base'] + $prices['extra'] + my_setup_fee(); return $prices;}, 10, 4 );
Useful for quotes, saved-design listings and anything pricing a configuration outside the cart.
mkl_pc/choice/init, sleep and wakeup
MKL\PC\Choice objects are serialised into cart and order data, so they have an explicit sleep
and wake cycle. mkl_pc_choice_sleep_properties decides what survives — add a property of your
own there or it is lost between the cart and the order.
add_filter( 'mkl_pc_choice_sleep_properties', function ( $properties, $choice ) { $properties[] = 'my_custom_property'; return $properties;}, 10, 2 );add_action( 'mkl_pc/choice/wakeup', function ( $choice ) { // Rebuild anything that was deliberately not serialised.} );
Keep the list short. Everything on it is stored with every cart item and every order line.
mkl_pc/choice/source and the saved-data filters
A Choice stores only IDs, and re-resolves the choice, its images and its names against the
product's current content every time it is read. That is right while a customer is
configuring, and wrong for a cart item, an order or a quote: those are records of what was
configured, and editing the configurator afterwards would rewrite them.
So a choice knows where it came from. Choice::is_stored() is true for anything restored from
storage — which Choice::__wakeup() detects, since orders, cart sessions and quote sessions all
arrive through unserialize() while a live add-to-cart never does. For a stored choice the data
saved with the configuration wins, and the product's current content is only the fallback:
- the image, through
get_saved_image_id() - the layer and choice names, and the choice SKU, through
get_saved() - the stacking order in the merged image, through
get_image_order()
Each falls back to the current content only when the configuration holds no value at all — a configuration saved without an image keeps no image, rather than picking up one added later.
mkl_pc/choice/source overrides the verdict, mkl_pc/choice/saved_value filters each value read
back, and mkl_pc/choice/image_id filters the resolved image ID. mkl_pc/choice/verify_saved_image
turns off the check that a saved attachment still exists, which saves a query per layer at the
cost of rendering nothing for layers whose image was deleted from the media library.
add_filter( 'mkl_pc/choice/image_id', function ( $image_id, $type, $choice ) { if ( 'image' !== $type || ! $choice->is_stored() ) return $image_id; // Swap a discontinued image for its replacement, in old orders only. return my_replacement_image_for( $choice->get_saved_image_id() ) ?: $image_id;}, 10, 3 );// Treat a configuration rebuilt by your own code as a record, not a live selection.add_action( 'mkl_pc/choice/init', function ( $choice ) { if ( my_plugin_is_restoring() ) $choice->set_source( MKL\PC\Choice::SOURCE_STORED );} );
Only reach for set_source() if you rebuild a stored configuration yourself instead of
unserialising it — restoring one the usual way is detected already.
mkl_pc__settings_defaults and mkl/pc/setting/{$key}
The first filters the whole defaults array; the second is a dynamic hook for one setting, where
{$key} is the setting's key.
add_filter( 'mkl/pc/setting/show_reset_button', function ( $default ) { return true;} );
These change defaults, not stored values — a merchant who has saved a value still wins.
mkl_pc/configuration_meta_mode
Whether an order gets a single combined meta or individual per-layer metas. Determines which of the order hooks fire.
add_filter( 'mkl_pc/configuration_meta_mode', function ( $mode, $product ) { return 'individual';}, 10, 2 );
mkl_pc_woocommerce_product_types
Which product types may be configured — simple, variable and variation by default. Extending
this is how a custom product type gains a configurator.
add_filter( 'mkl_pc_woocommerce_product_types', function ( $types ) { $types[] = 'my_custom_type'; return $types;} );
Global configurator hooks
mkl_pc/global_configurators/linked and …/unlinked fire when a product's link to a global
configurator changes, and carry whether data was wiped or copied.
add_action( 'mkl_pc/global_configurators/linked', function ( $product_id, $global_id, $wipe ) { my_log( "product $product_id now uses global $global_id" );}, 10, 3 );
…/apply_category_ids and …/category_includes_children decide which products a global
configurator claims; …/block_delete_when_in_use guards deletion, receiving the list of products
still using it.
Frontend action tokens
Public endpoints — saving a design, sharing a configuration — are guarded by short-lived tokens with optional captcha. The defaults are deliberately conservative:
// Require a captcha for guests saving a design.add_filter( 'mkl_pc_frontend_action_token_require_captcha', function ( $require, $purpose, $request ) { return 'save_design' === $purpose ? true : $require;}, 10, 3 );
…_skip_captcha defaults to is_user_logged_in(), so logged-in users are exempt and guests fall
through to …_require_captcha. …_captcha_provider is 'auto', which picks Turnstile then
hCaptcha from whatever is installed.
Loosening these weakens protection on endpoints anonymous visitors can reach. Widen
skip_captcha only when the request carries another trusted signal.
mkl_pc_rate_limit_rule and mkl_pc_rate_limit_client_key
rule receives [ 'max' => …, 'window' => … ] and the action key; the fallback is 60 requests an
hour. client_key decides how a client is identified — filter it when the site sits behind a
proxy that makes every visitor share an IP.
add_filter( 'mkl_pc_rate_limit_rule', function ( $rule, $action ) { if ( 'save_design' === $action ) { $rule['max'] = 20; } return $rule;}, 10, 2 );
mkl_pc_cache_dir and mkl_pc_config_file_name
Where cached config files live and what they are called. Filter them when the uploads directory is on read-only or shared storage.
add_filter( 'mkl_pc_cache_dir', function ( $dir ) { $dir['path'] = '/var/cache/mkl-pc/'; $dir['url'] = 'https://cdn.example.com/mkl-pc/'; return $dir;} );
Both path and url must be set and must point at the same place.
mkl_pc/custom_html/allowed_tags
The wp_kses allowlist for custom HTML fields, which shop managers fill in. Widen it only if you
trust everyone who can edit a configurator.
Third-party hooks fired here
The data layer fires WPML's wpml_object_id, wpml_translate_single_string,
wpml_active_languages, wpml_register_single_string and wpml_sync_custom_field, and
WooCommerce's woocommerce_quantity_input_min / …_max, woocommerce_get_item_data,
woocommerce_add_to_cart_validation, …_product_id, …_fragments,
woocommerce_cart_redirect_after_error and woocommerce_ajax_added_to_cart.