As you might know, starting from WooCommerce 8.2, WooCommerce orders are not just a custom post type anymore. The WooCommerce development team decided to switch to the High-Performance Order Storage solution instead. Simply put, WooCommerce orders aren’t stored in the same posts database table anymore and have their own tables.
Recently, I needed to add a custom meta box to the WooCommerce order editor. And I found out that most of the tutorials on this topic are outdated. They show how to utilize the add_meta_box() function but still point to the shop_order custom post type which isn’t used anymore.
So I had to dig deeper into this topic to figure out what’s the proper way to add custom meta boxes to the WooCommerce order editor after HPOS has become a standard.
What we’re going to create
As always, I decided to create at least a barely realistic example. In this tutorial, we’re going to create a custom order meta box and call it “Shipping Details”. This meta box will contain two custom fields:
- Package Type (select)
- Shipping details (textarea)
Here’s what we’ll get in the end:
Which hooks to use
The code responsible for registering and rendering the default order meta boxes is placed in the /src/Internal/Admin/Orders/Edit.php file. Here, you can find two action hooks that are interesting for us:
/**
* From wp-admin/includes/meta-boxes.php.
*
* Fires after all built-in meta boxes have been added. Custom metaboxes may be enqueued here.
*
* @since 3.8.0.
*/
do_action( 'add_meta_boxes', $this->screen_id, $this->order );
/**
* Provides an opportunity to inject custom meta boxes into the order editor screen. This
* hook is an analog of `add_meta_boxes_<POST_TYPE>` as provided by WordPress core.
*
* @since 7.4.0
*
* @param WC_Order $order The order being edited.
*/
do_action( 'add_meta_boxes_' . $this->screen_id, $this->order );
The last action hook has a dynamic name. In our case, it’ll be add_meta_boxes_woocommerce_page_wc-orders.
Both action hooks are fine but they work slightly differently. I’ll show you how to use both of them.
Using add_meta_boxes action hook
The add_meta_boxes hook is actually just a part of WordPress. For example, it fires when you open your regular post editor. This means that we need to check if the current editor screen is the one we need. Otherwise, our meta box will be added in places where we obviously don’t need it.
add_action( 'add_meta_boxes', 'register_shipping_details_metabox', 99, 2 );
function register_shipping_details_metabox( $screen_id, $order ) {
// Check if the current screen is the WooCommerce order editor
if ( $screen_id !== 'woocommerce_page_wc-orders' ) {
return;
}
add_meta_box( 'woocommerce-shipping-details', 'Shipping Details', 'render_shipping_details_metabox', 'woocommerce_page_wc-orders', 'normal', 'high' );
}
Using add_meta_boxes_woocommerce_page_wc-orders action hook
This hook isn’t documented anywhere and the only mention of it I found is this GitHub issue from the WooCommerce repo:
https://github.com/woocommerce/woocommerce/issues/40437
However, this hook is only a part of WooCommerce and won’t work without it. The benefit of using this hook is that we don’t need to do the $screen_id check as we did in the previous code snippet. The $screen_id is already in the hook name which means that this hook will fire only on the order editor page.
Here’s the code for this hook:
add_action( 'add_meta_boxes_woocommerce_page_wc-orders', 'register_shipping_details_metabox', 99, 1 );
function register_shipping_details_metabox( $order ) {
add_meta_box( 'woocommerce-shipping-details', 'Shipping Details', 'render_shipping_details_metabox', 'woocommerce_page_wc-orders', 'normal', 'high' );
}
Let’s render our fields
This part will be unique for your own case because you’ll obviously have your own fields and specifics. I’ll show you the code I used to create the two fields I mentioned at the beginning of this tutorial:
<?php
function render_shipping_details_metabox() {
$order_id = isset( $_GET['id'] ) ? $_GET['id'] : null;
$package_type = '';
$shipping_details = '';
if ( isset( $order_id ) ) {
$order = wc_get_order( $order_id );
$package_type = $order->get_meta( 'package_type', true );
$shipping_details = $order->get_meta( 'shipping_details', true );
}
$options = [
'standard' => 'Standard',
'fragile' => 'Fragile',
'envelope' => 'Envelope',
];
?>
<div class="form-field">
<label for="package_type">Package Type</label>
<select id="package_type" name="package_type">
<?php foreach ($options as $value => $label): ?>
<option value="<?php echo $value; ?>" <?php echo ($package_type === $value) ? 'selected' : ''; ?>>
<?php echo $label; ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="form-field">
<label for="shipping_details">Shipping details (optional)</label>
<textarea id="shipping_details" name="shipping_details" placeholder="Shipping details (optional)"><?php echo esc_html( $shipping_details ) ?></textarea>
</div>
<?php
}
Now, if you go to the order editor page (both update or create), you’ll see our two custom fields in their separate meta box called “Shipping Details”. Wonderful! The most difficult part is behind us and now the only thing left is to make sure that the values that come from our custom fields are successfully saved in the database.
How to save the values
Luckily, this part hasn’t changed much since WooCommerce orders were just a custom post type. The only thing that you need to remember is that you can’t use update_post_meta() to update an order’s meta field anymore.
Actually, it wasn’t a preferable option even earlier, when WooCommerce CRUD classes and operations have become a standard. But now, you can’t use the old way because, well, WooCommerce orders aren’t posts anymore 🤷
The hook you need to use in order to save and update the values from our custom fields is called woocommerce_process_shop_order_meta from the same Edit.php file we mentioned. From this hook, we can have access to both $order_id and the $order object itself but in my example having $order_id is enough.
<?php
add_action( 'woocommerce_process_shop_order_meta', 'save_order_shipping_details', 99, 1 );
function save_order_shipping_details( $order_id ) {
$order = wc_get_order( $order_id );
if ( ! $order ) {
return;
}
// Check if nonce is valid
if (
! isset( $_POST['woocommerce_meta_nonce'] ) ||
! wp_verify_nonce( $_POST['woocommerce_meta_nonce'], 'woocommerce_save_data' )
) {
return;
}
if ( isset( $_POST['package_type'] ) ) {
$package_type = sanitize_text_field( $_POST['package_type'] );
$order->update_meta_data( 'package_type', $package_type );
}
if ( isset( $_POST['shipping_details'] ) ) {
$shipping_details = sanitize_text_field( $_POST['shipping_details'] );
$order->update_meta_data( 'shipping_details', $shipping_details );
}
$order->save();
}
Now we’re all set. Our custom fields now will be saved when you create a new order or update an existing one.
Final code
<?php
add_action( 'add_meta_boxes_woocommerce_page_wc-orders', 'register_shipping_details_metabox', 99, 1 );
function register_shipping_details_metabox( $order ) {
add_meta_box( 'woocommerce-shipping-details', 'Shipping Details', 'render_shipping_details_metabox', 'woocommerce_page_wc-orders', 'normal', 'high' );
}
function render_shipping_details_metabox() {
$order_id = isset( $_GET['id'] ) ? $_GET['id'] : null;
$package_type = '';
$shipping_details = '';
if ( isset( $order_id ) ) {
$order = wc_get_order( $order_id );
$package_type = $order->get_meta( 'package_type', true );
$shipping_details = $order->get_meta( 'shipping_details', true );
}
$options = [
'standard' => 'Standard',
'fragile' => 'Fragile',
'envelope' => 'Envelope',
];
?>
<div class="form-field">
<label for="package_type">Package Type</label>
<select id="package_type" name="package_type">
<?php foreach ($options as $value => $label): ?>
<option value="<?php echo $value; ?>" <?php echo ($package_type === $value) ? 'selected' : ''; ?>>
<?php echo $label; ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="form-field">
<label for="shipping_details">Shipping details (optional)</label>
<textarea id="shipping_details" name="shipping_details" placeholder="Shipping details (optional)"><?php echo esc_html( $shipping_details ) ?></textarea>
</div>
<?php
}
add_action( 'woocommerce_process_shop_order_meta', 'save_order_shipping_details', 99, 1 );
function save_order_shipping_details( $order_id ) {
$order = wc_get_order( $order_id );
if ( ! $order ) {
return;
}
// Check if nonce is valid
if (
! isset( $_POST['woocommerce_meta_nonce'] ) ||
! wp_verify_nonce( $_POST['woocommerce_meta_nonce'], 'woocommerce_save_data' )
) {
return;
}
if ( isset( $_POST['package_type'] ) ) {
$package_type = sanitize_text_field( $_POST['package_type'] );
$order->update_meta_data( 'package_type', $package_type );
}
if ( isset( $_POST['shipping_details'] ) ) {
$shipping_details = sanitize_text_field( $_POST['shipping_details'] );
$order->update_meta_data( 'shipping_details', $shipping_details );
}
$order->save();
}
Summary
I hope that my tutorial helped you to solve your problems. As always, feel free to share your opinion about this post or ask any questions you’ve got in the comments section.
Don’t forget to follow me on LinkedIn or add my blog to your RSS reader if you don’t want to miss new posts on my blog.