Example Code To Simplify Your Imports and Exports

These snippets cover the more common cases where custom code is necessary during import. Unique plugin or theme requirements often necessitate such code. It can also be due to odd import file formats that aren't fully compatible with WP All Import on their own. You would place these snippets in WP All Import's Function Editor.

Click Here for more information.

Delete a Custom Field If Another Field Has a Specific Value

Here we use the pmxi_saved_post action, get_post_meta(), and delete_post_meta() to delete _my_custom_field if _my_update_check is set to 'yes'.

function conditional_delete( $id ) {

    // Retrieve check field's value.
    $check = get_post_meta( $id, '_my_delete_check', true );

    if ( $check === 'yes' ) {

        // If check value is 'yes' delete _my_field.
        delete_post_meta( $id, '_my_field' );

add_action( 'pmxi_saved_post', 'conditional_delete', 10, 1 );

Markup Price with Minimum and Maximum Limits

Here we clean up the price in our file, add a markup, and ensure it's within our required minimum and maximum prices. The $price and $multiplier parameters are required. The others are optional and can be used to further customize the final price:

The original price.
$multiplierMultiply the original price by this number.
$nearestRound to the nearest value (0.01 by default).
$minusSubtract this value from the rounded price.
$mapThe minimum price that can be returned.
$msrpThe maximum price that can be returned.

We call the function from any field that needs a marked up price:

function round_price_with_limits( $price = null, $multiplier = 1, $nearest = .01, $minus = 0, $map = 0, $msrp = 9999999999) {

    // Ensure a price was provided.
    if ( !empty( $price ) ) {

        // Remove unwanted characters from price.
        $price = preg_replace("/[^0-9,.]/", "", $price);

        // Remove unwanted characters from min price.
        $map = preg_replace("/[^0-9,.]/", "", $map);

        // Remove unwanted characters from max price.
        $msrp = preg_replace("/[^0-9,.]/", "", $msrp);

        // Calculate price with markup and round it.
        $rounded_price = ( round ( ( $price * $multiplier ) / $nearest ) * $nearest ) - $minus; 

        // If price is less than minimum, return minimum.
        if($rounded_price < $map){

            return $map;

        // If price is greater than maximum, return maximum.
        elseif($rounded_price > $msrp){

            return $msrp;

        } else {

            // Return price otherwise.
            return $rounded_price;



Trigger the next Import on Completion of an Import

Here we use the pmxi_after_xml_import action to redirect the connection to the next import's 'trigger' URL. We are able to ensure the first import will always complete before the next is started. This is especially helpful when our second import depends on data provided by the first.

All imports still require the 'processing' cron jobs or they won't run.

function after_xml_import($import_id, $import)

    // Only run for import ID 5.
    if ($import_id == 5) {

        // Call the next import's trigger URL.


add_action('pmxi_after_xml_import', 'after_xml_import', 10, 2);

Reduce Stock for Imported Order Items

Here we use the pmxi_saved_post action and wc_reduce_stock_levels() to deduct ordered items from product stock. This is necessary since WP All Import doesn't touch product stock when importing Orders by default.

function decrease_order_item_stock($post_id, $xml_node, $is_update)

    // Retrieve the import ID. 
    $import_id = ( isset( $_GET['id'] ) ? $_GET['id'] : ( isset( $_GET['import_id'] ) ? $_GET['import_id'] : 'new' ) );

    // Only run for imports 899, 898, and 895.
    if ( in_array( $import_id, [899, 898, 895] ) ) {

        // Decrease stock for order items if it hasn't already
        // been decreased.
        wc_reduce_stock_levels( $post_id );


add_action('pmxi_saved_post', 'decrease_order_item_stock', 10, 3);

Import Multisite Users to Multiple Subsites

Here we use our Custom Fields capability to assign Users to more than one subsite on WordPress Multisite. This workaround is required due to the way Multisite stores Users.

Custom fields are used to identify which subsite each User is authorized to use. Our example Multisite install uses the prefixes below for each subsite:


A user must have these three Custom Fields defined to be authorized on all of our subsites:


Each field must contain a serialized array of User Roles:


For example:

Get Parent Attributes When Exporting Variations to Google Merchant Center

Here we use wc_get_product to retrieve the WC_Product object for each exported record. This lets us retrieve attributes that are directly assigned to the parent products. By default, when exporting to GMC only the attributes attached to the variations themselves are exported.

// $attr must be the name of the attribute to be retrieved.
function my_get_parent_attr( $id, $attr ){

    // Get product object.
    $prod = wc_get_product( $id );

    // Check product type.
    if( $prod->is_type('variation') ){

        // Retrieve parent product's ID.
        $parent = wc_get_product( $prod->get_parent_id() );

        // Return parent's attribute value.
        return $parent->get_attribute($attr);


        // Return attribute value for simple products.
        return $prod->get_attribute($attr);


Here it is in use:

Enable Rapid Add-On API Sections for User or Customer Import

Here we use the pmxi_visible_template_sections filter to display our add-ons section. This allows it to show up for Users and Customers. We use the wp_all_import_is_images_section_enabled filter to hide the Images section as it's not normally displayed for Users and Customers.

// Enable our custom add-on's section.
function show_addon_section_users_customers( $sections, $post_type ) {

    // Enable add-on section for Users.
    if ( 'import_users' == $post_type ) 
        $sections[] = 'featured';

    // Enable add-on section for Customers.
    if ( 'shop_customer' == $post_type ) 
        $sections[] = 'featured';

    return $sections;

add_filter( 'pmxi_visible_template_sections', 'show_addon_section_users_customers', 11, 2 );

// Disable the Images section.
function hide_images_users_customers( $is_enabled, $post_type ){

    // Disable Images section for Users, return true to enable.
    if ( 'import_users' == $post_type )       
        $is_enabled = false;

    // Disable Images section for Customers, return true to enable.
    if ( 'shop_customer' == $post_type )       
        $is_enabled = false;
    return $is_enabled;


add_filter( 'wp_all_import_is_images_section_enabled', 'hide_images_users_customers', 10, 2 );

And, here's what the run() function would look like:

        "post_types" => array( "import_users", "shop_customer" )

Export Total Number of Sales for Variations

Here we export the total sales for each of our variations using some custom code. We need this workaround because WooCommerce doesn't track total sales at the variation level.

We call it like this:

function my_get_total_sales( $id ) {

    // Only use database calls for variation counts.
    if ( get_post_type( $id ) === 'product_variation' ) {
        global $wpdb;
        $table = $wpdb->prefix . 'woocommerce_order_itemmeta';
        $count = array();
        $total_count = 0;
        $itemmeta = $wpdb->get_results( "SELECT `order_item_id` FROM `$table` WHERE `meta_key` = '_variation_id' AND `meta_value` = '$id'" );

        // Ensure item meta was returned.
        if ( ! empty( $itemmeta ) ) {

            // Process each item.
            foreach ( $itemmeta as $result ) {

                // Ensure the order quantity was retrieved.
                if ( $qty = $wpdb->get_row( "SELECT `meta_value` FROM `$table` WHERE `meta_key` = '_qty' AND `order_item_id` = '{$result->order_item_id}'" ) ) {

                // Save the quantity ordered.
                $count[ $result->order_item_id ] = $qty->meta_value;


            // Switch to order items table.
            $table = $wpdb->prefix . 'woocommerce_order_items';

            // Process each item meta record.
            foreach ( $itemmeta as $item_obj ) {

                // Retrieve Order ID for each ordered item.
                $order_id_results = $wpdb->get_row( "SELECT `order_id`,`order_item_id` FROM `$table` WHERE `order_item_id` = '{$item_obj->order_item_id}'" );

                // Only continue if order was returned.
                if ( ! empty( $order_id_results ) ) {

                    // Retrieve the order status.
                    $status = get_post_status( $order_id_results->order_id );

                    // Check if order is completed.
                    if ( $status == 'wc-completed' ) {

                        // If it was add that item's count to total. 
                        $total_count = $total_count + $count[ $item_obj->order_item_id ];


    // Return total sales for variation.
    return $total_count;

    } else {

        // Get product object.
        $product = wc_get_product( $id );

        // If the product isn't varible, return total sales.
        return $product->get_total_sales();


Workaround for Importing from FTP

Here we call a custom function in the 'Download from URL' field to import from FTP. The code must be placed outside of WP All Import's Function Editor - your theme's functions.php file is a good place. We make use of wp_upload_dir() when saving the retrieved file.

The file is saved in the uploads directory, it's up to you to take any security precautions you deem necessary.

We call the function from the 'Download from URL' option on Step 1:

If your FTP requires a username and password, it would look something like this:

[custom_file_download("ftp://username:[email protected]/full/path/to/file.csv","csv")]

Otherwise, you can omit that part:


function custom_file_download( $url, $type = 'xml' ) {
    // Set our default cURL options.
    $ch = curl_init();
    curl_setopt( $ch, CURLOPT_URL, $url );
    curl_setopt( $ch, CURLOPT_RETURNTRANSFER, 1 );
    curl_setopt( $ch, CURLOPT_CUSTOMREQUEST, "GET" );

    /* Optional: Set headers if needed.
    *    $headers = array();
    *    $headers[] = "Accept-Language: de";
    *    curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);

    // Retrieve file from $url.
    $result = curl_exec( $ch );

    // Return error if cURL fails.
    if ( curl_errno( $ch ) ) {
        exit( 'Error:' . curl_error( $ch ) );
    curl_close( $ch );

    // Identify the upload directory path.
    $uploads  = wp_upload_dir();

    // Generate full file path and set extension to $type.
    $filename = $uploads['basedir'] . '/' . strtok( basename( $url ), "?" ) . '.' . $type;

    // If the file exists locally, mark it for deletion.
    if ( file_exists( $filename ) ) {
        @unlink( $filename );

    // Save the new file retrieved from FTP.
    file_put_contents( $filename, $result );

    // Return the URL to the newly created file.
    return str_replace( $uploads['basedir'], $uploads['baseurl'], $filename );


Variations Not Updating When Selected on Frontend

Here we use the woocommerce_ajax_variation_threshold filter to enable 150 variations to be loaded via AJAX on the frontend. This enables the product options to update when selecting each attribute from the dropdown. By default, those updates only work for products with 30 or fewer variations and products with more only have the selected options validated when adding them to the cart.

function soflyy_change_threshold( $amount, $product ) {

    // The max number of variations to load via AJAX.
    return 150;


add_filter( 'woocommerce_ajax_variation_threshold', 'soflyy_change_threshold', 10, 2 );

Retrieve Post Type of Current Import

Here we use the wp_all_import_is_post_to_create filter to retrieve the Post Type being imported. This code works with any of our hooks.

function get_import_post_type($continue_import, $current_xml_node, $import_id){

    // Retrieve import object.
    $import = new PMXI_Import_Record();

    // Ensure import object is valid.
    if ( ! $import->isEmpty() ) {

        // Retrieve post type.
        $post_type = $import->options['custom_type'];

        // Use post type in your code.


add_filter('wp_all_import_is_post_to_create', 'get_import_post_type', 10, 3);

Houzez - Linking Neighborhoods, Cities, States, and Countries

Here we use the pmxi_saved_post action to link Neighborhoods, Cities, and States for the Houzez theme. This is necessary because the Houzez Add-On for WP All Import doesn't handle it currently. You must use a Taxonomies import for these examples.

The neighborhoods are linked to cities by entries in the Options table. You'll need to set a 'parent_city' Custom Field:

Then add the code below to the Function Editor:

function link_houzez_city( $id, $xml, $is_update ) {

    // Retrieve 'parent_city' and use it to find that City.
    $term = get_term_by( "name", get_term_meta($id, "parent_city", true), "property_city" );

    // Retrieve the City's slug.
    $slug = $term->slug;
    // Generate the appropriate Option name.
    $option_name = '_houzez_property_area_' . $id;
    // Set the option to link this Neighborhood to its City.
    update_option($option_name, array("parent_city"=>$slug));

add_action( 'pmxi_saved_post', 'link_houzez_city', 10, 3 );

When adding Cities to States you must set the 'parent_state' Custom Field ( instead of parent_city ):

function link_houzez_state( $id, $xml, $is_update ) {

    // Retrieve 'parent_state' and use it to locate that State.
    $term = get_term_by( "name", get_term_meta($id, "parent_state", true), "property_state" );

    // Retrieve the State's slug.
    $slug = $term->slug;
    // Generate the Option name.
    $option_name = '_houzez_property_city_' . $id;
    // Set the Option to link the City and State.
    update_option($option_name, array("parent_state"=>$slug));


add_action( 'pmxi_saved_post', 'link_houzez_state', 10, 3 );

When adding States to Countries the Custom Field must be named 'parent_country' and must contain the 2 character Country Code ( e.g. US, CA, GB, IE, etc )

function link_houzez_country( $id, $xml, $is_update ) {

    // Retrieve 'parent_country' and use it as the slug.
    $slug = get_term_meta($id, "parent_country", 
    // Generate the Option name.
    $option_name = '_houzez_property_state_' . $id;
    // Set the Option to link State and Country.


add_action( 'pmxi_saved_post', 'link_houzez_country', 
3 );

Flatsome Theme - Taxonomy Top and Bottom Content

Here we use a custom function to import data to the Top and Bottom Content fields of the Flatsome Theme. This is necessary due to the format required for those fields.

The bottom content and top content are stored in a serialized array in the 'cat_meta' field. You must tell WPAI to update that field ( Manage Imports > Import Settings ):

Here's an example snippet to use in the Function Editor that accepts your Bottom and Top Content values as parameters. It returns the serialized array as required by Flatsome:

function my_term_meta( $bottomContent, $topContent = '' )
$meta = array('cat_header' => $topContent, 'cat_footer' => $bottomContent);
	return serialize($meta);


Here's the code in action:

Add Entry to the Import Log

Here we are using the pmxi_saved_post action to add an entry to the import log every time a post is successfully imported or updated. You can use a different action to add log entries at a different time, such as whenever an image is imported, or a custom field updated, etc.

function my_custom_log_entry($id) {     
    $logger = function($m) {printf("[%s] $m", date("H:i:s"));flush();};

    // Define your log entry here
    call_user_func($logger, "This is my log message."); 


add_action('pmxi_saved_post', 'my_custom_log_entry', 10, 1);

Reference Taxonomy Terms by Custom ID

Here we use get_terms() to retrieve previously imported categories by the _my_old_id custom field. This is necessary when using import files that only reference categories by ID.

Here's an example category file:

ParentAn example parent category.1
ChildAn example child category.12

Here's an example product file:


We start with a Taxonomies > Product Categories import. On Step 3, we save our ID to a custom field named _my_old_id:

The code below is placed in the Function Editor:

function my_get_cat_slug( $old_id, $id = true )
    $args = array(
        'hide_empty' => false,

        // Specify what field to search for the ID.
        'meta_query' => array(
            'key'       => '_my_old_id',
            'value'     => $old_id,
            'compare'   => '='

    // Search the Product Category taxonomy for our term.
    $terms = get_terms( 'product_cat', $args );
    if( $id === true ){

        // By default we return the term's WordPress ID.
        return $terms[0]->term_id;


        // If $id is false we return the term's slug instead.
        return $terms[0]->slug;	


Here we use our code to specify the Parent Term:

If all Parent Terms are listed before their children in the file, the records per iteration can be set to 1 to ensure they're linked (Manage Imports > Import Settings):

Otherwise, the import must be run twice to link the Parent Terms.

Now the imported Product Category terms are available to use when importing our products. We need each term's slug so we set the second parameter to false when calling our code:

If you need to reference multiple categories per record, you'll need some additional code in the Function Editor:

function my_get_multiple_slugs( $old_id, $id = true, $delimiter = ',' ){
    // Split the category references on the given delimiter.
    $values = explode($delimiter, $old_id);
    // Declare a variable for our located categories.
    $cat_list = [];
    // Process each category reference.
    foreach( $values as $value ){
        // Call our other function to process each category.
        $cat_list[] = my_get_cat_slug( $value, $id );
    // Return a list of the located categories using the defined delimiter.
    return implode($delimiter, $cat_list);

The taxonomies section should be updated to something like below, making sure the delimiter provided matches that used in your file:

Find File Elements That Start with Certain Text

Here we use an XPath expression to return values for all elements in the file that start with 'image'. The values are returned separated by commas.

{./*[starts-with(local-name(), 'image')]}

Map File Values During Import

Here we use a custom function to map an amenity code in our file to its description. This allows us to match our imported values with those on our site. The code expects either a single amenity or a comma-separated string of values.

function soflyy_map_amenities($amenities)
    // Convert comma separated list to array.
    $amenities = explode(",",$amenities);
    // Define your mappings.
    $map_values = array(
        'BA'         => 'Balcony', 
        'SP'         => 'Shared Pool', 
        'SS'         => 'Shared Spa',
        'file value' => 'returned value');

    // Declare a new array for our mapped values.	
    $mapped_amenities = array();

    // Process each amenity.	
    foreach($amenities as $amenity)
        // Check if we have a mapping for this amenity.
        if(array_key_exists($amenity, $map_values)){

            // If we do, use that value.
            $mapped_amenities[] = $map_values[$amenity];
        } else {
            // If we don't have a value mapping you 
            // can perform some other action.

    // Return the mapped values as a comma-separated string.		   
    return implode(",",$mapped_amenities);


Here's how we call the function with our amenities element:


Enable Custom Fields Section in Post Admin

Here we use the acf/settings/remove_wp_meta_box filter to re-enable the Custom Fields section when editing posts. The code must be outside of the Function Editor - your theme's functions.php file is a good place.

add_filter('acf/settings/remove_wp_meta_box', '__return_false');

Here are the details from the ACF developer: https://www.advancedcustomfields.com/blog/acf-pro-5-6-0-ui-ux/

cURL Error 60

Here we use the http_api_curl to temporarily disable cURL's peer SSL verification. This allows us to download from HTTPS links that don't have a valid certificate chain. However, it's a potential security issue and only used as an emergency measure while the source site is fixed. The code must be placed outside the Function Editor - such as in your theme's functions.php file.

function curl_error_60_workaround( $handle, $r, $url ) {

    // Disable peer verification to temporarily resolve error 60.
    curl_setopt($handle, CURLOPT_SSL_VERIFYPEER, false);


add_action( 'http_api_curl', 'curl_error_60_workaround', 10, 3 );

Here is a useful site for checking the validity of SSL certificates: https://www.sslshopper.com/ssl-checker.html

Send Export File to FTP Destination

Here we use the pmxe_after_export action to transfer our export file via FTP. We also make use of get_attached_file() to retrieve the export file when WP All Export's Secure Mode is off.

// Parts of this code were based on code from PHP Documentation Group, which is licensed under the Creative Commons Attribution 3.0 License.
// You can read the Creative Commons Attribution 3.0 License here: https://creativecommons.org/licenses/by/3.0/
// Also, hat tip to Daniel Stenberg for some additional code inspiration: https://curl.haxx.se/libcurl/php/examples/ftpupload.html

function wpae_after_export( $export_id ) {

    // Retrieve export object.
    $export = new PMXE_Export_Record();
    // Check if "Secure Mode" is enabled in All Export > Settings.
    $is_secure_export = PMXE_Plugin::getInstance()->getOption('secure');

    // Retrieve file path when not using secure mode.
    if ( !$is_secure_export) {
        $filepath = get_attached_file($export->attch_id);

    // Retrieve file path when using secure mode.                    
    } else {
        $filepath = wp_all_export_get_absolute_path($export->options['filepath']);

    // Path to the export file.
    $localfile = $filepath;

    // File name of remote file (destination file name).
    $remotefile = basename($filepath);
    // Remote FTP server details.
    // The 'path' is relative to the FTP user's login directory.
    $ftp = array(
        'server' => 'enter-hostname-here',
        'user' => 'enter-user-here',
        'pass' => 'enter-password-here',
        'path' => '/enter/path/to/folder/here'

    // Ensure username is formatted properly
    $ftp['user'] = str_replace('@', '%40', $ftp['user']);
    // Ensure password is formatted properly
    $ftp['pass'] = str_replace(array('#','?','/','\\'), array('%23','%3F','%2F','%5C'), $ftp['pass']);
    // Remote FTP URL.
    $remoteurl = "ftp://{$ftp['user']}:{$ftp['pass']}@{$ftp['server']}{$ftp['path']}/{$remotefile}";

    // Retrieve cURL object.
    $ch = curl_init();

    // Open export file.
    $fp = fopen($localfile, "rb");
    // Proceed if the local file was opened.
    if ($fp) {
        // Provide cURL the FTP URL.
        curl_setopt($ch, CURLOPT_URL, $remoteurl);

        // Prepare cURL for uploading files.
        curl_setopt($ch, CURLOPT_UPLOAD, 1);

        // Provide the export file to cURL.
        curl_setopt($ch, CURLOPT_INFILE, $fp);

        // Provide the file size to cURL.
        curl_setopt($ch, CURLOPT_INFILESIZE, filesize($localfile));
        // Start the file upload.

        // If there is an error, write error number & message to PHP's error log.
        if($errno = curl_errno($ch)) {
            if (version_compare(phpversion(), '5.5.0', '>=')) {
                // If PHP 5.5.0 or greater is used, use newer function for cURL error message.
                $error_message = curl_strerror($errno);

            } else {

                // Otherwise, use legacy cURL error message function.
                $error_message = curl_error($ch);

            // Write error to PHP log.
            error_log("cURL error ({$errno}): {$error_message}");

        // Close the connection to remote server.
    } else {

        // If export file could not be found, write to error log.
        error_log("Could not find export file");


add_action('pmxe_after_export', 'wpae_after_export', 10, 1);

Send Email After Import is Complete

Here we use the pmxi_after_xml_import action to email the results of our import to [email protected]. We make use of the WPDB class to retrieve the import stats and wp_mail() to send the email.

function wpai_send_email($import_id)
    // Only send emails for import ID 1.
    if($import_id != "1")
    // Retrieve the last import run stats.
    global $wpdb;
    $table = $wpdb->prefix . "pmxi_imports";

    if ( $soflyyrow = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM `" . $table . "` WHERE `id` = '%d'", $import_id ) ) ) {
        $count = $soflyyrow->count;
        $imported = $soflyyrow->imported;
        $created = $soflyyrow->created;
        $updated = $soflyyrow->updated;
        $skipped = $soflyyrow->skipped;
        $deleted = $soflyyrow->deleted;

    // Destination email address.
    $to = '[email protected]';

    // Email subject.
    $subject = 'Import ID: '.$import_id.' complete';

    // Email message.
    $body = 'Import ID: '.$import_id.' has completed at '. date("Y-m-d H:m:s"). "\r\n" . 'File Records:' .$count."\r\n".'Records Imported:'.$imported."\r\n".'Records Created:'.$created;
    $body .= "\r\n" . 'Records Updated:'. $updated . "\r\n" . 'Records Skipped:' . $skipped . "\r\n" . 'Records Deleted:' . $deleted;

    // Send the email as HTML.
    $headers = array('Content-Type: text/html; charset=UTF-8');
    // Send via WordPress email.
    wp_mail( $to, $subject, $body, $headers );

add_action('pmxi_after_xml_import', 'wpai_send_email', 10, 1);

Append ACF Repeater Data

Here we use the pmxi_saved_post action to append a row to the repeater_text field of our basic_repeater. This workaround is required since our source file has repeater data spread across multiple rows. We use the add_row() function to save our new repeater row.

We use an Existing Items import since our ACF posts already exist. Then we store our value to append in the my_repeater_data custom field:

We tell WP All Import to update the my_repeater_data field:

add_action( 'pmxi_saved_post', 'soflyy_add_data', 10, 3 );

function soflyy_add_data( $id, $xml, $update ) {

    // Parent field name.
    $selector = 'basic_repeater';

    // The field to be appended.
    $subfield1 = 'repeater_text'; 
    // Only continue if my_repeater_data contains a value.
    if ( $value = get_post_meta( $id, 'my_repeater_data', true ) ) {
        // Format data for repeater.
        $row = array( $subfield1 => $value );

        // Add new repeater row.
        add_row( $selector, $row, $id );

    delete_post_meta( $id, 'my_repeater_data' );

Custom Items Loop for WooCommerce Orders Exports

Here we use custom code to modify the format of exported Order items. In this case, we need one product per item element instead of all products in items directly. Here's our Custom XML template:

  <ProductID>{Product ID}</ProductID>
  <ItemCost>{Item Cost}</ItemCost>

The output would look like this:


To achieve the desired format, we need to reconfigure our export template to instead call our custom function:

  [my_output_items({SKU},{Product ID},{Quantity},{Item Cost})]

And disable the automatic use of CDATA tags:

Then our exported format becomes:


We save this code to WP All Export's Function Editor:

function my_output_items( $skus = '', $ids = '', $qty = '', $cost = '' ) {
    // Declare our variable to store the new XML.
    $xml = '';
    // Ensure $skus isn't empty and that it's an array.
    if ( !empty( $skus ) && is_array( $skus ) ) {

        // Process each SKU in the array.
        foreach ( $skus as $key => $value ) {

            // Add the opening item tag.
            $xml .= "**LT**item**GT**";

            // Add the Product ID tags and value.
            $xml .= "**LT**ProductID**GT**" . ( empty( $ids[ $key ] ) ? '' : $ids[ $key ] ) . "**LT**/ProductID**GT**";

            // Add the SKU tags and value.
            $xml .= "**LT**SKU**GT**" . ( empty( $value ) ? '' : $value ) . "**LT**/SKU**GT**";

            // Add the Quantity tags and value.
            $xml .= "**LT**Quantity**GT**" . ( empty( $qty[ $key ] ) ? '' : $qty[ $key ] ) . "**LT**/Quantity**GT**";

            // Add the ItemCost tags and value.
            $xml .= "**LT**ItemCost**GT**" . ( empty( $cost[ $key ] ) ? '' : $cost[ $key ] ) . "**LT**/ItemCost**GT**";

            // Add the closing item tag.
            $xml .= "**LT**/item**GT**";

    // If $skus isn't an array handle it here.
    } else {

        // Add the opening item tag.
        $xml .= "**LT**item**GT**";

        // Add the ProductID tags and value.		
        $xml .= "**LT**ProductID**GT**" . ( empty( $ids ) ? '' : $ids ) . "**LT**/ProductID**GT**";

        // Add the SKU tags and value.
        $xml .= "**LT**SKU**GT**" . ( empty( $skus ) ? '' : $skus ) . "**LT**/SKU**GT**";

        // Add the Quantity tags and value.
        $xml .= "**LT**Quantity**GT**" . ( empty( $qty ) ? '' : $qty ) . "**LT**/Quantity**GT**";

        // Add the ItemCost tags and value.
        $xml .= "**LT**ItemCost**GT**" . ( empty( $cost ) ? '' : $cost ) . "**LT**/ItemCost**GT**";

        // Add the closing item tag.
        $xml .= "**LT**/item**GT**";

    return $xml;


You can learn more about PHP functions in custom XML exports via the "Help" button: 

Count Number of Created Posts

Here we use the pmxi_after_xml_import action to count the number of posts created during the import. Using that number we can selectively perform other tasks.

function count_created_posts( $import_id ){

    // Only count created posts for import ID 1.
    if( $import_id == 1 ){

        // Query the database directly.
        global $wpdb;
        $import = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM `" . $wpdb->prefix . "pmxi_imports` WHERE ID = %d;", $import_id ) );

        // Define the number of created posts.
        $created = $import->created;

        if ( $created > 0 ) {

            // Do something if posts were created during import.


add_action('pmxi_after_xml_import', 'count_created_posts', 10, 1);

Cancel Import If File Empty

Here we use the pmxi_before_xml_import action to cancel the import if less than 100 records are in the file. This is especially important if using the 'Delete posts that are no longer present in your file' option (Step 4 or Manage Imports > Import Settings). When using that option, this keeps the previously imported records intact if the feed fails to return data.

function cancel_empty_import( $importID ) {

    // Retrieve import object. 
    $import = new PMXI_Import_Record(); 

    // Ensure object is valid and check if less than 100
    // records are in the file.	 
    if ( !$import->isEmpty() && $import->count < 100 ) { 
        // Load history file object.
        $history_file = new PMXI_File_Record(); 
        $history_file->getBy( 'import_id', $importID ); 

        // Ensure object is valid.
        if ( !$history_file->isEmpty() ) {  

            // Retrieve import file path.
            $file_to_import = wp_all_import_get_absolute_path( $history_file->path );

            // If file is empty or has less than 100 records,
            // cancel import.
            if ( file_exists( $file_to_import ) and filesize( $file_to_import ) === 0 or $import->count < 100 ) { 

                // Reset import stats to 0.
                $import->set( array( 
                    'queue_chunk_number' => 0,
                    'processing' => 0, 
                    'imported' => 0, 
                    'created' => 0, 
                    'updated' => 0, 
                    'skipped' => 0, 
                    'deleted' => 0, 
                    'triggered' => 0, 
                    'executing' => 0	 

            // Display the reason the import was cancelled.  
            echo 'Import skipped because of empty file / < 100 records';  

            // Stop the import. 

add_action( 'pmxi_before_xml_import', 'cancel_empty_import', 10, 1 );

Delete Source File After Import

Here we use the pmxi_after_xml_import action to delete the source file once imported. This can prevent the same import file from being imported twice. It's also helpful if you're pushing the import file to your server via FTP and need to know when it has been processed.

function delete_import_file( $import_id ) {

    // Retrieve import object.
    $import = new PMXI_Import_Record();
    $import->getById( $import_id );

    // Confirm import object is valid.
    if ( ! $import->isEmpty() ) {
        // Retrieve file information.
        $history_file = new PMXI_File_Record(); 
        $history_file->getBy( 'import_id', $import_id );

        // Confirm file isn't empty. 
        if ( !$history_file->isEmpty() ) {

            // Retrieve file path.  
            $import_file = wp_all_import_get_absolute_path( $history_file->path );

            // Mark file for deletion.
            @unlink( $import_file );


add_action( 'pmxi_after_xml_import', 'delete_import_file', 10, 1 );

Manually Set Import Feed Type

Here we use the wp_all_import_feed_type filter to specify we are importing an XML feed. This may be necessary if your feed URL doesn't end with the type - csv, xml, json, etc. This code must be called outside of WP All Import's Function Editor in a place such as your theme's functions.php file.

function set_feed_type( $type, $url ){

    // Specify Feed URL.
    if ($url == 'https://www.example.com/feedurl'){

        // Set feed type.
        $type = 'xml';


    // If URL doesn't match our feed return the default type.
    return $type;


add_filter( 'wp_all_import_feed_type', 'set_feed_type', 10, 2 );

Combine HTML Elements In XML Without CDATA Tags

Here we use custom code to copy the HTML in our XML file to its own element. This is required to import that HTML since it wasn't encoded or wrapped in CDATA tags. Otherwise, the individual HTML tags will be processed as XML tags by WP All Import. It's best to have a properly formed XML file, but we can fix it with code.

Our HTML is stored in the content element in our file. Our code loads that element and ensures it's not empty. Then keywords in our HTML are replace with their HTML tag equivalents. Finally, we write the HTML to its own element, content_html, so we can use it in our import.

function parse_content($node){

    // Our element containing the HTML to process.
    $result = $node->xpath('content');

    // Ensure a value was returned.
    if (!empty($result[0])) {

        // Replace keywords with HTML equivalents.
        $find_xml = array('section_title','section_content','section', 'texteparagraphe','titreparagraphe');
        $replace_html = array('h1','p','div','p','h2');
        $html = str_replace($find_xml, $replace_html, $result[0]->asXML());

        // Save the HTML to its own 'content_html' element.
        $node->addChild('content_html', $html);

    return $node;


add_filter('wpallimport_xml_row', 'parse_content', 10, 1);

Modify Record Before It's Imported

Here we use the wpallimport_xml_row filter to check the Blue element for a value. If it has one, we create an element named Color and set it to Blue. We do the same for the Red element. This works for all file types, not just XML.

Here's our starting CSV file:

Large Blue HatLargeYes
Small Red HatSmallYes
Medium Red and Blue HatMediumYesYes

Here's the CSV equivalent of how WP All Import sees it after our code runs:

Large Blue HatLargeYesBlue
Small Red HatSmallYesRed
Medium Red and Blue HatMediumYesYesBlueRed

Doing this allows the {color} XPath to return all of the Color values separated by commas. Here's what's returned for each row above:

function add_property_type( $node ) {
    // Element to be located
    $blue = $node->xpath( 'blue[1]' );
    // Check if Blue element has value
    if ( ! empty( $blue ) ) {
        if ( ! empty( $blue[0]->__toString() ) ) {
            // add Color node with value 'Blue'
            $node->addChild( 'color', 'Blue' );
    // Element to be located
    $red = $node->xpath( 'red[1]' );
    // Check if Red element has value
    if ( ! empty( $red ) ) {
        if ( ! empty( $red[0]->__toString() ) ) {
            // add Color node with value 'Red'
            $node->addChild( 'color', 'Red' );
    return $node;

add_filter( 'wpallimport_xml_row', 'add_property_type', 10, 1 );

Append Data To A Custom Field Instead Of Overwriting

Here we are using the pmxi_saved_post action to append an additional value to the your_meta_key field. This allows importing values from multiple rows in our file to a single post's custom field. We must import the new value to the _temp custom field first:

The field you append must not be updated by WP All Import or the original value will be lost (Step 4 or Manage Imports > Import Settings):

function custom_field_append($id)
    // Get the current value of your meta key.
    $value = get_post_meta($id, 'your_meta_key', true);

    // Get the temp value we imported.
    $temp = get_post_meta($id, '_temp', true);

    // Append the temp value to the original value and
    // save it to your meta key
    update_post_meta($id, 'your_meta_key', $value . $temp);

    // Delete the temp field.
    delete_post_meta($id, '_temp');


add_action('pmxi_saved_post', 'custom_field_append', 10, 1);

Update Second Custom Field If First Field Has Specified Value

Here we use the pmxi_saved_post action, get_post_meta(), and update_post_meta() to only update _my_custom_field if _my_update_check is set to 'yes'. The new value is stored in _my_new_value during import and copied to _my_custom_field as needed.

function conditional_update($id)
    // Retrieve field to check.
    $check = get_post_meta($id, '_my_update_check', true);

    // Check field's value and update _my_custom_field if 'yes'.
    if ($check === 'yes') {

        // Retrieve the new value.
        $new_value = get_post_meta($id, '_my_new_value', true);

        // Save the new value to the target field.
        update_post_meta($id, '_my_custom_field', $new_value);

add_action('pmxi_saved_post', 'conditional_update', 10, 1);

Use Parent Prices For Variations

Here we use the pmxi_saved_post action to assign the _parent_price value to each variation. This is necessary because our file only lists parent prices. The get_post_meta() function is used to retrieve _parent_price. Then wc_get_product() is used to get the product's object

If you're trying to use a price update import and only have a file with prices for the parent products, you'll need some custom code to apply those prices to the variations.

Be sure to set the price to a custom field named '_parent_price'. Then the code below should be helpful:

function my_set_price($post_id) {

    // Get the parent price.
    $parent_price = get_post_meta($post_id, "_parent_price", true);

    // Get the parent product object.
    $parent = wc_get_product($post_id);

    // Check if it's a variable product.
    if( $parent && 'variable' == $parent->get_type() ){

        // Get product's variations.
        $variations = $parent->get_children();
        // Loop through the variations and set the price
        foreach ($variations as $variation) {
            $single_variation = wc_get_product($variation);
	    $variation_id = $single_variation->get_variation_id();
	    update_post_meta($variation_id, "_price", $parent_price);
	    update_post_meta($variation_id, "_regular_price", $parent_price);
    delete_post_meta($post_id, "_parent_price");

add_action( 'pmxi_saved_post', 'my_set_price', 10, 1 );

Access Data Outside of Element Chosen on Step 2

Here we use the pmxi_before_xml_import action to add Status as a child to each Procurement element, allowing it to be selected on Step 3. Note that this workaround only works for XML imports.

Here is the original XML structure:


    <!-- Status element is outside of Procurement elements -->
                Description 2

On Step 3 we can't select Status since it's not a child of the element chosen on Step 2, Procurement:

After the code runs, the file looks like this:


            <!-- Status has been copied to each Procurement element -->
                Description 2

            <!-- Status has been copied to each Procurement element -->

Making Status available on Step 3:

Note: Status won't show up in the list until the import first runs. But, it can be typed in manually and used: {Status[1]}

function wpai_pmxi_before_xml_import( $importID ) {

    // Retrieve import object.
    $import = new PMXI_Import_Record();
    $import->getById( $importID );

    // Ensure import object is valid.
    if ( ! $import->isEmpty() ) {

        // Retrieve history file object.
        $history_file = new PMXI_File_Record();
        $history_file->getBy( 'import_id', $importID );

        // Ensure history file object is valid.
        if ( ! $history_file->isEmpty() ) {

            // Retrieve import file path.
            $file_to_import = wp_all_import_get_absolute_path( $history_file->path );

            // Load import file as SimpleXml.
            $file = simplexml_load_file( $file_to_import );

            // Check if Status is a child of Procurement.
            $query = $file->xpath( "//Apartment/Procurements[1]/Procurement[1]/Status[1]" );
            if ( ! empty( $query ) ) {

                // If it is, do nothing.


            // Get Status value.
            $iquery = $file->xpath( "//Apartment/Status[1]" );

            // Ensure value isn't empty.
            if ( ! empty( $iquery ) ) {

                // Value of status as string.
                $status = $iquery[0]->__toString();

                // Target path.
                $new_query = $file->xpath( "./Procurements/Procurement" );

                // Ensure path is valid.
                if ( ! empty( $new_query ) ) {

                    // Process each Procurement element.
                    foreach ( $new_query as $record ) {

                        // Ensure this element doesn't have Status.
                        if ( ! isset( $record->Status ) ) {

                        // Add {Status[1]} as child node.
                        $record->addChild( 'Status', $status );


                    // Save updated file.
                    $updated_file = $file->asXML( $file_to_import );


add_action( 'pmxi_before_xml_import', 'wpai_pmxi_before_xml_import', 10, 1 );

Map Values to Element Name Based on Value

Here we use the wpallimport_xml_row filter to map our boolean fields to a new element with string values. This is needed since our file contains one element for each feature with a value of either 0 or 1. All fields with a value of 1 must have their name copied to a new element for import.

Here's an example of our file:


We can handle this mapping with some custom code. The end result represented as CSV is below:

Villa11000Pool, Patio
House01101Patio, Basement, Air Conditioning
Apartment10001Pool, Air Conditioning

That allows using the {mycustomfeatures[1]} element in our import configuration as needed (Step 3 or Manage Imports > Edit Import).

function add_feature_node( $node ) {

    // Our mappings from element name to string.
    $fields = array(
        "pool"              => "Pool",
        "patio"             => "Patio",
        "basement"          => "Basement",
        "deck"              => "Deck",
        "airconditioning"   => "Air Conditioning"

    // Process each element.
    foreach ( $fields as $key => $field ) {

        // Retrieve the field's value.
        $result = $node->xpath( $key . '[1]' );

        // Check if element doesn't exist or contains an empty value
        if ( !isset( $result[0] ) || empty( $result[0]->__toString() ) ) {

            // If so, don't list that feature.
            unset( $fields[ $key ] );



    // Add the comma separated features to our new element.
    $node->addChild( 'mycustomfeatures', implode( ",", $fields ) );

    return $node;

add_filter( 'wpallimport_xml_row', 'add_feature_node', 10, 1 );

Match By SKU When The SKUs On Your Site Contain Extra Characters

Here we use the WPDB class' get_var() method to retrieve the products matching our SKUs. Using REPLACE we ignore an unwanted slash that's used in each SKU on the site, but not in our file: my/sku vs mysku

function get_product_by_sku( $sku ) {

    // Access WPDB object.
    global $wpdb;

    // Match by our files SKU and ignore slashes in the database.
    $product_id = $wpdb->get_var( $wpdb->prepare( "SELECT post_id FROM $wpdb->postmeta WHERE meta_key='_sku' AND REPLACE(meta_value, '/', '')='%s' LIMIT 1", $sku ) );

    // If a match was found return its ID.
    if ( $product_id ) return $product_id;

    return null;

We call our function on Step 4 of an Existing Items import. Our files SKU element is passed as the only parameter:

Import ACF Post2Post Plugin Bidirectional Fields

Here we use the pmxi_acf_custom_field filter to allow the ACF Post2Post plugin to link Bidirectional fields.

function import_set_bidiretional_fields( $value, $post_id, $name ) {

	// Get ACF field object by name.
	$field = get_field_object($name, $post_id);

	// Take no action if field is empty.
	if( empty($field) ) {
		return $value;

	// Apply update_value filter to trigger acf-post2post plugin.
	if ( $field['type'] == 'relationship' ) {
		$value = apply_filters( "acf/update_value/type={$field['type']}", $value, $post_id, $field );

	// Return the original field value.
	return $value;

add_filter( 'pmxi_acf_custom_field', 'import_set_bidiretional_fields', 10, 3 );

Import Images to WP User Avatar Plugin

Here we use the pmxi_saved_post action to save images to the WP User Avatar plugin. Using get_user_meta() we retrieve the avatar URL from the custom field. The media_sideload_image() function saves the image to WordPress. We make use of add_action() and remove_action() to call my_get_id() which links the avatar to the User with update_user_meta(). This works when importing Users or Customers.

The link to each image must be saved in wp_user_avatar when importing:

function my_import_avatar_image ($id) {

    // Get the URL for the avatar image.
    $image_url = get_user_meta($id, "wp_user_avatar", true);

    // Set the my_get_id function to run when attachments added.
    add_action( 'add_attachment', 'my_get_id' );

    // Sideload the avatar image.
    $new_url = media_sideload_image($image_url, $id, null, "src");

    // Remove the my_get_id function from add_attachment action.
    remove_action( 'add_attachment', 'my_get_id' );

function my_get_id( $att_id ) {

    // Get the post object for the attachment.
    $img = get_post( $att_id );

    // Get the attachment's parent.
    $user = $img->post_parent;

    // Set wp_user_avatar to the image's ID.
    update_user_meta( $user, "wp_user_avatar", $att_id );

add_action('pmxi_saved_post', 'my_import_avatar_image', 10, 3);

This code has been derived from the following example: http://wordpress.stackexchange.com/a/46365/108630

Import Data to Custom Database Table

Here we use the pmxi_saved_post action to save data to a custom database table while importing to a defined post type. The value for the custom database table is saved to _your_temp_field. Then retrieved using get_post_meta() and written to the database using the wpdb object. To cleanup, we remove the temporary custom field using delete_post_meta().

function save_data_to_custom_database_table($id) 
    // Make wpdb object available.
    global $wpdb;

    // Retrieve value to save.
    $value = get_post_meta($id, '_your_temp_field', true);

    // Define target database table.
    $table_name = $wpdb->prefix . "your_table_name";

    // Insert value into database table.
    $wpdb->insert($table_name, array('post_id' => $id, 'column_name' => $value), array('%s','%s'));

    // Delete temporary custom field.
    delete_post_meta($id, '_your_temp_field');

add_action('pmxi_saved_post', 'save_data_to_custom_database_table', 10, 1);

Include Featured Image in Product Gallery

Here we use the wp_all_import_variable_product_imported action to prepend the featured image to the product's gallery. We use get_post_meta() to retrieve current gallery images and get_post_thumbnail_id()for the featured image. It's added to the list of gallery images, duplicates are removed, then update_post_meta() saves the updates.

function copy_featured_img_to_gallery($post_id){
    // Retrieve the Product Gallery image IDs.
    $gallery = explode(",",get_post_meta($post_id, "_product_image_gallery", true));

    // Add the Featured Image to the Gallery IDs.
    array_unshift($gallery, get_post_thumbnail_id( $post_id ));

    // Ensure no image IDs are duplicated in the Gallery.
    $gallery = array_unique($gallery);

    // Save the updated list of Gallery image IDs.
    update_post_meta($post_id, "_product_image_gallery", implode(",",$gallery));


add_action('wp_all_import_variable_product_imported', 'copy_featured_img_to_gallery', 10, 1);

List One 'Any Attribute' Variation for Variable Products

Here we use the wp_all_import_variable_product_imported action to delete all but one variation and set its attributes to 'any'. This may be useful if you have a large number of Attribute options, but each variation's details ( price, stock, etc ) are all the same and don't need to be tracked separately.

The updated products look something like below:

function set_attributes_to_any( $post_parent ){
    global $wpdb;
    $table = $wpdb->posts;

    // Retrieve all variations for this product.
    $variations = $wpdb->get_results("SELECT * FROM $table WHERE post_parent = " . $post_parent );
    if ( ! empty($variations)){

        // Keep one variation.
        $empty_variation = array_shift($variations);
        if ( ! empty($variations)){

            // Delete all other variations for this product.
            foreach($variations as $variation){
        $table = _get_meta_table('post');

        // Find all Attributes set for our remaining variation.
        $post_meta = $wpdb->get_results("SELECT meta_key, meta_value FROM $table WHERE post_id = " . $empty_variation->ID . " AND meta_key LIKE 'attribute%';" );

        // Make Attributes show as 'any'.
        if ( ! empty($post_meta)){
            foreach ($post_meta as $meta) {
                update_post_meta($empty_variation->ID, $meta->meta_key, '');

add_action( 'wp_all_import_variable_product_imported', 'set_attributes_to_any', 10, 1 );

Only Import Existing Taxonomy Terms

Here we use the pmxi_single_category filter to prevent WP All Import from creating new taxonomy terms. Only existing terms will be assigned to records during import. This works for all taxonomies - categories, tags, etc.

function dont_create_terms( $term_into, $tx_name ) {
    // Check if term exists, checking both top-level and child
    // taxonomy terms. 
    $term = empty($term_into['parent']) ? term_exists( $term_into['name'], $tx_name, 0 ) : term_exists( $term_into['name'], $tx_name, $term_into['parent'] );

    // Don't allow WP All Import to create the term if it doesn't
    // already exist.
    if ( empty($term) and !is_wp_error($term) ) { 
        return false;

    // If the term already exists assign it.
    return $term_into;


add_filter( 'pmxi_single_category', 'dont_create_terms', 10, 2 ); 

Do Not Remove Certain Categories During Import

Here we use the wp_all_import_set_post_terms filter and get_the_terms() to ensure the 'Featured' category remains for any product that had it manually assigned. This allows WP All Import to add and remove other categories without losing our manual assignments.

function dont_remove_featured_category( $term_taxonomy_ids, $tx_name, $pid, $import_id ) {

    // Only check Product Categories.
    if ( $tx_name == 'product_cat' ){

        // Retrieve all currently assigned categories.
        $txes_list = get_the_terms($pid, $tx_name);

        // Do nothing if no categories are set.
        if ( ! empty($txes_list) ){
           foreach ($txes_list as $cat){

                // If category name is 'Featured' add it to import.
                if ($cat->name == 'Featured'){
                    $term_taxonomy_ids[] = $cat->term_taxonomy_id;

    // Return the updated list of taxonomies to import.
    return $term_taxonomy_ids;


add_filter( 'wp_all_import_set_post_terms', 'dont_remove_featured_category', 10, 4 );

Use Custom Delimiter When Retrieving Multiple Values

Here we use the wp_all_import_multi_glue filter to separate multiple returned values with pipes instead of commas.

Here's an example CSV file:

Red and Blue ShirtMediumRedBlue

If we retrieve both Color elements using a single XPath statement such as {Color}, by default they will be returned comma-delimited: Red, Blue

The example code will change that comma to a pipe: Red|Blue

add_filter( 'wp_all_import_multi_glue', function( $delimiter ){
    return '|';
}, 10, 1 );

Only Update Custom Field If New Value Is Not Empty

Here we use the pmxi_custom_field filter to only overwrite _my_custom_field if the new value isn't empty. This is helpful if the field should always have a value and your file doesn't always provide it.

function keep_existing_if_empty($value, $post_id, $key, $original_value, $existing_meta, $import_id)
    // Only check _my_custom_field.
    if ($key == '_my_custom_field') {

        // Check if it has a value.
        if (empty($value)) {

            // If empty, use the existing value.
            $value = isset($existing_meta[$key][0]) ? $existing_meta[$key][0] : $value;


    return $value;


add_filter('pmxi_custom_field', 'keep_existing_if_empty', 10, 6);

Only Update Custom Field If It's Currently Empty

Here we use the pmxi_custom_field filter to only update _my_custom_field if it's currently empty. This allows for making manual changes to fields that won't be overwritten during import.

function update_existing_if_empty($value, $post_id, $key, $existing_meta, $import_id) 
    // Only process import ID 5.
    if ($import_id == 5) {

        // Only check _my_custom_field.
        if ($key == '_my_custom_field') {

            // Check if field exists and if it's empty.
            if (!isset($existing_meta[$key][0]) || empty($existing_meta[$key][0])) {

                // Use existing value only if currently empty.
                $value = $existing_meta[$key][0];


    return $value;


add_filter('pmxi_custom_field', 'update_existing_if_empty', 10, 5);

Only Update ACF Fields If Imported Values Are Not Empty

Here we use the pmxi_acf_custom_field filter and get_post_meta() to prevent writing empty values to existing ACF fields.

function wp_all_import_pmxi_acf_custom_field( $value, $pid, $name ) { 

    // Retrieve existing ACF field value.
    $existing = get_post_meta( $pid, $name, true );

    if ( empty( $value ) ) { 

        // If the new value is empty, use existing value.
        $value = $existing;


    return $value; 


add_filter( 'pmxi_acf_custom_field', 'wp_all_import_pmxi_acf_custom_field', 10, 3 );

Filter Posts by Date

Here we use the wp_all_import_is_post_to_create and wp_all_import_is_post_to_update filters to create or update records based on a date in our file. Such code is necessary since the visual filters won't work with date fields.

We retrieve the date from column_4 in our file. Then we specify 2018-10-10 as our target date before converting it to a Unix timestamp. We include those newer than our target date in the import.

function filter_by_date( $continue_import, $data, $import_id ) {

    // Only apply to import ID 3
    if ( $import_id == 3 ) {

        // Change 'column_4' to your file's date field.
        $date_in_file = strtotime( $data['column_4'] );

        // Change '2018-10-10' to your real target date.
        $date = "2018-10-10";

        // Convert specified date to Unix timestamp.
        $target_date = strtotime($date);

        // Compare file date with target date.
        if ( $date_in_file >= $target_date ) {

            // Create or update record if file date >= target date.
            return true;

        } else {

            // Do not create or update the record otherwise.
            return false;


    // Take no action for other import IDs.
    return $continue_import;


// Apply the code to posts set to be created.
add_filter('wp_all_import_is_post_to_create', 'filter_by_date', 10, 3);

// Apply the code to posts set to be updated.
add_filter('wp_all_import_is_post_to_update', 'filter_by_date', 10, 3);

Only Create Post If Custom Field Value Is Unique

Here we use the wp_all_import_is_post_to_create filter to only create records if they have a unique value for my_custom_field. This prevents duplicates when importing overlapping records using multiple 'New Items' imports. We use WP_Query to check for existing posts with the same value.

function create_only_if_unique_custom_field( $continue_import, $data, $import_id )
    // Only run for import ID 1.
    if ($import_id == 1) {

        // The custom field to check.
    	$key_to_look_up = "my_custom_field";

        // The value to check where 'num' is the element name.
    	$value_to_look_up = $data['num'];

        // Prepare the WP_Query arguments
        $args = array (

            // Set the post type being imported.
            'post_type'  => array( 'post' ),

            // Check our custom field for our value.
            'meta_query' => array(array(
            'key'        => $key_to_look_up,
            'value'      => $value_to_look_up,

        // Run the query and do not create post if custom
        // field value is duplicated.
	$query = new WP_Query( $args );
	return !($query->have_posts());

    } else {

        // Take no action if a different import ID is running.
       return $continue_import;


add_filter('wp_all_import_is_post_to_create', 'create_only_if_unique_custom_field', 10, 3);

Do Not Create Products With Duplicate SKU

Here we use the wp_all_import_is_post_to_create filter to avoid creating products with duplicate SKUs. This helps when the same product may appear in multiple 'New Items' imports. For fields other than SKU use this snippet.

function dont_duplicate_skus( $continue_import, $data, $import_id ) {
    // Get the SKU from the import file.
    // Change 'sku' to the column name that contains your SKU.
    $sku = $data['sku'];
    // Enable access to global wpdb object.
    global $wpdb;

    // Check if the SKU already exists.
    $product_id = $wpdb->get_var( $wpdb->prepare( "SELECT post_id FROM $wpdb->postmeta WHERE meta_key='_sku' AND meta_value='%s' LIMIT 1", $sku ) );
    if ( $product_id ) {

        // If product ID exists then skip importing.
        return false;

    } else {

        // Else, import the product.
        return true;


add_filter('wp_all_import_is_post_to_create', 'dont_duplicate_skus', 10, 3);

Only Update Posts with a Specific Status

Here we use the wp_all_import_is_post_to_update filter to only update records that are set to 'draft'. We use get_post_status() to retrieve the current status of each record to determine if it should be updated.

function my_check_post_status ($continue_import, $post_id, $data, $import_id ) {

    // Only process import ID 1.
    if ($import_id == 1) {

	// Retrieve post status.
	$my_post_status = get_post_status($post_id);

	// Check if status is set to draft.
        if ($my_post_status == "draft") {

            // Tell WP All Import to update post if it's draft.
            return true;

        // Do not update post if it's not set to draft.
        return false;

    else {

        // Do nothing if it's not our specified import ID.
        return $continue_import;


add_filter( 'wp_all_import_is_post_to_update', 'my_check_post_status', 10, 4 );

Only Update Products If The Stock Status Changed

Here we use the wp_all_import_is_post_to_update filter and get_post_meta to compare each product's _stock_status with the status in our file. If they match the product won't be updated.

function only_update_if_stock_status_changed ( $continue_import, $post_id, $data, $import_id ) {

    // Only run for import ID 1.
    if ($import_id == 1) {

        // Get stock status from the import file.
        // Our file element is named 'stock_status'.
        $import_stock_status = $data["stock_status"];

        // Retrieve product's current stock status.
        $woo_stock_status = get_post_meta($post_id, "_stock_status", true);

        // Check if import_stock_status and
        // woo_stock_status match.
        if ($import_stock_status !== $woo_stock_status) {

            // Update the product if they don't.
            return true;

            // Otherwise, don't update the product.
            return false;

    // Do nothing if it's not our target import.
    return $continue_import;


add_filter( 'wp_all_import_is_post_to_update', 'only_update_if_stock_status_changed', 10, 4 );

Only Update Product If Price Was Not Manually Changed

Here we use the wp_all_import_is_post_to_update filter and get_post_meta() to check if _last_imported_price matches the currently set price. This prevents updating the product if the price was manually changed. The _last_imported_price custom field must be set to your price element:

function do_not_update_if_price_hand_modified( $continue_import, $post_id, $data, $import_id ) {

    // Only run for import ID 1.
    if ($import_id == 1) {

        // Retrieve last imported price.
        $imported_price = get_post_meta($post_id, "_last_imported_price", true);

        // If price isn't greater than zero update the product.
        if ($imported_price <= 0) 
            return true;

        // Update product if the new prices matches that 
        // last imported.
        if ($imported_price === get_post_meta($post_id, "_price", true)) 
            return true;

        // Do not update product otherwise.
        return false;

    else {

        // Take no action if it's not our import ID.
        return $continue_import;


add_filter( 'wp_all_import_is_post_to_update', 'do_not_update_if_price_hand_modified', 10, 4 );

Delete Orphaned Variations When Deleting Previously Imported Products

Here we use the wp_all_import_is_post_to_delete filter to only delete variations created by WP All Import that have been orphaned. This is needed as our import file won't always list all of the variations. Also, we need to make sure we don't leave orphaned variations after deleting a parent product. The 'Delete products that are no longer present in your file' option must be checked or the code does nothing (Manage Imports > Import Settings).

function is_post_to_delete_orphans($to_delete, $pid, $import)
    // Only process products.
    if ($import->options['custom_type'] == 'product') {

        // Retried product post object.
        $post_to_delete = get_post($pid);

        // Check if parent or variation.
        switch ($post_to_delete->post_type) {

            // Mark to delete if parent.
            case 'product':
                $to_delete = true;

            // Process variations.
            case 'product_variation':
                $parent_product = get_post($post_to_delete->post_parent);

                // Only delete variation if orphaned.
                $to_delete = empty($parent_product) ? true : false;

    return $to_delete;


add_filter('wp_all_import_is_post_to_delete', 'is_post_to_delete_orphans', 11, 3);

Prevent Some Posts From Being Deleted By WP All Import

Here we use the wp_all_import_is_post_to_delete filter and get_post_meta() to avoid deleting records that have a do_not_delete custom field. This allows manually specifying that some products should not be deleted even if they're missing from your file.

It only works when using the 'Delete products that are no longer present in your file' option (Step 4 or Manage Imports > Import Settings).

function is_post_to_delete_on_meta($is_post_to_delete, $pid, $import)
    // Check if a custom field named 'do_not_delete' exists and
    // do not delete record if it does.
    return $var =  ( get_post_meta($pid, 'do_not_delete', true) ) ? false : true;


add_filter('wp_all_import_is_post_to_delete', 'is_post_to_delete_on_meta', 10, 3);

Save Images To Your Theme's or Plugin's Custom Gallery

Here we use the pmxi_gallery_image action to add images to a few common gallery field configurations. We use get_post_meta() to retrieve the existing gallery fields and update_post_meta() to save the new updated galleries. When working with named key arrays we use wp_get_attachment_image_src() to retrieve the image URLs.

We import our images using the Images section (Step 3 or Manage Imports > Edit Import).

Named Key Arrays ([image_id_1 => "image_url_1"]):

function gallery_id_url($post_id, $att_id, $filepath, $is_keep_existing_images = '')

    // The custom field used by the gallery.
    $key = '_gallery';

    // The image size to list in the gallery.
    $size = 'full';     

    // Retrieve the current gallery value.
    $gallery = get_post_meta($post_id, $key, TRUE);

    // If it's empty declare a new array.
    if (empty($gallery)) {
        $gallery = array();

    // Check if image is already in the gallery.
    if (!isset($gallery[$att_id])) {

        // If not, retrieve the image's URL.
        $src = wp_get_attachment_image_src($att_id, $size);

        // Add the image ID and URL to our gallery.
        $gallery[$att_id] = $src[0];

        // Save the gallery.
        update_post_meta($post_id, $key, $gallery);


add_action('pmxi_gallery_image', 'gallery_id_url', 10, 4);

Numeric Array Keys ([0 => image_id_1, 1 => image_id_2]):

function gallery_n_id($post_id, $att_id, $filepath, $is_keep_existing_images = '')
    // The custom field used for the gallery.
    $key = '_gallery';  

    // Retrieve existing gallery values.
    $gallery = get_post_meta($post_id, $key, TRUE);

    // If gallery is empty declare a new array.
    if (empty($gallery)) {
        $gallery = array();

    // Check if the image is in the gallery.
    if (!in_array($att_id, $gallery)) {

        // If not, add it.
        $gallery[] = $att_id;

        // Save the new gallery.
        update_post_meta($post_id, $key, $gallery);


add_action('pmxi_gallery_image', 'gallery_n_id', 10, 4);

Individual Post Meta (custom field) per image:

function gallery_meta_id($post_id, $att_id, $filepath, $is_keep_existing_images = '')
    // The custom field used for the gallery.
    $key = '_gallery'; // Edit this: Set meta key for gallery array here

    // Retrieve the current gallery values.
    $result = get_post_meta($post_id, $key, FALSE);

    // Check if the image is already in the gallery.
    if (is_array($result)) {

        // If not, add it.
        if (!in_array($att_id, $result)) {
            add_post_meta($post_id, $key, $att_id);


add_action('pmxi_gallery_image', 'gallery_meta_id', 10, 4);

Comma-separated image IDs (23,25,31):

function gallery_ids_in_string($post_id, $att_id, $filepath, $is_keep_existing_images = '')
    // The custom field used by gallery.
    $key = '_gallery';

    // The separator to use between each ID.
    $separator = ",";

    // Retrieve the current values in the gallery field.
    $gallery = get_post_meta($post_id, $key, true);

    // Ensure gallery is valid.
    if (is_string($gallery) || is_empty($gallery) || ($gallery == false)) {

        // Split value into array.
        $gallery = explode($separator, $gallery);

        // Add image if it's not in the gallery.
        if (!in_array($att_id, $gallery)) {

            // Ensure array doesn't start with empty value.
            if ($gallery[0] == '') unset($gallery[0]);

            // Add image ID to array.
            $gallery[] = $att_id;

            // Save updated gallery field.
            update_post_meta($post_id, $key, implode($separator, $gallery));


add_action('pmxi_gallery_image', 'gallery_ids_in_string', 10, 4);

Save Imported Images To A Custom Folder

Here we use the wp_all_import_images_uploads_dir filter to save our images to the uploads/customfolder directory. This can prevent saving a large number of images in a single folder when they're imported the same day.

function wpai_set_custom_upload_folder($uploads, $articleData, $current_xml_node, $import_id) {

    // Change our upload path to uploads/customfolder.
    $uploads['path'] = $uploads['basedir'] . '/customfolder';
    $uploads['url'] = $uploads['baseurl'] . '/customfolder';    
    // Check if the target directory exists.
    if (!file_exists($uploads['path'])) {

        // If not, create the directory.
        mkdir($uploads['path'], 0755, true);


    return $uploads;


add_filter('wp_all_import_images_uploads_dir', 'wpai_set_custom_upload_folder', 10, 4);

Apply wp_handle_upload Filter to Uploaded Files

Here we use wp_all_import_handle_upload to apply the wp_handle_upload filter to all imported attachments and images. It's normally fired when uploading files to WordPress. However, it's disabled during imports to increase speed. Firing this filter allows plugins such as Imsanity to work for imported files.

add_filter('wp_all_import_handle_upload', function( $file ){

    return apply_filters( 'wp_handle_upload', $file, 'upload');

}, 10, 1);

Set Exported Order Status

Here we use the pmxe_exported_post action to mark exported Orders as completed. We use the WC_Order method update_status() which can set any desired order status.

function set_order_status_on_export($post_id, $exportObject)

    // Retrieve export ID. 
    $export_id = ( isset( $_GET['id'] ) ? $_GET['id'] : ( isset( $_GET['export_id'] ) ? $_GET['export_id'] : 'new' ) );

    // Only run for export 1.
    if ($export_id == "1") {

        // Retrieve Order object.
        $order = new WC_Order($post_id);

        // Set Order status to completed.
        $order->update_status('completed', 'export_completed'); 


add_action('pmxe_exported_post', 'set_order_status_on_export', 10, 2);

Filter Variations by Parent Status When Exporting to GMC

Here we use the wp_all_export_csv_rows filter to only export variations whose parents are not set to 'draft'. This is especially useful for Google Merchant Center exports as they apply Status filters directly to variations by default.

function exclude_drafts_from_gmc_export($articles, $options, $export_id) {

    // Only filter GMC exports.
    if ($options["xml_template_type"] == "XmlGoogleMerchants") {

        // Process every exported product.
        foreach ($articles as $key => $article) {

            // If IDs aren't set to be exported, do nothing.
            if ( ! empty($article['id']) ) {
                $post_id = $article['id'];
                $parent_id = wp_get_post_parent_id($post_id);

                // Check the parent's Status.
                if ( get_post_status($parent_id) == "draft" ) {

                    // Don't export variation if parent is 'draft'.


    return $articles;
add_filter('wp_all_export_csv_rows', 'exclude_drafts_from_gmc_export', 10, 3);

Limit Records Exported to CSV by Date

Here we use the wp_all_export_csv_rows filter to only export records where _my_time is older than tomorrow. This helps when you need more control than the visual filters offer. The example works for CSV, Excel, and Google Merchant Center exports only. You must include _my_time when configuring the export.

function filter_export_by_date($articles, $options, $export_id)

    // Process each record to be exported.
    foreach ($articles as $key => $article) {

        // Set target date as Unix timestamp.
        $target_date = strtotime("tomorrow");

        // Convert _my_time element's time to Unix timestamp.
        $my_time = strtotime($article["_my_time"]);

        // Check if $my_time is older that $target_date
        if ($my_time < $target_date) {

            // Do not export records older than $target_date.


    // Return updated array of records to export.
    return $articles;


add_filter('wp_all_export_csv_rows', 'filter_export_by_date', 10, 3);

Limit Number of Records Exported When Exporting to XML

Here we use the wp_all_export_xml_rows filter to limit the number of records exported to XML. We use get_posts() to handle retrieving only the records we want to export. Our example further limits exported records to those with a specific taxonomy term set.

add_filter('wp_all_export_xml_rows', function($shouldExport, $record, $exportOptions, $exportId) {

    // The Export ID to be filtered.
    $exportToApplyFilterTo = 3;

    // The number of records to be exported.
    $lastNumberOfPostsToExport = 200;

    // Do nothing if it's not our defined export ID running.
    if($exportId == $exportToApplyFilterTo) {

        // Retrieve the records we want to export.
        $posts = get_posts(
            'numberposts' => $lastNumberOfPostsToExport,

            // Only export published records.
            'post_status' => 'publish',

            'tax_query' => array(

                    // The taxonomy to check.
                    'taxonomy' => 'property_status',

                    // Search by Term ID.
                    'field' => 'term_id',

                    // The term_id of the taxonomy term.
                    'terms' => 575,
                    'include_children' => false
            // END TAXONOMY QUERY

            'fields' => 'ids',
            'order' => 'desc',
            'orderby' => 'ID',
            'post_type' => $record->post_type

        // Only export records found by get_posts() above.
        if(in_array($record->ID, $posts)) {
            return true;
        } else {
            return false;

    // Do nothing if our defined export ID isn't running.
    return $shouldExport;
}, 11, 4);

Append Custom Line Endings to Export File

Here we use the wp_all_export_after_csv_line filter to append a newline character to each row in our CSV file during export.

function custom_line_ending( $stream, $export_id ){

    // Define character for line ending.
    fwrite( $stream, "n" );
    return $stream;


add_filter( 'wp_all_export_after_csv_line', 'custom_line_ending', 10, 2 );

Programmatically Add Elements To XML Exports

Here we use the wp_all_export_additional_data filter to add an element named created_at to our exported XML file.

function wpae_additional_data_field($add_data, $options)
    // Add 'created_at' element and set its value to
    // the current date and time.
    $add_data['created_at'] = date("Y-m-d H:i:s");

    return $add_data;


add_filter('wp_all_export_additional_data', 'wpae_additional_data_field', 10, 2);

Move Generated File After Export

Here we use the pmxe_after_export action to move our export file to /wp-content/uploads. The get_attached_file() function allows us to retrieve the export file path when Secure Mode is enabled. The WordPress uploads directory is located using wp_get_upload_dir().

function move_export_file ($export_id, $exportObj){
    // Get WordPress's upload directory.
    $upload_dir = wp_get_upload_dir();
    // Check whether "Secure Mode" is enabled in All Export > Settings
    $is_secure_export = PMXE_Plugin::getInstance()-> getOption('secure');

    if ( !$is_secure_export ) {

        // Get filepath when 'Secure Mode' is off.
        $filepath = get_attached_file($exportObj->attch_id);
    } else {

        // Get filepath with 'Secure Mode' on.
        $filepath = wp_all_export_get_absolute_path($exportObj->options['filepath']);

    // Get the filename.
    $filename = basename( $filepath );
    // Move export file into /wp-content/uploads.
    rename( $filepath,  $upload_dir['basedir'] . DIRECTORY_SEPARATOR . $filename );


add_action('pmxe_after_export', 'move_export_file', 10, 2);

Run Cron Export With Only A Trigger Cron Job and 1 Processing Cron Job

Here we use the pmxe_after_iteration action to avoid the need for multiple 'processing' cron jobs. However, if something fails on one of the iterations the export will hang. It will also hang if the process calling the 'trigger' or 'processing' URL closes the connection before the export is complete.

function wpae_continue_cron( $export_id, $exportObj ) {

    // Only run for export ID 12.
    if ( $export_id == '12' ) {

        // Import 12's 'processing' URL. 
        $cron_processing_url = 'http://lame-addax-cat.w6.wpsandbox.pro/wp-cron.php?export_key=g99mni1B6Kpu&export_id=12&action=processing';

        // Redirect the connection to the 'processing' URL.
        header( "Location: " .  $cron_processing_url . "" );

add_action( 'pmxe_after_iteration', 'wpae_continue_cron', 10, 2 );

Set Password for Imported Posts

Here we use the pmxi_saved_post action to set the password for each post as it's imported. In this case our posts all have a different password, each defined in the import file:

Every time a post is successfully imported or updated our code will retrieve the custom field's value with get_post_meta() and pass it to wp_update_post(), which will actually set the passwords.

function set_post_password( $pid ) {
    wp_update_post( array(

        // Specify post to update.
        'ID' => $pid,

        // Set password to value of custom field.
        'post_password' => get_post_meta( $pid, '_password', true )
    ) );


add_action( 'pmxi_saved_post', 'set_post_password', 10, 1 );

Retrieve Import ID for Use in Template

Here we use some custom code to retrieve the import ID. It can be saved to a custom field to track the records updated by each import. Also, the $import_id definition can be used with our various hooks that don't provide the Import ID as a parameter.

function get_import_id(){

    // Retrieve the import ID while the import is running.
    $import_id = ( isset( $_GET['id'] ) ? $_GET['id'] : ( isset( $_GET['import_id'] ) ? $_GET['import_id'] : 'new' ) );

    return $import_id.

Convert Comma-separated List into Serialized Array

Here we use custom code to convert our list element's value into a serialized array. Our theme requires this field to only contain serialized values.

Original value: red, green, blue, yellow

Value after our code: a:4:{i:0;s:3:"red";i:1;s:6:" green";i:2;s:5:" blue";i:3;s:7:" yellow";}

We call the code like this:

function list_to_serialized( $value ){

    // Split the list at the commas.
    $value = explode(',', $value);

    // Return the serialized list.
    return serialize( $value );


Generate Taxonomy Hierarchy from Child Terms

Here we generate a full taxonomy hierarchy for each of our terms using custom code. Our import file only contains the child terms and we don't yet have the full hierarchy on our site.

The field with our terms looks like this:
2.1.1. | 2.1.4. | 2.1.5. | 3.1.1. | 3.1.2.

Our code takes our element's value and outputs the hierarchical version:
2.>2.1.>2.1.1. | 2.>2.1.>2.1.4. | 2.>2.1.>2.1.5. | 3.>3.1.>3.1.1. | 3.>3.1.>3.1.2.

We must configure our import to accept that hierarchical string:

function get_hierarchy_taxonomies( $tax_str ) {

    // Convert our list of terms to an array.
    $tax_array = explode("|", $tax_str);

    // Process each term.
    foreach( $tax_array as $val ){

        // Generate the top level term.
    	$temp = substr( trim( $val ), 0, 2 ) . '>'

        // Generate the second level term. 
        $temp .= substr( trim( $val ), 0, 4 ) . '>'

        // Add the original term to the hierarchy.
        $temp .= trim( $val );


    // Return our updated term hierarchies separated by pipes.
    return implode( "|", $tax_hierarchy_array );


Prevent Certain Records from Being Updated

Here we use the wp_all_import_is_post_to_update filter to prevent updating records we've manually modified. This is accomplished by setting the do_not_update custom field on any record we don't want to update. We use get_post_meta() to check if do_not_update exists. If it does, we tell WP All Import not to update that record by returning false.

function is_post_to_update( $continue_import, $pid ) { 

    // Retrieve custom field value.
    $do_not_update = get_post_meta( $pid, 'do_not_update', true ); 

    // Update post if do_not_update field not set.
    return ( !empty( $do_not_update ) ) ? false : $continue_import; 


add_filter( 'wp_all_import_is_post_to_update', 'is_post_to_update', 10, 2 );  

Bulk Attach Images to a Record's Custom Gallery

Here we use the pmxi_saved_post action to link all of our images for each property at once. This can be faster than using pmxi_gallery_image when there are many images per property (or any other post type using custom galleries).

The get_attached_media() function is used to retrieve a list of all the images attached to the property. We then confirm a value was returned and process each of those images. Once we have our list of image IDs, we save them to _image_field using update_post_meta().

add_action( 'pmxi_saved_post', 'update_gallery_ids', 10, 3 );

function update_gallery_ids( $id ) {

    // Declare our variable for the image IDs.
    $image_ids = array();

    // Retrieve all attached images.
    $media = get_attached_media( 'image', $id );

    // Ensure there are images to process.
    if( !empty( $media ) ) {

        // Process each image.
        foreach( $media as $item ) {

            // Add each image ID to our array.
            $image_ids[] = $item->ID;


        // Convert our array of IDs to a comma separated list.
        $image_ids_str = implode( ',',$image_ids );

        // Save the IDs to the _image_field custom field.
        update_post_meta( $id, '_image_field', $image_ids_str );

Split String at Delimiter and Return One Value

Here we use a custom function to divide our string at each comma. Say we have one element in our file that contains all of our Attributes like 5, Red where the first value is Size and the second is Color.

We can use our function to return each value as needed like this:

function return_list_value( $needle, $index, $string ) {

    // Expect index to start at 1 instead of 0.
    $index -= 1;

    // Split string by $needle value.
    $split = explode($needle, $string);

    // Return the value at $index.
    return $split[$index];


Retrieve URL from String

Here we use a custom function to retrieve a URL from a string. This can help if your image element has text other than the URL itself.

For example: <img src='https://my-image-url.com/image.jpg' />

The function is called like this:

function find_url( $string ) {

    // Find the URL.
    preg_match_all('#\bhttps?://[^\s()<>]+(?:\([\w\d]+\)|([^[:punct:]\s]|/))#', $string, $match);

    // Return the first found URL.
    return  $match[0][0];


Round Imported Prices

Here we use a custom function to round our prices. The $price parameter is required and the others are optional. Any unwanted characters are removed from the price - anything other than 0-9, period, and comma.

It's called like this:

function round_price( $price = null, $multiplier = 1, $nearest = .01, $minus = 0 ) {

    if ( !empty( $price ) ) {

        // Remove any extra characters from price.
        $price = preg_replace("/[^0-9,.]/", "", $price);

        // Perform calculations and return rounded price.
        return ( round ( ( $price * $multiplier ) / $nearest ) * $nearest ) - $minus; 


Merge Multiple XML Files

This is a basic example script showing how to merge multiple XML files into 1 file before importing them with WP All Import. This script would be saved in a .php file and placed on your server, then you would point WP All Import to the script URL when setting up the import.

WPAI_Example_Merge_XML_Function parameters:

$file1The full URL/path/filename to the main file
$file2The full URL/path/filename to the file you want to merge into $file1
$root_nodeThe root node that wraps all of the post nodes
$items_nodeThe repeating post node (i.e. the one you'd choose to import on step 2 of WP All Import).
$filenameThe file that the merged data will be saved into


function WPAI_Example_Merge_XML_Function( $file1, $file2, $root_node, $items_node, $filename = 'merged.xml' ) {
    $main_doc = new DOMDocument();
    $main_doc->load( $file1 );

    $merge_doc = new DOMDocument();
    $merge_doc->load( $file2 );

    // Get root element from file1
    $root = $main_doc->getElementsByTagName( $root_node )->item(0);

    // Get post elements from file2 
    $items = $merge_doc->getElementsByTagName( $items_node );

    for ( $i = 0; $i < $items->length; $i++ ) {
        $node = $items->item( $i );
        $import = $main_doc->importNode( $node, true );
        $root->appendChild( $import );

    $file = $main_doc->save( $filename );

$final_file_name = 'merged.xml';
WPAI_Example_Merge_XML_Function( 'd1.xml', 'd2.xml', 'data', 'post', $final_file_name );
WPAI_Example_Merge_XML_Function( $final_file_name, 'd3.xml', 'data', 'post', $final_file_name );
echo file_get_contents( $final_file_name );

Once you've saved the script in a .php file and uploaded it to your server, you'll point WP All Import to the URL for the script in the "Download from URL" field, e.g.:


For reference, these are the XML files that this snippet example works with:


<?xml version="1.0" encoding="utf-8"?>
		<title>Post 1</title>


<?xml version="1.0" encoding="utf-8"?>
		<title>Post 2</title>


<?xml version="1.0" encoding="utf-8"?>
		<title>Post 3</title>

Export parent SKU when variation SKU is blank

If your WooCommerce variations are inheriting the Parent SKU, then they will have blank SKUs in your export file. To get around this, you can use the following function on a new instance ("Add Field") of the "ID" export field:

function my_get_sku( $id ) {
	$prod = wc_get_product( $id );
	if ( ! $prod ) return;
	if ( ! empty( $prod->get_sku() ) ) {
		return $prod->get_sku();
	if ( ! empty( $prod->get_parent_id() ) ) {
		$parent = wc_get_product( $prod->get_parent_id() );
		if ( ! $parent ) return;
		return $parent->get_sku();

Find post by custom field value

This is a helper function that can query products, posts, custom post types, users and taxonomy terms by looking up a custom field value.

Example uses are below. Learn how to call PHP functions in imports in our inline PHP doc.


Find post with the value from {id[1]} inside the "_old_post_id" custom field:


Find WooCommerce Product with value from {ean[1]} in the "_custom_ean" custom field:


Find user with value from {id[1]} in the "_old_user_id" custom field (user meta field):


Find taxonomy term (WooCommerce Product Category in this example) with value from {cat_id[1]} in "_custom_term_id" custom field (term meta field):



function sf_helper_meta_query_lookup( $meta_key = '', $meta_value = '', $object_type = 'post', $taxonomy = '', $return_field = 'ID', $compare = '=', $custom_args = array() ) {
    if ( empty( $object_type ) || empty( $meta_key ) || empty( $meta_value ) ) return;
	$func = 'get_posts';

	switch ( $object_type ) {
		case 'user':
			$func = 'get_users';
		case 'term':
			$func = 'get_terms';
			if ( $return_field == 'ID' ) {
				$return_field = 'term_id';
			$func = 'get_posts';
	if ( ! empty( $custom_args ) ) {
		$objects = $func( $custom_args );
		if ( ! empty( $objects ) ) {
			return $objects[0]->$return_field;
		} else {
			return false;
	$args = array();
	$meta_query = array( array(
		'key'     => $meta_key,
		'value'   => $meta_value,
		'compare' => $compare
	) );
	if ( $func == 'get_terms' ) {
		$args = array(
			'taxonomy'   => $taxonomy,
			'hide_empty' => false,
			'meta_query' => $meta_query
	} elseif ( $func == 'get_users' ) {
		$args = array( 
			'meta_query' => $meta_query
	} else {
		$args = array(
			'post_type'  => $object_type,
			'meta_query' => $meta_query
    if ( $objects = $func( $args ) ) {
        return $objects[0]->$return_field;
    } else {
		return false;

Keep old IDs when migrating products or posts

This snippet attempts to keep the old post/product ID when you migrate posts/products from one site to another. It should be saved on the import site via All Import › Settings › Function Editor.

Important note: if a post of any type exists on the new site with an ID that you're attempting to use from the old site, then this snippet won't work and a new ID will be generated for the imported post.

For users and customers see Keep old IDs when migrating users or customers.

function my_pmxi_article_data( $articleData, $import, $post_to_update, $current_xml_node ) {
    // Turn the XML node into an array.
    $xml_data = json_decode( json_encode( (array) $current_xml_node ), 1 );

    // Change 'id' if your ID exists in an element not named {id[1]}.
    $articleData['import_id'] = $xml_data['id'];

    // Return article data.
    return $articleData;
add_filter('pmxi_article_data', 'my_pmxi_article_data', 10, 4);

Keep old IDs when migrating users or customers

This snippet attempts to keep the old user/customer ID when you migrate users/customers from one site to another. It should be saved on the import site via All Import › Settings › Function Editor.

Important note: if a user/customer exists on the new site with an ID that you're attempting to use from the old site, then this snippet won't work and a new ID will be generated for the imported user/customer. Also, an ID will be auto-assigned when the user or customer is created. This snippet then tries to update that ID to the one specified.

For posts (including orders and products) see Keep old IDs when migrating products or posts.

function my_set_user_id( $post_id, $xml_node, $is_update ) {

	global $wpdb;

	// Retrieve the import ID.
	$import_id = wp_all_import_get_import_id();

	// Only run for import 1 and only run when the user is first created.
	if ( $import_id == '1' && !$is_update) {

		// Convert SimpleXml object to array for easier use.
		$record = json_decode( json_encode( ( array ) $xml_node ), 1 );

		// ID to set for user, change 'userid' to your real file element name.
		$requested_id = $record['userid'];

		// Check if the requested ID is already used.
		$id_exists = $wpdb->get_var($wpdb->prepare('SELECT user_login FROM '.$wpdb->users.' WHERE ID = '.$requested_id));

		// If the requested ID is available...
		if( $id_exists === null ){

			// ...assign the user ID as desired...
			$wpdb->update($wpdb->users, ['ID'=> $requested_id], ['ID' => $post_id]);

			// ...update the user ID for the associated meta, so the association with the user isn't lost...
			$wpdb->update($wpdb->usermeta, ['user_id'=> $requested_id], ['user_id' => $post_id]);

			// ...and update the ID in the pmxi_posts table, so the import can still manage this user.
			$wpdb->update($wpdb->prefix . 'pmxi_posts', ['post_id'=> $requested_id], ['post_id' => $post_id, 'import_id' => $import_id]);


add_action( 'pmxi_saved_post', 'my_set_user_id', 10, 3 );