Guide

Data and configuration hooks

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

HookTypeArgumentsPurpose
mkl_product_configurator_init_datafilter$initData, $productThe configurator's initial data
mkl_product_configurator_get_front_end_datafilter$initData, $productThe same, after frontend-specific data is merged
mkl_product_configurator_get_front_end_data/product_typefilter$type, $productThe product type used to build it
mkl_product_configurator_content_datafilter$data, $postIdThe configurator's content
mkl_pc_get_configurator_datafilter$data, $idThe data written to the cached config file
mkl_pc_get_configurator_data_js_outputfilter$output, $id, $dataThe JavaScript that file contains
mkl_pc_get_configurator_gzip_data_js_outputfilter$gzipWhether that output is gzipped
mkl_pc_config_file_namefilter$filenameName of the cached config file
mkl_pc_cache_dirfilter$dirPath and URL of the cache directory
mkl_pc_default_config_urlfilter$urlFallback URL when no cache file exists

Reading and saving

HookTypeArgumentsPurpose
mkl_pc/db/getfilter$data, $that, $productIdAny data read out of the configurator store
mkl_pc_db_fieldsfilter$fieldsAccepted fields, with their sanitise and escape callbacks
mkl_product_configurator/data/set/contentfilter$data, $idContent being saved
mkl_product_configurator/data/set/layersfilter$data, $idLayers being saved
mkl_product_configurator/data/set/{$component}filter$data, $idAny component being saved
mkl_product_configurator/data/set/choicefilter$choice, $id, $rawData, $modifiedChoicesOne choice being saved
mkl_pc_before_save_product_configurationaction$id, $rawDataBefore a configuration is written
mkl_pc_before_save_product_configuration_{$component}action$id, $rawDataSame, per component
mkl_pc_saved_product_configurationaction$idAfter a configuration is written
mkl_pc_saved_product_configuration_{$component}action$id, $dataSame, per component
mkl_pc_saved_product_configuration_contentaction$id, $dataAfter content is written
mkl_pc_saved_product_configuration_layersaction$id, $layersAfter layers are written
mkl_pc_saved_configurator_dataaction$id, $refId, $component, $data, $modifiedChoicesAfter any component is written
mkl_pc_saved_global_layeraction$resultId, $layer, $contentAfter a global layer is written
mkl_set_configurator_data_sanitizefiltertrueWhether incoming data is sanitised
mkl_pc_verify_chunked_storage_integrityfilter$cached, $parentId, $variationId, $forceChunked-storage integrity result
mkl_pc_chunked_storage_finalizedaction$parentId, $variationId, $verifyChunked storage finished writing

Choices and configurations

HookTypeArgumentsPurpose
mkl_pc/choice/initaction$choice, $layerDataA Choice object was constructed
mkl_pc/choice/sleepaction$choiceIt is being serialised
mkl_pc/choice/wakeupaction$choiceIt is being restored
mkl_pc_choice_sleep_propertiesfilter$properties, $choiceWhich properties survive serialisation
mkl_pc_choice_set_selected_choice__choicesfilter$choices, $choiceThe layer's choice list during resolution
mkl_pc_choice_set_selected_choice__choicefilter$choice, $thatThe resolved choice
mkl_pc_choice_set_selected_choice__imagesfilter$images, $choiceIts images
mkl_pc/choice/sourcefilter$source, $choiceWhether a choice counts as stored or live
mkl_pc/choice/saved_valuefilter$value, $key, $choiceOne value read back from a stored configuration
mkl_pc/choice/image_idfilter$imageId, $type, $choiceThe image ID a choice resolves to
mkl_pc/choice/verify_saved_imagefiltertrue, $attachmentId, $choiceWhether to check a saved image still exists
mkl_pc_item_metafilter$meta, $layer, $product, $itemKey, $contextA layer's label/value pair everywhere it is displayed
mkl_pc_item_meta/wrap_choice_namefiltertrue, $layerWhether the choice name is wrapped in a <span>
mkl_pc_new_configuration_datafilter$dataThe post array for a new saved configuration
mkl_pc_configuration_visibilityfilter$visibility, $configurationVisibility of a saved configuration
mkl_pc_configuration_saved_message_successfilter$messageSuccess message after saving
mkl_pc/configuration/get_configured_pricefilter$prices, $configuration, $layers, $argsBase, extra and total for a configuration

Images

HookTypeArgumentsPurpose
mkl_pc_images_merge__single_image_srcfilter$imageSource of each image in a merge
mkl-pc-serve-image-process-layer-imagefilter$path, $layerPath of a layer image being merged
mkl_pc_get_image_url_default_empty_imagefilter$urlPlaceholder when a layer has no image

Global configurators

HookTypeArgumentsPurpose
mkl_pc/global_configurators/linkedaction$productId, $globalId, $wipeDataA product was linked to a global configurator
mkl_pc/global_configurators/unlinkedaction$productId, $globalId, $copyDataIt was unlinked
mkl_pc/global_configurators/source_changedaction$productId, $postedId, $previousIts source changed
mkl_pc/global_configurators/created_from_productaction$newId, $sourceProductIdOne was created from a product
mkl_pc/global_configurators/copy/single_meta_keysfilter$keysMeta keys copied wholesale
mkl_pc/global_configurators/apply_category_idsfilter$ids, $globalIdCategories a global configurator applies to
mkl_pc/global_configurators/category_includes_childrenfiltertrue, $globalIdWhether child categories are included
mkl_pc/global_configurators/block_delete_when_in_usefiltertrue, $post, $consumersBlock deleting one that is in use

