How-To-Create-A-Custom-Shipping-Method-For-WooCommerce
Home » BLOG » WordPress » How To Create A Custom Shipping Method For WooCommerce

How To Create A Custom Shipping Method For WooCommerce

category:  WordPress

During Covid-19 crisis, it seems we have more brand-new online stores on the market. I have one client who run the business in Japan and want to use the shipping company from Japan. This shipping company uses the price list based on size and weight. Moreover, the shipping company offers the chilled charges which is an optional service fee. The shoppers can choose the extra charge for chilled fee if they want.

Client’s Requirements

  • The website sells and ship the products within Japan only.
  • Shipping fee will calculate based on area, size and weight. We will use the high shipping fee that we come up from size or weight.
  • Shipping fee includes tax.
  • On the checkout and cart pages, it will show the shipping fee based on the destination and chilled charged.
  • On the checkout and cart pages, shoppers choose the chilled charged as they want. It is an optional.

Shipping fee table we use in this tutorial
Below is the shipping fee table we use in this tutorial. In your case, you can change the calculation to meet your needs.

Shipping fee based on size and weight
Shipping fee based on size and weight

Reference links I use in this tutorial

Copy the code: shipping method api
Class attributes explanation: Exploring the shipping method api
WooCommmerce Code Referenece: WC_Shipping_Method

Extra plugins I use in this tutorial

  • WPC Composite Products for WooCommerce plugin – The plugin allows the shop owner to add multiple simple products into one package product (box set for example). This is my client’s requirement. You don’t need this plugin for creating your own shipping method.
  • Elementor plugin – The most popular and advanced page builder for WordPress.

We will do 3 steps today

  • Create the shipping zones. For example, Okinawa zone, Kyushu zone and so on.
  • Add the regions into each shipping zones. For example, Okinawa zone will be added all provinces within Okinawa area.
  • Create our custom shipping method associated to each shipping zone. For example, Okinawa shipping method, Kyushu shipping method and so on.

Create the Shipping Zones

With this tutorial, we will build the shipping methods based on the shipping zones. So we will set shipping zone as below.

shipping zone setting
Create the Shipping Zones

You can configure the shipping zone at “Woocommerce>Settings>Shipping>Shipping Zones>Add shipping zone“.

You will see each shipping zone, we will add the regions associated to the shipping zone. For example, for Okinawa zone, you will add all provinces from Okinawa area. This setting is a must for our tutorial.

Build our shipping methods plugin

Next, we will create our shipping methods plugin. Our shipping methods plugin will be structure as below.

ic-table-shipping-rate folder
classes folder
> okinawa-shipping-method.php
> kyushu-shipping-method.php
> chubu_hokuriku-shipping-method.php
> hokkaido-shipping-method.php
> kansai-shipping-method.php
> kanto_shinetsu-shipping-method.php
> shikoku-shipping-method.php
> tohoku-shipping-method.php
languages folder
> mo and po files
ic-table-shipping-rate.php

In order to create the plugin, following the steps below.

Create the plugin folder and main plugin file

  • Create new plugin folder names “ic-table-shipping-rate” under WordPress plugins folder.
  • Create new main plugin file names “ic-table-shipping-rate.php” under “ic-table-shipping-rate” folder
  • Add the code below into the “ic-table-shipping-rate.php
<?php
// https://docs.woocommerce.com/document/create-a-plugin/#section-7
if (!defined('ABSPATH')) {
    exit; // Exit if accessed directly
}

/**
 * Plugin Name: IC Shipping Fee Plugin
 * Plugin URI: 
 * Description: Create new Shipping Methods following the shipping rate from shipping provider 
 * Version: 1.0.0
 * Author: Apple Rinquest
 * Author URI: https://applerinquest.com
 * Text Domain: ic-table-shipping-rate
 * Domain Path: /languages/
 *
 */


// -------------------- Code starts here -----------------------------

/**
 * Check if WooCommerce plugin is active
 * https://docs.woocommerce.com/document/shipping-method-api/#section-6
 * Note that the checking will fail if the WooCommerce plugin folder is named anything other than "woocommerce"
 **/
