This week a client asked to add estimated ship times to the single-product pages in WooCommerce. Ship times are sometimes different for different products, so adding it as a custom field to each product made sense. As a bonus, there’s a “Shipping” tab in the Product Data metabox that the field can go in. Some Googling led me to Tom McFarlin‘s “Adding Custom Fields to Simple Products with WooCommerce” tutorial on tuts+, which is pretty thorough, but out of date – the actions he’s are no longer in WooCommerce, so it’s time for a new version of that tutorial, my-style. There are three main tasks to accomplish: Add the field to the WordPress back-end, save whatever is in the field, and output the data on the Single Product page. We’ll look at each task separately. The completed plugin is up on GitHub if you want to follow along.
Add the Custom Field to WooCommerce in the WordPress Back-End
WooCommerce provides an incredible number of hooks and filters. We’re going to us woocommerce_product_options_shipping
hook, because it runs in the shipping block, but is an equivalent hook for each Product Data tab. Look at the files in the woocommerce/includes/admin/meta-boxes/views
directory to understand how the Product Data, (and all the WooCommerce Metaboxes), are created, and what hooks are available.
Now we know what hook we need, it’s time for some code:
function print_admin_field() {
/**
* @var \WC_Product
* @see https://docs.woocommerce.com/wc-apidocs/source-class-WC_Meta_Box_Product_Data.html#42
*/
global $product_object;
if( $product_object->get_type() == 'simple' ) {
$field = [
'id' => 'sif_ships_in',
'label' => __('Ships In', 'jb-sif'),
'placeholder' => __('example: 1-2 days', 'jb-sif'),
'value' => get_post_meta( $product_object->get_id(), '_sif_ships_in', true ), // Pre-fill any values that have already been saved.
'description' => __( 'Add a "Ships in X" message below the Product Meta on the single-product page.', 'jb-sif'),
'desc_tip' => true
];
\woocommerce_wp_text_input( $field );
}
}
add_action( 'woocommerce_product_options_shipping', 'JB\SIF\print_admin_field');
We make sure we have the $product_object
variable available, (it’s set in the WC_Meta_Box_Product_Data
class, linked with the @see
directive), then we make sure we’re adding the field to a “Simple” product type. Then, instead of writing a bunch of HTML, we set some info in an array, and pass it to the woocommerce_wp_text_input()
function, (with a backslash, because the plugin is namespaced). WooCommerce has several of these helper fields available that create a form fields and their associated markup so they fit nicely with the rest of the WooCommerce admin.
Notice we’re already getting a value with get_post_meta()
. That’s because the same code gets used every time the field is displayed – so if there’s a value in the database we want to display it. This is all we need to properly display a field in the Shipping section of the Product Data. We could add a name
attribute to the $field
variable, but it defaults to the value of id
, so I left it out. Setting desc_tip
to true puts the description in the tooltip that appears when someone hovers the circle with the question mark beside the field.
The last line of the code block above hooks our function into the woocommerce_product_options_shipping
action. Because the plugin has its own namespace we need to include the full name of the function in the add_action()
call.
Save Whatever is Entered in the Field
Again, a game of hooks figuring out how to save the data. Tom’s tutorial from last year used a woocommerce_process_product_meta
hook which no longer exists. It looks like it’s been replaced by a woocommerce_process_product_meta_*
hook for each product type. Since we’re only using a Simple product, we’ll use the woocommerce_process_product_meta_simple
hook. Now we know which hook to use, some code:
function save( $post_id ) {
// check nonce
if( ! ( isset( $_POST['woocommerce_meta_nonce'], $_POST['sif_ships_in'] ) || wp_verify_nonce( sanitize_key( $_POST['woocommerce_meta_nonce'] ), 'woocommerce_save_data' ) ) ) {
return false;
}
update_post_meta( $post_id, '_sif_ships_in', sanitize_text_field( $_POST['sif_ships_in'] ) );
}
add_action( 'woocommerce_process_product_meta_simple', 'JB\SIF\save' );
In the save()
function, (which we can name “save” without collisions because we’re using namespaces), we check the field is part of the $_POST
array, check the nonce set by WooCommerce, and save the contents of the field, without forgetting to sanitize the data first.
WooCommerce may already checks the nonce for us, but I am not 100% sure, so I added an extra check.
Output the Data on the Single Product Page
WooCommerce’s plethora of hooks make placing the output relatively simple. I chose to put it right below the “Add to Cart” button on the Single Product page, but if you look through WooCommerce’s templates/content-single-product.php
file and the files in the templates/single-product
directory you’ll find lots hooks to use – and of course you’re not restricted to the single-product page. If you want to add your output to each product in a list of products take a look in the templates/content-product.php
file.
function print_output() {
global $product;
$ships_in = get_post_meta( $product->get_id(), '_sif_ships_in', true );
if( !empty( $ships_in )): ?>
< ?php endif;
}
add_action( 'woocommerce_single_product_summary', 'JB\SIF\print_output', 45 );
Our print_output()
function is pretty simple: Grab the post_meta, check it’s not empty, and output it mixed in with a “Ships In” string. It’s hooked into the woocommerce_single_product_summary
action with a priority of 45, which means it appear after the Add to Cart button and after the Product Meta. WooCommerce’s template files are really good at explaining what functions are hooked to actions, and the priority they’re hooked at, which makes it easy to place things on a page without having to copy templates over to your theme.
Conclusion
Adding fields to the WooCommerce part of a WordPress admin area is much easier than adding a whole metabox. You can grab the completed plugin on Github. You’ll see there’s a bit more to the plugin: requirements are checked using Mark Jaquith’s method before embarking on a namespaced plugin with shorthand array syntax.
If this is useful let me know, and feel free to open tickets and send pull requests on Github.