tringwebdesign tringwebdesign - 4 months ago 23
PHP Question

WooCommerce: Dynamic Custom Checkout Fields in admin

I am having issues adding custom checkout fields to the order admin (and maybe emails, i'm not there yet). Coding issues aside, I mostly followed https://www.kathyisawesome.com/woocommerce-customize-checkout-fields/ - here's my code so far:

http://pastebin.com/y8LDcDxS

I have it adding 'x' amount of $fields['extra_fields'] based on product quantity, assuming one product in the cart. I'm struggling with kia_display_order_data_in_admin() - I keep getting bit by get_country() whatever I do. I've got the non-dynamic variation (based on having two products in the cart) in the pastebin - everything else i've tried to make this part dynamic doesn't work.

I see some options to get these dynamic fields loaded in admin but I just cant get there:


  1. Grab product quantity again and append $i to the end of the standard field names

  2. Somehow get an array of all fields so I can extract $fields['extra_fields']

  3. Get the extra_fields post meta by order_id

  4. Something else?



EDIT: After Kia's help, I have revised the code a little and it will now dynamically add custom checkout fields. This works all the way up to the admin order screen, but won't work when trying to update order details in admin
kia_save_extra_details()


EDIT 2: Please see the accepted answer below as this works for my question

Answer

Are you sure Product Add-Ons might not be more suited to this? If not, I think what you need is a custom input type. This should get you started.

/* Add a new checkout field 
 * 
 * These is our base field group, later on, depending on product quanitiy, $key = 'passenger_title_1' for example
 * 
 */

function supreme_filter_checkout_fields($fields){
    $fields['extra_fields'] = array(
    'passenger_details' => array(
        'type' => 'passenger_details',
        'required'      => false,
        'label' => __( 'Passenger Details' )
        ),
    );

    return $fields;

}
add_filter( 'woocommerce_checkout_fields', 'supreme_filter_checkout_fields' );

What the heck is a "passenger_details" input? Well, we're going to define that because woocommerce_form_field() has a catch all in case the type doesn't match any of WooCommerce's defaults. See 'woocommerce_form_field_' . $args['type']

Here is where we're going to list the group of fields that you want for each passenger.

function supreme_filter_checkout_field_group( $field, $key, $args, $value ){
    $op_cart_count = WC()->cart->get_cart_contents_count();

    $html = '';

    for ( $i = 1; $i <= $op_cart_count; $i++) {

        $html .= woocommerce_form_field( "passenger_details[$i][title]", array( 
            "type" => "select",
            "return" => true,
            "value" => "",
            "options" => array( "mr" => __( "Mr" ), "mrs" => __( "Mrs" ), "miss" => __( "Miss" ) ),
            "required"      => false,
            "label" => __( "Title" )
            )
        );
        $html .= woocommerce_form_field( "passenger_details[$i][first_name]", array(
            "type" => "text",
            "return" => true,
            "value" => "",
            "required"      => false,
            "label" => __( "First Name" )
            )
        );
        $html .= woocommerce_form_field( "passenger_details[$i][middle_name]", array(
            "type" => "text",
            "return" => true,
            "value" => "",
            "required"      => false,
            "label" => __( "Middle Name" )
            )
        );
        $html .= woocommerce_form_field( "passenger_details[$i][last_name]", array(
            "type" => "text",
            "return" => true,
            "value" => "",
            "required"      => false,
            "label" => __( "Last Name" )
            )
        );
        $html .= woocommerce_form_field( "passenger_details[$i][date_of_birth]", array(
            "type"          => "text",
            "return" => true,
            "value" => "",
            "class"         => array("date-of-birth form-row-wide"),
            "required"      => false,
            "label"         => __("Date of Birth"),
            "placeholder"       => __("Select Date"),
            "options"     =>   array("" => __("Date of Birth", "woocommerce" ))
            )
        );
        $html .= woocommerce_form_field( "passenger_details[$i][dietary_required]", array(
            "type" => "select",
            "return" => true,
            "value" => "",
            "class"         => array("dietary-required form-row-wide"),
            "options" => array( "" => __( "Please Select" ), "y" => __( "Yes" ), "n" => __( "No" ) ),
            "required"      => false,
            "label" => __( "Dietary Requirements?" )
            )
        );
        $html .= woocommerce_form_field( "passenger_details[$i][dietary_preference]", array(
            "type" => "select",
            "return" => true,
            "value" => "",
            "class"         => array("dietary-requirements form-row-wide"),
            "options" => array( "v" => __( "Vegetarian" ), "GF" => __( "Gluten Free" ) ),
            "required"      => false,
            "label" => __( "Meal Preferences" )
            )
        );

    }
    return $html;
}
add_filter( 'woocommerce_form_field_passenger_details', 'supreme_filter_checkout_field_group', 10, 4 );



// display the extra field on the checkout form
function supreme_extra_checkout_fields(){ 

    $checkout = WC()->checkout();

    foreach ( $checkout->checkout_fields['extra_fields'] as $key => $field ) :

        woocommerce_form_field( $key, $field, $checkout->get_value( $key ) );

    endforeach;


}
add_action( 'woocommerce_checkout_after_customer_details' ,'supreme_extra_checkout_fields' );

EDIT Handle sanitization and saving of data. Note that I changed the supreme_filter_checkout_field_group function above and removed the ID parameter.

~~Using the id argument we can dynamically set the input name. The resulting markup should be something like:~~

<input type="text" class="input-text " name="last_name" id="passenger_details[2]last_name" placeholder="" value="">

This was wrong. the name parameter needs to be an array! And the name is pulled from the first parameter of woocommerce_form_field(). The ID is irrelevant to our needs, though does need to be unique to maintain proper markup. This is the result we need and should now be getting with the above modification:

<input type="text" class="input-text " name="passenger_details[2][last_name]" id="passenger_details[2][last_name]" placeholder="" value="">

Now on submit the $_POST['passenger_data'] should be an array (passenger) of arrays (passenger details). And after my edit, it is!

Now, to process this data and sanitize the individual fields in the passenger data we create a function for sanitizing our custom field type:

/**
 * Sanitize our custom field
 * 
 */
function supreme_custom_process_checkout_field_passenger_details( $posted ){

    $clean = array();

    foreach( $posted as $passenger ){
        $details = supreme_custom_checkout_clean_passenger_details( $passenger );

        if( ! empty( $details ) ){
            $clean[] = $details;
        }
    }

    return $clean;
}
add_filter( 'woocommerce_process_checkout_passenger_details_field', 'supreme_custom_process_checkout_field_passenger_details' );


function supreme_custom_checkout_clean_passenger_details( $passenger = array() ){
    $details = array();
    if( isset( $passenger["title"] ) ){
        $details['title'] = sanitize_text_field( $passenger["title"] );
    }
    if( isset( $passenger["first_name"] ) ){
        $details['first_name'] = sanitize_text_field( $passenger["first_name"] );
    }
    if( isset( $passenger["middle_name"] ) ){
        $details['middle_name'] = sanitize_text_field( $passenger["middle_name"] );
    }
    if( isset( $passenger["last_name"] ) ){
        $details['last_name'] = sanitize_text_field( $passenger["last_name"] );
    }
    if( isset( $passenger["date_of_birth"] ) ){
        $details['date_of_birth'] = $date = preg_replace("([^0-9/])", "", $passenger["date_of_birth"] );
    }
    if( isset( $passenger["dietary_required"] ) ){
        $details['dietary_required'] = $passenger["dietary_required"] == "y" ? "y": "n";
    }
    if( isset( $passenger["dietary_preference"] ) && isset( $passenger["dietary_required"] ) && $passenger["dietary_required"] == "y" ){
        $details['dietary_preference'] = $passenger["dietary_preference"] == "GF" ? "GF": "v";
    }
    return $details;
}

Now that it is sanitized, updating is just a matter of saving the post meta:

/**
 * update_post_meta
 * 
 */
function supreme_custom_checkout_field_update_order_meta( $order_id, $posted ){

    if( ! empty( $posted["passenger_details"] ) ){
        update_post_meta( $order_id, "_passenger_details", $posted["passenger_details"] );
    } else {
        delete_post_meta( $order_id, "_passenger_details" );
    }

}
add_action( 'woocommerce_checkout_update_order_meta', 'supreme_custom_checkout_field_update_order_meta', 10, 2 );

Now, we definitely have all the fields saved in the _passenger_details post meta key and we can loop through that array any time we need to display it. For example, here it is on the admin order page:

// display the extra data in the order admin panel
function kia_display_order_data_in_admin( $order ){  

    $passenger_details = get_post_meta( $order->id, "_passenger_details", true ); 

    if( ! empty( $passenger_details ) ) { 

        $passenger_defaults = array(
                "title" => "",
                "first_name" => "",
                "middle_name" => "",
                "last_name" => "",
                "date_of_birth" => "",
                "dietary_required" => "",
                "dietary_preference" => ""
            );

    ?>
    <div class="passenger_data">
        <h4><?php _e( "Passenger Details", "your-slug" ); ?></h4>
        <?php 
            $i = 1;

            foreach( $passenger_details as $passenger ){

                $passenger = wp_parse_args( $passenger, $passenger_defaults );

                echo "<p><strong>" . sprintf( __( "Passenger #%s", "your-slug" ), $i  ) . "</strong>" . "<br/>";
                echo __( "Title", "your-slug" ) . ' : ' . $passenger["title"] . "<br/>";
                echo __( "First Name", "your-slug" ) . ' : ' . $passenger["first_name"] . "<br/>";
                echo __( "Middle Name", "your-slug" ) . ' : ' . $passenger["middle_name"] . "<br/>";
                echo __( "Last Name", "your-slug" ) . ' : ' . $passenger["last_name"] . "<br/>";
                echo __( "Date of Birth", "your-slug" ) . ' : ' . $passenger["date_of_birth"] . "<br/>";
                echo __( "Dietary Required", "your-slug" ) . ' : ' . $passenger["dietary_required"] . "<br/>";
                echo __( "Dietary Preference", "your-slug" ) . ' : ' . $passenger["dietary_preference"] . "<br/>";

                echo "</p>";

                $i++;

            }

         ?>
    </div>
<?php }
}
add_action( "woocommerce_admin_order_data_after_order_details", "kia_display_order_data_in_admin" );

That still leaves the thankyou page and emails now, but the data is there.