Using XPath to Retrieve, Modify, And Filter File Elements

Using XPath to Retrieve, Modify, And Filter File Elements

WP All Import uses PHP's XPath 1.0 support to allow for powerful manipulation of your data file during import. This allows you to quickly group and limit what fields are pulled into a particular field. You can also perform simple text replacement.

Note: Most XPath statements that work directly in the SimpleXMLElement::xpath function will work in WP All Import: https://www.php.net/manual/en/simplexmlelement.xpath.php

Note: All XPath statements used in WP All Import must be wrapped in curly braces or they won't be interpreted as XPath - {elementOrExpression}

Note: If typing text into the import configuration with literal curly braces, the opening curly brace must be escaped with a backslash - {something}

What is XPath?

Officially, 'XPath is a language for addressing parts of an XML document...': https://www.w3.org/TR/1999/REC-xpath-19991116/What that means in practice is that XPath is what's used 'behind the scenes' in WP All Import to group and iterate through the data from your import file. Because of that, you have the opportunity to use custom XPath expressions to manipulate file data during the import.

But How Do I Use It?

Say you have an import file with some image elements like this:

You could drag those elements to the Images section one by one, which is fine if you only have a few and there's always the same number of image elements:

However, what do you do if you have dozens per record and each record could have any number of image elements? That's where the power of XPath can greatly simplify your import configuration.

Here's an XPath statement that would grab all of the images, regardless of number in each record:

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

It would simply be placed in the Images section:

Additional Resources

https://www.ibm.com/developerworks/library/x-xpathphp/index.html

https://www.w3schools.com/xml/xpath_intro.asp

Related

Inline PHP

FOREACH Loops

IF Statements

Example Code To Simplify Your Imports and Exports

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:

ParameterDescription$priceThe 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 $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.
wp_remote_get("yourtriggerURLhere");

}
}

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:

wp_ wp_2_ wp_3_

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

wp_capabilities wp_2_capabilities wp_3_capabilities

Each field must contain a serialized array of User Roles:

a:1:{s:10:"subscriber";b:1;}

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);

}else{

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

$wp_user_avatar_addon->run(
array(
"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:

[custom_file_download("http://example.com/full/path/to/file.csv","csv")]

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();
$import->getById($import_id);

// 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",
true);

// Generate the Option name.
$option_name = '_houzez_property_state_' . $id;

// Set the Option to link State and Country.
update_option($option_name,
array("parent_country"=>$slug));

}

add_action( 'pmxi_saved_post', 'link_houzez_country',
10,
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:

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

Here's an example product file:

namepriceid_category_defaultApples1.252

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(
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;

}else{

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

[soflyy_map_amenities({amenities[1]})]

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();
$export->getById($export_id);

// 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.
curl_exec($ch);

// 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.
curl_close($ch);

} 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")
return;

// 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"). "rn" . 'File Records:' .$count."rn".'Records Imported:'.$imported."rn".'Records Created:'.$created;
$body .= "rn" . 'Records Updated:'. $updated . "rn" . 'Records Skipped:' . $skipped . "rn" . '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:

{Product ID}
{SKU}
{Quantity}
{Item Cost}

The output would look like this:

197
195
KEYB3
KEYB2
1
5
30.00
30.00

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:

197
KEYB3
1
30.00

195
KEYB2
5
30.00

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();
$import->getById($importID);

// Ensure object is valid and check if less than 100
// records are in the file.
if ( !$import->isEmpty() && $import->count 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 set( array(
'queue_chunk_number' => 0,
'processing' => 0,
'imported' => 0,
'created' => 0,
'updated' => 0,
'skipped' => 0,
'deleted' => 0,
'triggered' => 0,
'executing' => 0
))->update();

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

// Stop the import.
die();
}
}
}
}

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:

NameSizeBlueRedLarge Blue HatLargeYesSmall Red HatSmallYesMedium Red and Blue HatMediumYesYes

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

NameSizeBlueRedColorColorLarge Blue HatLargeYesBlueSmall Red HatSmallYesRedMedium 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:

Blue,,RedBlue,Red

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:

32434


On

16683

Description

16684

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:

32434
On

16683

Description


On

16684

Description 2


On

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.
return;

}

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

NamepoolpatiobasementdeckairconditioningVilla11000House01111Apartment10001

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