// https://docs.woocommerce.com/document/create-a-plugin/#section-1
if (in_array('woocommerce/woocommerce.php', apply_filters('active_plugins', get_option('active_plugins')))) {

    // # load translation file
    add_action('init', 'quo_shipping_textdomain');
    function quo_shipping_textdomain()
    {
        load_plugin_textdomain(
            'ic-table-shipping-rate',
            false,
            basename(dirname(__FILE__)) . '/languages'
        );
    }

    // # Put your plugin code here
    function oas_shipping_method_init()
    {
        // https://docs.woocommerce.com/document/shipping-method-api/#section-3
        // Your class will go here

        include 'classes/okinawa-shipping-method.php';
        include 'classes/kyushu-shipping-method.php';
        include 'classes/shikoku-shipping-method.php';
        include 'classes/kansai-shipping-method.php';
        include 'classes/chubu_hokuriku-shipping-method.php';
        include 'classes/kanto_shinetsu-shipping-method.php';
        include 'classes/tohoku-shipping-method.php';
        include 'classes/hokkaido-shipping-method.php';

        // NOTE: add new shipping method here if you have more
    }
    add_action('woocommerce_shipping_init', 'oas_shipping_method_init');


    // tell WooCommerce that your custom shipping methods exist with the "add_oas_shipping_method" function
    function add_oas_shipping_method(
        $methods
    ) {
        // # add new shipping method here
        $methods['okinawa_shipping_method'] = 'WC_Okinawa_Shipping_Method';
        $methods['kyushu_shipping_method'] = 'WC_Kyushu_Shipping_Method';
        $methods['shikoku_shipping_method'] = 'WC_Shikoku_Shipping_Method';
        $methods['kansai_shipping_method'] = 'WC_Kansai_Shipping_Method';
        $methods['chubu_hokuriku_shipping_method'] = 'WC_Chubu_Hokuriku_Shipping_Method';
        $methods['kanto_shinetsu_shipping_method'] = 'WC_Kanto_Shinetsu_Shipping_Method';
        $methods['tohoku_shipping_method'] = 'WC_Tohoku_Shipping_Method';
        $methods['hokkaido_shipping_method'] = 'WC_Hokkaido_Shipping_Method';

        return $methods;
    }
    add_filter(
        'woocommerce_shipping_methods',
        'add_oas_shipping_method'
    );
} else {

    // If WooCommerce plugin is not activated, we will show the admin notice.
    if (!function_exists('WC')) {
        add_action('admin_notices', 'ic_fails_to_load');
        return;
    }
}



