To add custom checkout fields to Woocommerce checkout, we need to do three things:
- Add the custom fields to the checkout page form
- Validate the custom fields (optional)
- Save the custom fields to the order
This article will give an example of each of these and then include an example of how to access these custom fields.
The client is using Woocommerce to register students for a class. Each product in Woocommerce is a specific course on specific dates. The quantity of the product ordered is equal to the number of students to register. So the amount of fields shown on the checkout will be dependent upon the quantity of the product ordered.
If two is the quantity on the order, then the checkout field needs to show two sets of student fields.

Add the custom fields to the checkout page
We will hook into woocommerce_after_order_notes
action called in the templates\checkout\form-shipping.php
to add our student fields. The add_student_fields
function adds the markup for the fields.
Note: On this particular site we configured the cart to only allow one class/product
add_action( 'woocommerce_after_order_notes', 'add_student_fields' );
/**
* Adding custom fields to checkout form
* see: https://docs.woocommerce.com/document/tutorial-customising-checkout-fields-using-actions-and-filters/
*/
function add_student_fields( $checkout ) {
global $woocommerce;
$items = $woocommerce->cart->get_cart();
// Only adding if a single item in cart - making sure only a single product in cart
if ( count( $items ) > 1 ) {
return;
}
echo '<div class="gci-after-order-notes">';
echo '<h3>' . __( 'Student Information' ) . '</h3>';
foreach ( $items as $key => $item ) {
$quantity = $item['quantity'];
// hidden field used in update meta field functions because the number of students could change if partial refunded?>
<input type="hidden" name="studentnum" value="<?php echo intval( $quantity ); ?>"/>
<?php
for ( $i = 1; $i <= $quantity; $i ++ ) {
echo '<h4>Student ' . intval( $i ) . '</h4>';
woocommerce_form_field( 'studentname_' . $i, array(
'label' => __( 'Name', 'woocommerce' ),
'required' => true,
'class' => array( 'form-row-wide' ),
'autocomplete' => 'given-name family-name',
'priority' => 10,
), $checkout->get_value( 'studentname_' . $i ) );
woocommerce_form_field( 'studentnickname_' . $i, array(
'label' => __( 'Prefer to be called', 'woocommerce' ),
'required' => false,
'class' => array( 'form-row-wide' ),
//'autocomplete' => 'given-name family-name',
'priority' => 10,
), $checkout->get_value( 'studentnickname_' . $i ) );
woocommerce_form_field( 'studentemail_' . $i, array(
'label' => __( 'Email', 'woocommerce' ),
'required' => false,
'type' => 'email',
'class' => array( 'form-row-wide' ),
'validate' => array( 'email' ),
'autocomplete' => 'no' === get_option( 'woocommerce_registration_generate_username' ) ? 'email' : 'email username',
'priority' => 20,
), $checkout->get_value( 'studentemail_' . $i ) );
woocommerce_form_field( 'studentphone_' . $i, array(
'label' => __( 'Phone', 'woocommerce' ),
'required' => false,
'type' => 'tel',
'class' => array( 'form-row-wide' ),
'validate' => array( 'phone' ),
'autocomplete' => 'tel',
'priority' => 30,
), $checkout->get_value( 'studentphone_' . $i ) );
woocommerce_form_field( 'studentfax_' . $i, array(
'label' => __( 'Fax', 'woocommerce' ),
'required' => false,
'type' => 'tel',
'class' => array( 'form-row-wide' ),
'validate' => array( 'phone' ),
'autocomplete' => 'tel',
'priority' => 30,
), $checkout->get_value( 'studentfax_' . $i ) );
echo '<hr/>';
}
}
echo '</div>';
}
}
Validate the custom fields (optional)
Woocommerce has an action hook we can use to validate our custom fields. If the field does not validate, we can use wc_add_notice
to show this error on the form. We only need to make sure that the student name was filled in.
//Validates student info
add_action( 'woocommerce_checkout_process', 'custom_checkout_field_process' );
function custom_checkout_field_process() {
// get the amount of students
$quantity = ( isset( $_POST['studentnum'] ) ) ? intval( $_POST['studentnum'] ) : 0;
if ( $quantity > 0 ) {
for ( $i = 1; $i <= $quantity; $i ++ ) {
// Check if name is set. if it's not set, add an error.
if ( ! isset( $_POST[ 'studentname_' . $i ] ) || '' === $_POST[ 'studentname_' . $i ] ) {
wc_add_notice( __( 'Please enter the student\'s name for student ' . intval( $i ) . '.' ), 'error' );
}
}
}
}
Save the custom fields to the order
When the user on the checkout page submits the order and all the fields validate, then we need to save the custom fields to the order.
Here we are hooking into the woocommerce_checkout_update_order_meta
hook. Our function creates an empty $students
array and then grabs the number of students to save. For each student we create an associative array called $student
with the sanitized info from the form and then append it to the $students
array. After collecting each student’s information, it is saved to the order.
//update student info on checkout page
add_action( 'woocommerce_checkout_update_order_meta', 'student_fields_update_order_meta', 12, 1 );
public function student_fields_update_order_meta( $order_id ) {
$order = wc_get_order( $order_id );
$quantity = ( $_POST['studentnum'] ) ? intval( $_POST['studentnum'] ) : 1;
$students = array();
for ( $i = 1; $i <= $quantity; $i ++ ) {
$student = array();
if ( ! empty( $_POST[ 'studentname_' . $i ] ) ) {
$student['name'] = sanitize_text_field( $_POST[ 'studentname_' . $i ] );
}
if ( ! empty( $_POST[ 'studentnickname_' . $i ] ) ) {
$student['nickname'] = sanitize_text_field( $_POST[ 'studentnickname_' . $i ] );
}
if ( ! empty( $_POST[ 'studentemail_' . $i ] ) ) {
$student['email'] = sanitize_text_field( $_POST[ 'studentemail_' . $i ] );
}
if ( ! empty( $_POST[ 'studentphone_' . $i ] ) ) {
$student['phone'] = sanitize_text_field( $_POST[ 'studentphone_' . $i ] );
}
if ( ! empty( $_POST[ 'studentfax_' . $i ] ) ) {
$student['fax'] = sanitize_text_field( $_POST[ 'studentfax_' . $i ] );
}
if ( ! empty( $student ) ) {
$students[] = $student;
}
}
$order->update_meta_data( 'students', $students );
$order->save();
}
How to access these custom fields
Here are example helper functions we created to access these custom fields. We have one to get the student array of an order. We also need to be able to get a list of students that have registered for a class/product. That means we have to go through each order of a product and grab those students. Strangely, the trickiest part was getting a list of order id’s of a product. Found a solution by LoicTheAztec which I altered for our use. The URL of his stackoverflow post is in the code.
function get_students_from_order( $order_id ) {
if ( $order_id ) {
$order = wc_get_order( $order_id );
if ( $order ) {
$students = get_post_meta( $order_id, 'students', true );
return $students;
}
}
return array();
}
function get_students_from_product( $product_id = null ) {
$return = array();
if ( $product_id ) {
$order_ids = get_orders_ids( $product_id );
foreach ( $order_ids as $order_id ) {
$students = gci_get_students_from_order( $order_id );
if ( $students ) {
$return = array_merge( $return, $students );
}
}
}
return $return;
}
/**
* From: LoicTheAztec on https://stackoverflow.com/questions/43664819/get-all-orders-ids-from-a-product-id
*
* NOTE: discovered this also returns order ID's which are child posts of original order. These are if refunds are
* made, but no order meta is applied. These also have status of 'wc-completed'
*
* @param $product_id
*
* @return array
*/
function get_orders_ids( $product_id ) {
global $wpdb;
// Define HERE the orders status to include in <== <== <== <== <== <== <==
$orders_statuses = "'wc-completed', 'wc-processing', 'wc-on-hold'";
# Requesting All defined statuses Orders IDs for a defined product ID
$orders_ids = $wpdb->get_col( "
SELECT DISTINCT woi.order_id
FROM {$wpdb->prefix}woocommerce_order_itemmeta as woim,
{$wpdb->prefix}woocommerce_order_items as woi,
{$wpdb->prefix}posts as p
WHERE woi.order_item_id = woim.order_item_id
AND woi.order_id = p.ID
AND p.post_status IN ( $orders_statuses )
AND woim.meta_key LIKE '_product_id'
AND woim.meta_value LIKE '$product_id'
ORDER BY woi.order_item_id DESC"
);
// Return an array of Orders IDs for the given product ID
return $orders_ids;
}