NamepoolpatiobasementdeckairconditioningmycustomfeaturesVilla11000Pool, PatioHouse01101Patio, Basement, Air ConditioningApartment10001Pool, 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){
wp_delete_post($variation->ID);
}
}
$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;
break;
}
}
}
}

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

NameSizeColorColorRed 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;

}else{
// 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;
break;

// 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;
break;
}
}

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'.
unset($articles[$key]);

}
}
}
}

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.
unset($articles[$key]);

}
}

// 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(
array(
'numberposts' => $lastNumberOfPostsToExport,

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

// START TAXONOMY QUERY, REMOVE IF NOT FILTERING BY TAXONOMY
'tax_query' => array(
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:

The function is called like this:

function find_url( $string ) {

// Find the URL.
preg_match_all('#bhttps?://[^s()]+(?:([wd]+)|([^[: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:

ParameterDescription$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

Code:

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

https://example.com/mergexmlscript.php

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

d1.xml

Post 1

d2.xml

Post 2

d3.xml

Post 3

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.

Examples

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

[sf_helper_meta_query_lookup("_old_post_id",{id[1]})]

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

[sf_helper_meta_query_lookup("_custom_ean",{ean[1]},"product")]

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

[sf_helper_meta_query_lookup("_old_user_id",{id[1]},"user")]

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

[sf_helper_meta_query_lookup("_custom_term_id",{cat_id[1]},"term","product_cat")]

Code:

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';
break;
case 'term':
$func = 'get_terms';
if ( $return_field == 'ID' ) {
$return_field = 'term_id';
}
break;
default:
$func = 'get_posts';
break;
}

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 );

Keep old IDs when migrating categories and other taxonomy terms

This code tries to keep the old term ID when you migrate taxonomy terms from one site to another. It can be saved on the import site via All Import › Settings › Function Editor.

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

For products or posts see Keep old IDs when migrating products or posts, and for users or customers see Keep old IDs when migrating users or customers.

function my_set_term_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 term 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 term, change 'termid' to your real file element name.
$requested_id = $record['termid'];

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

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

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

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

// ...update the term ID for the associated taxonomy, so the association isn't lost...
$wpdb->update($wpdb->term_taxonomy, ['term_id'=> $requested_id], ['term_id' => $post_id]);

// ...and update the ID in the pmxi_posts table, so the import can still manage this term.
$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_term_id', 10, 3 );

How to Pass Exported WordPress Data Through PHP Functions

How to Pass Exported WordPress Data Through PHP Functions

You can use PHP functions to process your WordPress exports in WP All Export with the following steps:

Add or drag in the export fieldClick the field to edit it.Save your function in the Function Editor.Enable "Export the value returned by a PHP function" and type in the function name.Save the field.

Use PHP to process WordPress data while it's exported

WP All Export has the ability to process the data in any export field with PHP functions. You can write a custom function to process your data, or use a native PHP function to do it.

First, click the Add Field button under the export template and then select the field from the Select a field to export drop-down list. You can also click an existing field to bring up the edit export field modal.

From the edit export field modal, enable Export the value returned by a PHP function:

Export the value returned by a PHP function option.

In the example above, the Title is being exported. In this case, the title will be passed to your function as the first and only argument:

function my_process_title( $title ) {
// do something with $title
// return it
}

Using native PHP functions on export fields

In the your_function_name text box, you can use any native PHP function that uses one argument. For example, if you wanted to make the Title export field export all of the titles in uppercase, you could use strtoupper():

In fact, you can use any function that exists and is accessible during the export. That includes WordPress functions like wp_get_attachment_url, sanitize_title, etc.

Using custom PHP functions on export fields

For more complicated cases, you can write full-featured PHP functions inside the Function Editor.

As an example, let's say that you're exporting data from a real estate theme like WP Residence and you want to export the property gallery image URLs for each exported property. They're stored in the image_to_attach custom field as IDs. If we were just to export that custom field, we'd get this:

35,39,37,41,43,

We could use this function to extract the IDs, get the attachment URLs, then return a delimited list of the URLs:

function my_convert_ids_to_urls( $ids ) {
// Return nothing if there are no IDs.
if ( empty( $ids ) ) {
return null;
}

// Turn the IDs into an array of IDs.
$ids = explode( ',', $ids );

// Convert the IDs to attachment URLs.
$urls = array_map( function( $id ) {
return wp_get_attachment_url( $id );
}, $ids );

// Get rid of empty array elements.
$urls = array_filter( $urls );

// Return comma-delimited list of image URLs.
return implode( ',', $urls );
}

Let's use this function with the image_to_attach export field from WP Residence:

Using image on the image_to_attach field.

And that's it. Our export file will contain a column labeled Image Gallery URLs, and each row in that column will contain a comma delimited list of image URLs for the images in that property gallery.

Head over to https://www.wpallimport.com/documentation/developers/code-snippets/ for a full list of code snippets to get you started.

Overview

Overview

In this tutorial you』ll learn how to use WP All Import to import data from your XML or CSV into any plugin or theme.

Many themes and plugins have 「custom」 places to enter in data – not just the post title and content boxes on normal Posts or Pages.

The technical term for these extra fields is Custom Fields or post meta.

WP All Import has full support for Custom Fields and can even auto-detect the names of the fields used by your themes and plugins so you don』t have to ask the author, guess, or look at any code.

Watch the video tutorial below to see how easy it is:

Auto-Detecting The Fields

For WP All Import to detect a field, it must exist at least once in your database in a published post (not draft or any other post status).

If you already have posts on your site that have the fields you want to import to filled out, WP All Import will detect the fields.

If not, simply create a dummy post, enter in dummy values for the fields you want to import data to, then publish it.

Click See Detected Fields to see the detected fields.

See Detected Fields

Once you click See Detected Fields, WP All Import will show you all of the detected fields.

Populated Detected Fields

WP All Import can also show you the different possible values for your fields. So for example, if you need to know what the possible values of a 「select」 option is, just manually create a post with the 「select」 option in every state you want to know the field value for.

Example – Select Box In OpenDoor Theme

Rent or Buy Select Field

Saving two posts, one with the select set to 「Rent」 and the other with the select set to 「Buy」, results in WP All Import auto-detecting the possible values:

Of course, you can import to fields without WP All Import auto-detecting their names and values – you just have to know the names and possible field values yourself.

Serialized Fields

Serialized Fields

If you aren』t a developer and don』t know what 「serialization」 is, this article will probably be over your head.

Developers, read on…

In very rare cases, some themes store multiple data points in a single field. Themes shouldn』t do this. It』s not nice, and the practice is generally frowned upon. But, some do. These fields are often called 「serialized」 fields.

Serialized Selected

Serialized Dialog

You can either use the serialization feature in WP All Import to import the key and value pairs, or you can use a custom PHP function to return the value in the proper format needed by your theme or plugin.

The Serialize feature generates an array with the key => value pairs you specify, and then returns the serialize()』d value of the array.

How to Map Custom Fields

How to Map Custom Fields

Mapping is used when the value you need imported to your site is different than the value in your file. A visual interface is available for mapping for Custom Fields and Custom Taxonomies. To map elsewhere, you can use a custom PHP function.

Visual Mapping Tool

For example, you may have a file with product categories that are written in Dutch, but you want to import these products to categories that already exist on your site with English names.

Or, your theme or plugin requires the value of a Custom Field it uses be either 1 or 0, but in your XML or CSV file, the values are expressed as Yes or No.

You need to map the values in your file to the values you actually imported to your site.

Example – Mapping For A Dropdown

In the previous article, Importing to Theme & Plugin Fields, we explained how to detect the fields used by your theme or plugins, and their possible values.

Our theme has a dropdown to specify whether a Property Listing is for rent, or for sale.

Rent or Buy Example

The field name is 「rob_value」, and the possible values, as stored in the database, are 「Rent」 and 「Buy」.

Field Example

Note: You can see the values as they are stored in the database by clicking inside the Value textbox for the 「rob_value」 field in WP All Import. The values stored in the database may not be the same as values shown inside the select box in the theme. We must map to the values as they are stored in the database.

Our XML file has a element that is set to FORSALE if the property is for sale (Buy), and FORRENT if the property is for rent (Rent).

XML Data

Without mapping, simply dragging & dropping to the Value box for rob_value would not work, because the value of is not correct for our theme.

To solve this problem, we use the Mapping feature.

Mapping

Click Field Options… and then click Mapping.

A dialog box will pop up allowing you specify the mapping rules.

Mapping Rules Example

Now, when the import is run, FORSALE in your file will be translated to Buy, and FORRENT in your file will be translated to Rent, so your data will be imported correctly, as per the requirements of your theme and plugins.

Combine and Process Multiple Data Elements into a Custom Export Field

Combine and Process Multiple Data Elements into a Custom Export Field

Custom export fields can be used to combine any number of data elements, function calls, and static text into the same export field.

Create a Custom Export Field

To create a custom export field, click Add Field and select Custom export field.

After selecting Custom export field, drag elements from the Available Data section on the right into the export field editor on the left. Unlike normal export fields, you can combine multiple fields together. For example, you can drag the First Name and Last Name elements to create a Full Name in a Users export.

Here's the result:

Using Static Data

You can include any type of static text or data required in your export. For example, you can include the currency symbol next to your prices when exporting products.

Here's the result:

Using PHP functions

You can use functions directly inside the Custom export field. Moreover, you can call functions with as many arguments as required.

Following the previous example, you can use a function to apply a 20% discount if the product price is above 100 USD and the product category is clothing. This function would take in two parameters: a price, and a category.

Here's the result:

Update Existing Posts

Update Existing Posts

WP All Import can import data to posts that already exist on your site, even if they were manually created instead of imported by WP All Import.

You need something in your import file that WP All Import can use to 「match」 the 「records」 in your import file to the posts that already exist on your site – that』s why it』s called Manual Record Matching.

When importing into existing records, you can specify which data WP All Import will update or overwrite, and which will be left alone.

Follow along with the below example to get a complete understanding of how to import data into existing posts on your site.

Example – Updating Multiple Property Listings With New Prices

I have a few property listings with outdated prices.

Example Properties

I have a CSV file with the MLS numbers of the properties, and the new prices.

CSV File

I』ve entered the MLS number of each property in my theme, so we can use the MLS number as the 「matcher」 so that WP All Import Import knows which price should be assigned to which property.

MLS Number

To do the import, upload your CSV in Step 1, and choose Existing Items.

Step 1 Existing Items

Continue to Step 2 and then to Step 3. In Step 3, set the price Custom Field to the price from your file.

Setting the price

The only field we want to import data to is the price. So just leave all the other fields blank. WP All Import Import will warn you that your post title and content are blank, but that』s fine – you can continue anyway. We don』t want to update the title or content here, just the price.

Now it』s time for the most important part – telling WP All Import Import how to match the records in our CSV file with the existing property listings already on our site. We』re going to match by the MLS number – since we have the MLS number in both places – on our site, and in our file.

Choose 「Match by Custom Field」 and click in the Name box to see a dropdown list of Custom Fields available to match by. Then choose 「mls_value」 – the Custom Field name the theme uses internally for the MLS field.

Record Matching

Then drag & drop the MLS column in your CSV file to the Value textbox.

MLS Value

Now, for each record in your file, WP All Import will look for a property on your site with an mls_value Custom Field that equals {mlsno[1]} from your file, and then import the price to it.

To ensure WP All Import only imports the price, and doesn』t overwrite the title, content, and other fields we left empty with blank values, we specify which data points we wanted updated, and which we want ignored.

Choose which data to update

Here』s the result after running the import – our 3 posts were updated with the new prices.

Import Summary

Data Updated

Server Configuration

Server Configuration

If your server can successfully complete an import then it is properly configured. We have guides on import speed, terminated imports, and file upload errors.

If your server cannot successfully complete an import then you should read our troubleshooting guide.

To learn about web hosts that we recommend, please see our recommended hosts page.

Server Requirements

There are no server hardware requirements for WP All Import. If you are maxing out your server』s available resources during the import process, upgrading to a more powerful server will help to increase your import speed. If you are not maxing out your server』s available resources then you will probably not benefit from upgrading to a more powerful server.

Generally speaking, if your server can run WordPress then it can run WP All Import. Your server will need the following software packages installed:

PHP 7.0 or greaterMySQL 5.5 or greatercURLlibxmlSimpleXMLxmlreaderxmlwriterZipArchive

Recommended Server Settings

The following settings are recommendations to help avoid server errors. If you aren』t running in to any errors during the import process then you don』t need to worry about these settings.

Maximum Input Variables (PHP): This is set in php.ini with max_input_vars. It determines the maximum numbers of input variables that PHP will allow. In our experience 6000 is sufficient, but you may wish to raise this limit if you are importing a large amount of product attributes.

Maximum Upload File Size (PHP): This is set in php.ini with upload_max_filesize. It determines the maximum file size that your server will allow to be uploaded. This value must be larger than the size of the file you wish to upload to WP All Import.

Maximum Post Size (PHP): This is set in php.ini with post_max_size. It determines the maximum file size allowed to be used in PHP process. This should be set higher than upload_max_filesize.

Memory Limit (PHP): This is set in php.ini with memory_limit. It determines how much memory a script is able to allocate. This should be set higher than post_max_size.

Maximum Execution Time (PHP): This is set in php.ini with max_execution_time. It determines how long a process is allowed to run before it』s terminated. You can ask your host to increase the limit, but first you should lower the 『Records per iteration』 setting in WP All Import.

FCGID Timeouts (Apache): This is set in httpd.conf with FcgidIOTimeout. It determines how long mod_fcgid will wait while trying to perform a read or write. It should be set as high as your host will allow. In our experience 90 seconds is sufficient.

Recommended Hosts

Recommended Hosts

While many hosts work well with WP All Import, if you are looking for a specific recommendation Liquid Web and Nexcess work exceptionally well with WP All Import and we've partnered with them to offer a discount for new users. To take advantage of this discount, use the coupon code WPALLIMPORT35 for 35% off your first 3mo when signing up.

Managed Hosting Plans

Nexcess offers two products that we recommend, one tuned for general purpose WordPress sites, and another specifically tuned for WooCommerce sites. Both plans start at $19/mo and work great with WP All Import.

Nexcess Managed WordPress Hosting

Nexcess Managed WooCommerce Hosting

VPS, Cloud, and Dedicated Servers

If you need more horsepower, flexibility, or control over your hosting environment, then you probably want to go with one of Liquid Web's hosting options. They have a variety of options to choose from, with VPS hosting starting at $39/mo, cloud hosting at $51/mo, and dedicated servers at $140/mo.

Liquid Web VPS Hosting

Liquid Web Cloud Hosting

Liquid Web Dedicated Servers

Which one is best for me?

If you aren't sure, the managed hosting options from Nexcess will probably serve your needs very well. If you have a large site or need more flexibility than a managed hosting plan can offer, then you should get in touch with Liquid Web to discuss their VPS, cloud, and dedicated hosting options.

Managed Hosting vs VPS Hosting

Managed Hosting: The hosting company has many sites from different customers all running on the same server inside the same operating system, competing for the same resources. Think of it like sharing a house with roommates. Everyone shares the same kitchen, living room, and bathroom. If the same person spends four hours a day showering or fills the refrigerator with their groceries, you』re going to have a problem. Like landlords, some shared hosts are better about mitigating these issues, but you are still sharing a bathroom with many other people.

VPS & Cloud Hosting: Sites from different customers are still running on the same server, but on separate operating systems, in virtual computers. Another piece of software called a hypervisor makes sure that each operating system always has the same amount of resources available to it. Think of it like living in an apartment complex. You still have a lot of neighbors technically living in the same building, but no matter how many people are taking a shower at the same time, yours is always available.

Dedicated Hosting: You are renting out an entire server and are not sharing it with anyone. This is overkill for the vast majority of sites, but if you need complete control over your hosting environment, then renting a dedicated server is a good option.

Imports & Server Resources

Imports require a lot of server resources, especially if you are importing a lot of data or have a large database. Hosting review sites cover speed, reliability, and customer service but rarely let you know which hosts will throttle your processing power or limit your SQL queries.

Some hosts will pack many customers into small, underpowered, servers. Sites that consume a lot of resources will eat up all of the available processing power, leaving none for you and your import. Alternatively, some hosts will proactively kill your import process to protect other people』s sites.

We』ve tested a lot of hosts and out of all of them, Liquid Web and Nexcess are the two we』ve had the best experiences with. Both are fast, reliable, have well-reviewed 24/7 customer support, and work great with WP All Import.