// ============================
// Calculation Shipping Fee
// ============================
function oas_shipping_fee($obj, $package)
{
    // # calculation goes here

    // Note: The destination data can get from $package
    // how to access the country from shipping address? You can access by $package["destination"]["country"];
    /* how the destination array structure looks like:
                      [destination] => Array
                        (
                            [country] => TH
                            [state] => TH-10
                            [postcode] => 10100
                            [city] => Tonpao
                            [address] => 
                            [address_1] => 
                            [address_2] => 
                        )
                     */

    // initial value
    $cost = 0;
    $tmp_cost = 0;
    $weight = 0;
    $_product = array();
    $product_components =  array();
    $chilled_fee = 0;
    $tmp_chilled_total_size = 0;
    $tmp_chilled_total_weight = 0;

    // calculate shipping fee per item
    foreach ($package['contents'] as $item_id => $values) {
        // # get a product data from Product Object

        // https://woocommerce.github.io/code-reference/classes/WC-Product-Simple.html
        // Link above shows what methods you can access to protected properties from the class such as get_weight(), get_type() and more.
        // Other product classes should use the same methods as WC-Product-Simple class since new product object will extend from the default WC product classes. if not, you must look at the associated product class in code reference.
        $_product = $values['data'];


        // # WPC Composite Products plugin will return "composite" product type.
        // Code location: wpc-composite-products.php>WC_Product_Composite Class
        if ($_product->is_type('composite') == 1) {

            // ====================
            // # Composite product
            // ====================

            // # Composite product shipping fee calculation: 
            // 1. we want to keep the parent product size for shipping fee calcuation.
            // 2. we sum up all weight from the parent product and all component products for shipping fee calculation.
            // 3. we calculate the shipping fee from 1 and 2         


            // # get the child products from "WPC composite products" plugin
            // Code location: wpc-composite-products.php>WC_Product_Composite Class>get_components method
            $product_components = get_post_meta($values['product_id'], 'wooco_components', true);

            /*
                            $product_components array structure:
                                Array
                                (
                                    [0] => Array
                                        (
                                            [name] => 60 Size 2 Kg
                                            [desc] => Description
                                            [type] => products
                                            [orderby] => default
                                            [order] => default
                                            [products] => 1101
                                            [categories] => 
                                            [tags] => 
                                            [exclude] => 
                                            [default] => null
                                            [optional] => no
                                            [price] => 
                                            [qty] => 1
                                            [custom_qty] => yes
                                            [min] => 0
                                            [max] => 1000
                                        )
                                )                         
                            */


            // # Parent product size
            // Weight (kg)
            // https://woocommerce.wp-a2z.org/oik_api/wc_get_weight/
            $weight = (strlen($_product->get_weight()) === 0 ? 0 : wc_get_weight((int)$_product->get_weight(), 'kg', 'g'));

            // Dimension (cm)
            $width = (strlen($_product->get_width()) === 0 ? 0 : $_product->get_width());
            $length = (strlen($_product->get_length()) === 0 ? 0 : $_product->get_length());
            $height = (strlen($_product->get_height()) === 0 ? 0 : $_product->get_height());

            // Overall total size (cm)
            $parent_size = $width + $length + $height;
            $tmp_chilled_total_size = $tmp_chilled_total_size + $parent_size;


            // # Child product size
            $compoment_weight = 0;
            if (is_array($product_components)) {
                foreach ($product_components as $compoment) {
                    $tmp_prod = explode(',', $compoment['products']);
                    if (count($tmp_prod) > 0) {
                        foreach ($tmp_prod as $prod_id) {
                            $obj_prod = wc_get_product((int)$prod_id);
                            $compoment_weight =  $compoment_weight + (strlen($obj_prod->get_weight()) === 0 ? 0 : wc_get_weight((int)$obj_prod->get_weight(), 'kg', 'g'));
                        }
                    } else {
                        $obj_prod = wc_get_product($compoment['products']);
                        $compoment_weight =  $compoment_weight + (strlen($obj_prod->get_weight()) === 0 ? 0 : wc_get_weight((int)$obj_prod->get_weight(), 'kg', 'g'));
                    }
                }
            }

            // calculate the shipping fee per item
            $tmp_cost = $tmp_cost + $obj->oas_size_criteria($parent_size, ($weight + $compoment_weight));
            $tmp_chilled_total_weight = $tmp_chilled_total_weight + ($weight + $compoment_weight);

            // multiply shipping fee with qty per item
            $tmp_cost = $tmp_cost * floatval($values['quantity']);

            // display on cart and checkout pages
            $cost = $cost + $tmp_cost;


            // debug:
            // echo 'parent weight=' . $weight;
            // echo '<br>';
            // echo 'parent size=' . $parent_size;
            // echo '<br>';
            // echo 'parent qty=' . $values['quantity'];
            // echo '<br>';
            // echo 'child weight=' . $compoment_weight;
            // echo '<br>';
            // echo 'parent and child products weight=' . ($weight + $compoment_weight);
            // echo '<br>';
            // echo 'total cost per item =' . $tmp_cost;
            // echo '<br>';
            // echo '<hr>';


            // clear 
            $tmp_cost = 0;
        }  // composite product ends

        else {

            // =================
            // # simple product
            // =================


            // Weight (kg)
            $weight = (strlen($_product->get_weight()) === 0 ? 0 : wc_get_weight((int)$_product->get_weight(), 'kg', 'g'));
            $tmp_chilled_total_weight = $tmp_chilled_total_weight + $weight;

            // Dimension (cm)
            $width = (strlen($_product->get_width()) === 0 ? 0 : $_product->get_width());
            $length = (strlen($_product->get_length()) === 0 ? 0 : $_product->get_length());
            $height = (strlen($_product->get_height()) === 0 ? 0 : $_product->get_height());

            // Overall total size (cm)
            $total_size = $width + $length + $height;
            $tmp_chilled_total_size = $tmp_chilled_total_size + $total_size;

            // shipping fee per item
            $tmp_cost = $tmp_cost + $obj->oas_size_criteria($total_size, $weight);

            // multiply shipping fee with qty in the cart for each item
            $tmp_cost = $tmp_cost * floatval($values['quantity']);

            // display on cart(if set at WooCommerce settings) and checkout pages
            $cost = $cost + $tmp_cost;


            // // debug
            // echo '$weight=' . $weight;
            // echo '<br>';
            // echo '$total_size=' . $total_size;
            // echo '<br>';
            // echo '$tmp_cost=' . $tmp_cost;
            // echo '<br>';
            // echo '<hr>';


            // clear 
            $tmp_cost = 0;
        } // single product ends


    } // loop



    // # display shipping fee on the cart(if woocommerce is set) and checkout pages
    $rate = array(
        'id' => $obj->id . $obj->instance_id,
        'label' => __('Shipping Fee', 'ic-table-shipping-rate'),   // Label for the rate
        'cost'  => $cost,  // Amount for shipping or an array of costs (for per item shipping)
        'taxes' => 'false',   // Pass an array of taxes, or pass nothing to have it calculated for you, or pass 'false' to calculate no tax for this method
        'calc_tax' => 'per_order' // Calc tax per_order or per_item. Per item needs an array of costs passed via 'cost'
    );
    // Register the rate
    $obj->add_rate($rate);


    // # Chilled fee per order
    $chilled_fee = oas_chilled_size_criteria(
        $tmp_chilled_total_size,
        $tmp_chilled_total_weight
    );

    // debug:
    // echo '<pre>';
    // echo 'tmp_chilled_total_size=' . $tmp_chilled_total_size;
    // echo '<br>';
    // echo 'tmp_chilled_total_weight=' . $tmp_chilled_total_weight;
    // echo '<br>';
    // echo ' $chilled_fee=' .  $chilled_fee;
    // echo '<br>';
    // echo '</pre>';


    // # display chilled fee per order
    $rate = array(
        'id' => $obj->id . $obj->instance_id . 'chilled_fee',
        'label' => __(
            'Shipping Fee including chilled Fee',
            'ic-table-shipping-rate'
        ),   // Label for the rate
        'cost'  => floatval($cost) + floatval($chilled_fee),  // Amount for shipping or an array of costs (for per item shipping)
        'taxes' => 'false',   // Pass an array of taxes, or pass nothing to have it calculated for you, or pass 'false' to calculate no tax for this method
        'calc_tax' => 'per_order' // Calc tax per_order or per_item. Per item needs an array of costs passed via 'cost'
    );
    // Register the rate
    $obj->add_rate($rate);


    /**
     * # Add more rate options on the checkout page
     * Your shipping method can pass as many rates as you want – just ensure that the id for each is different. 
     * The user will get to choose rate during checkout.
     * https://wordpress.stackexchange.com/questions/368685/add-multiple-shipping-rates-from-add-rate-function-with-custom-id
     * 
     */
}