Settings and plugin

HookTypeArgumentsPurpose
mkl_pc__settings_defaultsfilter$defaultsDefault values for every setting
mkl/pc/setting/{$key}filter$defaultThe default for one setting
mkl/pc/theme_idfilter$themeIdThe active configurator theme
mkl_pc_installed_themesfilter$themesThe list of available themes
mkl_pc_translatable_settingsfilter$keysWhich settings WPML may translate
mkl_pc_translatable_settings_defaultsfilter$defaultsTheir default values
mkl_pc_woocommerce_product_typesfilter$typesProduct types that can be configured
mkl_pc_product_post_typesfilter$postTypesPost types treated as products
mkl_pc/configuration_meta_modefilter$mode, $productHow configuration meta is written to orders
mkl_pc_allowed_3d_material_propertiesfilter$propertiesMaterial properties a 3D choice may set
mkl_pc_3d_default_hidden_object_namesfilter$defaultsObjects hidden by default in a 3D scene
mkl_pc/custom_html/allowed_tagsfilter$tagsTags allowed in custom HTML fields
mkl_pc_is_loadedaction(none)The plugin finished loading
mkl_pc_before_check_updateaction(none)Before the update routine runs
mkl_pc_updated_pluginaction(none)After it does

AJAX, REST and security

HookTypeArgumentsPurpose
mkl_pc_frontend_action_token_ttlfilter10 * MINUTE_IN_SECONDSLifetime of a frontend action token
mkl_pc_frontend_action_token_purposesfilter$purposesValid token purposes
mkl_pc_frontend_action_token_skip_captchafilter$skip, $purpose, $requestSkip captcha for this request
mkl_pc_frontend_action_token_require_captchafilterfalse, $purpose, $requestRequire captcha for this request
mkl_pc_frontend_action_token_captcha_providerfilter'auto', $purpose, $requestWhich captcha provider to use
mkl_pc_rate_limit_rulefilter$rule, $actionRequests allowed per window
mkl_pc_rate_limit_client_keyfilter$key, $ipHow 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.

php functions.php
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_get_front_end_data.

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.

php functions.php
add_filter( 'mkl_pc_get_configurator_data', function ( $data, $id ) {	return $data;}, 10, 2 );
mkl_pc_get_configurator_data and the cached config file.

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'.

php functions.php
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 );
mkl_pc_item_meta.

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.

php functions.php
add_filter( 'mkl_pc_db_fields', function ( $fields ) {	$fields['my_reference'] = array(		'sanitize' => 'sanitize_text_field',		'escape'   => 'esc_html',	);	return $fields;} );
mkl_pc_db_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.

php functions.php
add_filter( 'mkl_pc/db/get', function ( $data, $that, $product_id ) {	return $data;}, 10, 3 );
mkl_pc/db/get.

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 writing
  • mkl_product_configurator/data/set/{$component} — filters the data being written
  • mkl_pc_saved_product_configuration_{$component} — after that component
  • mkl_pc_saved_configurator_data — after any component, with the modified choices
  • mkl_pc_saved_product_configuration — once, at the end

{$component} is content, layers, angles and so on.

php functions.php
add_action( 'mkl_pc_saved_product_configuration', function ( $id ) {	my_clear_cache_for_product( $id );} );
mkl_pc_saved_product_configuration and the save actions.

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.

php functions.php
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 );
mkl_pc/configuration/get_configured_price.

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.

php functions.php
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.} );
mkl_pc/choice/init, sleep and wakeup.

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.

php functions.php
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 );} );
mkl_pc/choice/image_id and mkl_pc/choice/source.

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.

php functions.php
add_filter( 'mkl/pc/setting/show_reset_button', function ( $default ) {	return true;} );
mkl_pc__settings_defaults and mkl/pc/setting/{$key}.

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.

php functions.php
add_filter( 'mkl_pc/configuration_meta_mode', function ( $mode, $product ) {	return 'individual';}, 10, 2 );
mkl_pc/configuration_meta_mode.

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.

php functions.php
add_filter( 'mkl_pc_woocommerce_product_types', function ( $types ) {	$types[] = 'my_custom_type';	return $types;} );
mkl_pc_woocommerce_product_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.

php functions.php
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 );
Global configurator hooks.

…/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:

php functions.php
// 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 );
Frontend action tokens.

…_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.

php functions.php
add_filter( 'mkl_pc_rate_limit_rule', function ( $rule, $action ) {	if ( 'save_design' === $action ) {		$rule['max'] = 20;	}	return $rule;}, 10, 2 );
mkl_pc_rate_limit_rule and mkl_pc_rate_limit_client_key.

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.

php functions.php
add_filter( 'mkl_pc_cache_dir', function ( $dir ) {	$dir['path'] = '/var/cache/mkl-pc/';	$dir['url']  = 'https://cdn.example.com/mkl-pc/';	return $dir;} );
mkl_pc_cache_dir and mkl_pc_config_file_name.

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.