// ============================
// OAS chilled fee calculation
// ============================

/**
 * OAS chilled fee - size criteria
 * 
 **/
function oas_chilled_size_criteria($size, $weight)
{
    $cost = 0;

    // # get the highest cost and return back
    // we check from size criteria first then check the weight criteria

    // 60 size
    if (
        $size > 0 && $size <= 60
    ) {
        // cost by size
        $cost = 230;

        // cost by weight
        $cost = oas_chilled_weight_criteria($weight, $cost);
    }

    // 80 size
    if (
        $size > 60 && $size <= 80
    ) {
        $cost = 360;

        // cost by weight
        $cost = oas_chilled_weight_criteria($weight, $cost);
    }

    // 100 size
    if (
        $size > 80 && $size <= 100
    ) {
        $cost = 680;

        // cost by weight
        $cost = oas_chilled_weight_criteria($weight, $cost);
    }

    // 120 size
    if (
        $size > 100 && $size <= 120
    ) {
        $cost = 680;

        // cost by weight
        $cost = oas_chilled_weight_criteria($weight, $cost);
    }

    // over 120 size
    if (
        $size > 120
    ) {
        $cost = 680;

        // cost by weight
        $cost = oas_chilled_weight_criteria($weight, $cost);
    }

    return $cost;
}


/**
 * OAS  chilled fee - weight criteria
 */
function oas_chilled_weight_criteria($weight, $cost)
{
    // check by weight criteria
    if ($weight > 0 && $weight <= 2 && $cost < 230) {
        $cost = 230;
    } elseif ($weight > 2 && $weight <= 5 && $cost < 360) {
        $cost = 360;
    } elseif ($weight > 5 && $weight <= 10 && $cost < 680) {
        $cost = 680;
    } elseif ($weight > 10 && $weight <= 15 && $cost < 680) {
        $cost = 680;
    } elseif ($weight > 15 && $cost < 680) {
        $cost = 680;
    }

    return $cost;
}



// ============================
// admin notice
// ============================
/**
 * Fires admin notice when Elementor is not installed and activated.
 *
 * @since 1.0.0
 *
 * @return void
 */

function ic_fails_to_load()
{
    $class = 'notice notice-error';
    /* translators: %s: html tags */
    $message = sprintf(__('The %1$sIC Shipping Table Rate %2$s plugin requires %1$sWooCommerce%2$s plugin installed & activated.', 'woo-cart-abandonment-recovery'), '<strong>', '</strong>');

    printf('<div class="%1$s"><p>%2$s</p></div>', esc_attr($class), wp_kses_post($message));
}

Explanation code above

  • We check WooCommerce plugin is activated or not. If not, we will display the admin notice that our plugin needs WooCommerce plugin to activate.
  • Create our custom shipping method class by extending the WC_Shipping_Method class
  • In our class, we set the class properties such as title, description and so on. Don’t forget to add the shipping zone support in order to show our shipping method on the shipping methods list in Shipping zone.
  • In our class, we create the calculate_shipping method for our shipping fee calculation and display shipping fee on cart and checkout pages.
  • That’s it. It is the whole concept to create your own shipping method and shipping fee calculation.

Create the class files

  • create the classes folder under ic-table-shipping-rate folder
  • create new okinawa-shipping-method.php for shipping fee in Okinawa area.
  • add the code below into okinawa-shipping-method.php
<?php
if (!defined('ABSPATH')) {
    exit; // Exit if accessed directly
}

// =========================================
// # WC_Okinawa_Shipping_Method class starts
// =========================================
if (!class_exists('WC_Okinawa_Shipping_Method')) {
    class WC_Okinawa_Shipping_Method extends WC_Shipping_Method
    {
        /**
         * Constructor for your shipping class
         * The instance ID is passed to this.
         *
         * @access public
         * @return void
         */
        public function __construct($instance_id = 0)
        {
            $this->instance_id        = absint($instance_id); // Unique instance ID of the method (zones can contain multiple instances of a single shipping method). This ID is stored in the database.
            $this->id                 = 'okinawa_shipping_method'; // A unique text based ID for the new shipping method you are adding.
            $this->method_title       = __('Okinawa Shipping Fee', 'ic-shipping-table');  // The title applied to the shipping method. This is not the same title that will be displayed on the front-end on checkout page but used only for backend settings.
            $this->method_description = __('Refer to OAS price list for Okinawa zone - No need to set up here. Table rate is in the code.', 'ic-shipping-table'); // Description shown in admin
            $this->enabled            = 'yes'; // Stores whether or not the instance or method is enabled. In the above snippet we have assigned the instance which is saved in database. This value will be displayed in Shipping Methods section of the shipping zone.
            $this->title              = __('Okinawa Shipping Fee', 'ic-shipping-table');

            $this->supports = array(
                'shipping-zones',
                'instance-settings',
            );
            // # Supports property is used to determine which all features the new shipping method will support. This property is an array with possible values as:
            // 1. shipping-zones – Shipping zone functionality.
            // 2. instance-settings – Instance settings screens.
            // 3. settings – Non-instance settings screens. Enabled by default for backward compatibility with methods before instances existed.
            // 4. instance-settings-modal – Allows the instance settings to be loaded within a modal in the zones UI.
        }


        /**
         * calculate_shipping function.
         * # calculate_shipping() is a method which you use to add your rates -
         * WooCommerce will call this when doing shipping calculations. 
         * 
         * This function is called to calculate shipping for a package. The function accepts an array containing the package of items to be shipped. 
         *
         * @access public
         * @param mixed $package
         * @return void
         */
        public function calculate_shipping($package = array())
        {
            // https://docs.woocommerce.com/document/shipping-method-api/#section-5
            // # This is where you'll add your rates to cart(if you set to show the fee calculation on cart) and checkout pages

            oas_shipping_fee($this, $package);
        }


        /**
         * OAS shipping fee table for Okinawa
         * 
         **/
        function oas_size_criteria($size, $weight)
        {
            $cost = 0;

            // # get the highest cost and return back
            // we check from size criteria first then check the weight criteria

            // 60 size
            if (
                $size > 0 && $size <= 60
            ) {
                // cost by size
                $cost = 780;

                // cost by weight
                $cost = $this->oas_weight_criteria($weight, $cost);
            }

            // 80 size
            if (
                $size > 60 && $size <= 80
            ) {
                $cost = 990;

                // cost by weight
                $cost = $this->oas_weight_criteria($weight, $cost);
            }

            // 100 size
            if (
                $size > 80 && $size <= 100
            ) {
                $cost = 1220;

                // cost by weight
                $cost = $this->oas_weight_criteria($weight, $cost);
            }

            // 120 size
            if (
                $size > 100 && $size <= 120
            ) {
                $cost = 1440;

                // cost by weight
                $cost = $this->oas_weight_criteria($weight, $cost);
            }

            // 140 size
            if (
                $size > 120 && $size <= 140
            ) {
                $cost = 1670;

                // cost by weight
                $cost = $this->oas_weight_criteria($weight, $cost);
            }

            // 160 size
            if (
                $size > 140 && $size <= 160
            ) {
                $cost = 1890;

                // cost by weight
                $cost = $this->oas_weight_criteria($weight, $cost);
            }


            return $cost;
        }


        /**
         * OAS shipping fee - weight criteria
         */
        function oas_weight_criteria($weight, $cost)
        {
            // check by weight criteria
            if ($weight > 0 && $weight <= 2 && $cost < 780) {
                $cost = 780;
            } elseif ($weight > 2 && $weight <= 5 && $cost < 990) {
                $cost = 990;
            } elseif ($weight > 5 && $weight <= 10 && $cost < 1220) {
                $cost = 1220;
            } elseif ($weight > 10 && $weight <= 15 && $cost < 1440) {
                $cost = 1440;
            } elseif ($weight > 15 && $weight <= 20 && $cost < 1670) {
                $cost = 1670;
            } elseif ($weight > 20 && $weight <= 25 && $cost < 1890) {
                $cost = 1890;
            }

            return $cost;
        }
    }
}
        // WC_Okinawa_Shipping_Method class ends

As you can see from the okinawa-shipping-method.php above. We create our new shipping method by extending from WC_Shipping_Method class. Then we create the construct and calculate_shipping methods following the Shipping Method API doc. The oas_size_criteria and oas_weight_criteria methods are specific for my client’s shipping rate. These methods are the idea for you to create your own custom functionalities.

Other shipping methods

For other shipping methods of the rest of shipping zones, the code will be the same as okinawa-shipping-method.php. The difference is the size and weight calculation (oas_size_criteria and oas_weight_criteria methods). So I don’t add any more code here. I think you get the idea, right?

Languages folder

The languages folder is for translation files (PO and MO file). If you have no plan for translation plugin, you can ignore this folder. But if you want your plugin is translated, you can use POedit tool for creating the PO and MO files.

The result

Once you finish your plugin, activate it. Then add some products to the cart. Visit the cart page then enter the address that match your shipping zone and shipping method you create in your plugin. Finally you should see something just like the screenshot below.

Cart page with 2 shipping fee options

Create custom shipping method for WooCommerce
Cart page with 2 options for shipping fee

Checkout page with 2 shipping fee options

Create custom shipping method for WooCommerce
Checkout page with 2 options for shipping fee

Conclusion

Building your own shipping method, it seems simple. WooCommerce provides a well-documented and easy to follow. For this tutorial, you can do the code refactoring in order to make it more reusable if you like. I did this custom shipping method within the short deadline (1 day). Happy coding 🙂