How To Create Custom Shipping Methods In Magento

About The Author

Matthew wrote his first piece of code when he was just five years old in Locomotive BASIC. Ever since he has dabbled in everything from MATLAB to Java, but took … More about Matthew ↬

Email Newsletter

Weekly tips on front-end & UX.
Trusted by 200,000+ folks.

In this article, Matthew Haworth will use Magento’s shipping-method code abstraction to create a shipping carrier. He will be covering topics such as extending the abstract shipping class and implement the required methods, allowing tracking codes to be set against an order, work with promotions to allow for free shipping, and much more!

In this tutorial, we will use Magento’s powerful shipping-method code abstraction to create a shipping carrier. We will create three shipping methods that provide a fixed shipping price, allow for free shipping promotions, define logic based on an item’s weight and, finally, make it all configurable in the admin panel.

We will cover the following:

  • Extend the abstract shipping class and implement the required methods.
  • Make the shipping method configurable in Magento’s admin panel.
  • Work with promotions to allow for free shipping.
  • Allow tracking codes to be set against an order.


Before We Start

This tutorial assumes that you are familiar with how to create a Magento module. If you are not, please first refer to an earlier tutorial in this series, “The Basics of Creating a Magento Module.” To begin, you will need a Community or Enterprise installation of Magento, either locally or on a server that you are able to access.

The logic we will implement in this tutorial could be client-specific, so we will implement our module as a “local module” and, therefore, create it in app/code/local. Let’s start by creating the following file structure:


app
  - code
    - local
      - SmashingMagazine
        - MyCarrier
          - Model
            - Carrier.php
          - etc
            - config.xml
            - system.xml
  - etc
    - modules
      - SmashingMagazine_MyCarrier.xml

Now we can create SmashingMagazine_MyCarrier.xml:


<?xml version="1.0"?>
<config>
    <modules>
        <SmashingMagazine_MyCarrier>
            <active>true</active>
            <codePool>local</codePool>
            <depends>
                <Mage_Shipping />
            </depends>
        </SmashingMagazine_MyCarrier>
    </modules>
</config>

Notice the dependency on the shipping module. This ensures that our SmashingMagazine MyCarrier module will load after the Mage Shipping module, and it will throw an error if the Mage Shipping module has been disabled.

Carriers, Methods, Requests And Results

Before continuing, we should understand the terminology that Magento uses throughout its shipping abstraction. A “carrier” represents a shipping carrier in the sense you would expect (DPD, FedEx, etc.). Each carrier has one or many shipping methods, which contain the carrier code, the carrier title, the method code, the method title, a price to be paid by the customer and a cost of shipping to the retailer (optional).

During the checkout process, Magento creates a shipping-rate “request” object that contains all of the shipping information. The request can be used to determine which rates apply. For example, an “express” shipping method might not apply to orders under $10. All applicable rates are then “appended” to a shipping-rate “result” object, which generates a list of methods for the customer to choose from.

The following list names these concepts defined above, along with their representation as Magento classes:

  • Request Mage_Shipping_Model_Rate_Request
  • Result Mage_Shipping_Model_Rate_Result
  • Method Mage_Shipping_Model_Rate_Result_Method
  • Carrier Any class that extends the abstract class Mage_Shipping_Model_Carrier_Abstract and implements the interface Mage_Shipping_Model_Carrier_Interface

Extending The Shipping Abstract

To create our shipping carrier, we need to extend Mage_Shipping_Model_Carrier_Abstract, implement Mage_Shipping_Model_Carrier_Interface and add the required abstract methods.

The most important method is collectRates. This is the method that receives a shipping request, appends applicable shipping methods and returns a shipping result.

Copy the following code into app/code/local/SmashingMagazine/MyCarrier/Model/Carrier.php:


<?php
class SmashingMagazine_MyCarrier_Model_Carrier
    extends Mage_Shipping_Model_Carrier_Abstract
    implements Mage_Shipping_Model_Carrier_Interface
{
    protected $_code = 'smashingmagazine_mycarrier';

    public function collectRates(
        Mage_Shipping_Model_Rate_Request $request
    )
    {
        return Mage::getModel('shipping/rate_result');
    }

    public function getAllowedMethods()
    {
        return array();
    }
}

This is the skeleton for a shipping method class, but it is pretty useless because we have no shipping methods.

Let’s start by hardcoding a method. This method will be called “standard” and have a price of $4.99. For now, we will assume there is no cost to the retailer.


<?php
class SmashingMagazine_MyCarrier_Model_Carrier
    extends Mage_Shipping_Model_Carrier_Abstract
    implements Mage_Shipping_Model_Carrier_Interface
{
    protected $_code = 'smashingmagazine_mycarrier';

    public function collectRates(
        Mage_Shipping_Model_Rate_Request $request
    )
    {
        $result = Mage::getModel('shipping/rate_result');
        /* @var $result Mage_Shipping_Model_Rate_Result */

        $result->append($this->_getStandardShippingRate());

        return $result;
    }

    protected function _getStandardShippingRate()
    {
        $rate = Mage::getModel('shipping/rate_result_method');
        /* @var $rate Mage_Shipping_Model_Rate_Result_Method */

        $rate->setCarrier($this->_code);
        /**
         * getConfigData(config_key) returns the configuration value for the
         * carriers/[carrier_code]/[config_key]
         */
        $rate->setCarrierTitle($this->getConfigData('title'));

        $rate->setMethod('standand');
        $rate->setMethodTitle('Standard');

        $rate->setPrice(4.99);
        $rate->setCost(0);

        return $rate;
    }

    public function getAllowedMethods()
    {
        return array(
            'standard' => 'Standard',
        );
    }
}

Now we are just one step away from a working shipping method — the module configuration file.

Module Configuration

The module configuration has the standard structure (as detailed in “The Basics of Creating a Magento Module”). Copy the following into app/code/local/SmashingMagazine/MyCarrier/etc/config.xml:


<?xml version="1.0" encoding="UTF-8"?>
<config>
    <modules>
        <SmashingMagazine_MyCarrier>
            <module>0.0.1</module>
        </SmashingMagazine_MyCarrier>
    </modules>
    <global>
        <models>
            <smashingmagazine_mycarrier>
                <class>SmashingMagazine_MyCarrier_Model</class>
            </smashingmagazine_mycarrier>
        </models>
    </global>
    <!-- Default configuration -->
    <default>
        <carriers>
            <smashingmagazine_mycarrier>
                <active>1</active>
                <!--
                     This configuration should not be made visible
                     to the administrator, because it specifies
                     the model to be used for this carrier.
                -->
                <model>smashingmagazine_mycarrier/carrier</model>
                <!--
                    The title as referenced in the carrier class
                -->
                <title>Smashing Magazine Carrier</title>
                <!--
                    The sort order specifies the position that
                    this carrier appears relative to the other
                    carriers available in checkout.
                -->
                <sort_order>10</sort_order>
                <!--
                    Out of the box, Magento offers shipping
                    carriers the ability to restrict themselves
                    to specific countries. For this configuration
                    option, 0 means allow all countries available,
                    and 1 means allow all countries specified
                    in the country list that we will add later
                    in system.xml
                -->
                <sallowspecific>0</sallowspecific>
            </smashingmagazine_mycarrier>
        </carriers>
    </default>
</config>

This default configuration “registers” the model we have just created as a shipping carrier. As you may know, Magento merges all of its configuration XML together and caches the result (if the cache is enabled). When a customer loads the shipping-method list, Magento loops through all of the carriers in the carriers node of the configuration and loads the shipping methods from the models determined by the “active” carriers.

We should now be able to see our shipping method in the checkout.

Making It Configurable

We have already specified the default configuration for this module. So, let’s make our module configurable in the admin panel by copying the following into app/code/local/SmashingMagazine/etc/system.xml:


<?xml version="1.0" encoding="UTF-8"?>
<config>
    <sections>
        <carriers translate="label" module="shipping">
            <groups>
                <smashingmagazine_mycarrier translate="label">
                    <label>Smashing Magazine Carrier</label>
                    <frontend_type>text</frontend_type>
                    <sort_order>2</sort_order>
                    <show_in_default>1</show_in_default>
                    <show_in_website>1</show_in_website>
                    <show_in_store>1</show_in_store>
                    <fields>
                        <!--
                            The following fields are available
                            to modify in the admin panel.
                            The values are saved in the
                            database.

                            This shipping carrier abstract checks
                            this value to determine whether
                            the carrier should be shown.
                        -->
                        <active translate="label">
                            <label>Enabled</label>
                            <frontend_type>select</frontend_type>
                            <source_model>adminhtml/system_config_source_yesno</source_model>
                            <sort_order>1</sort_order>
                            <show_in_default>1</show_in_default>
                            <show_in_website>1</show_in_website>
                            <show_in_store>0</show_in_store>
                        </active>
                        <!--
                            This value can be used to specify a
                            custom title for our method.
                        -->
                        <title translate="label">
                            <label>Title</label>
                            <frontend_type>text</frontend_type>
                            <sort_order>2</sort_order>
                            <show_in_default>1</show_in_default>
                            <show_in_website>1</show_in_website>
                            <show_in_store>1</show_in_store>
                        </title>
                        <!--
                            The sort order is used in Magento
                            to determine what order the carrier
                            will appear in relative to the
                            other carriers available.
                        -->
                        <sort_order translate="label">
                            <label>Sort Order</label>
                            <frontend_type>text</frontend_type>
                            <sort_order>100</sort_order>
                            <show_in_default>1</show_in_default>
                            <show_in_website>1</show_in_website>
                            <show_in_store>0</show_in_store>
                        </sort_order>
                        <!--
                            This value is used to specify whether
                            the carrier is available only for
                            specific countries or all countries
                            available in the current Magento
                            installation.
                        -->
                        <sallowspecific translate="label">
                            <label>Ship to Applicable Countries</label>
                            <frontend_type>select</frontend_type>
                            <sort_order>90</sort_order>
                            <frontend_class>shipping-applicable-country</frontend_class>
                            <source_model>adminhtml/system_config_source_shipping_allspecificcountries</source_model>
                            <show_in_default>1</show_in_default>
                            <show_in_website>1</show_in_website>
                            <show_in_store>0</show_in_store>
                        </sallowspecific>
                        <!--
                            If 'specific countries' is chosen
                            in the previous option, then this field
                            allows the administrator to specify
                            which specific countries this carrier
                            should be available for.
                        -->
                        <specificcountry translate="label">
                            <label>Ship to Specific Countries</label>
                            <frontend_type>multiselect</frontend_type>
                            <sort_order>91</sort_order>
                            <source_model>adminhtml/system_config_source_country</source_model>
                            <show_in_default>1</show_in_default>
                            <show_in_website>1</show_in_website>
                            <show_in_store>0</show_in_store>
                            <can_be_empty>1</can_be_empty>
                        </specificcountry>
                    </fields>
                </smashingmagazine_mycarrier>
            </groups>
        </carriers>
    </sections>
</config>

These fields are visible in the admin panel by navigating to System → Configuration → Shipping Method → Smashing Magazine Carrier.

Using Multiple Shipping Methods

Express Shipping

So far, we have added a standard shipping method for the price of $9.99. However, the customer may wish to pay more to receive their order faster. The following code creates a shipping rate with a higher price and different shipping code:


protected function _getExpressShippingRate()
{
    $rate = Mage::getModel('shipping/rate_result_method');
    /* @var $rate Mage_Shipping_Model_Rate_Result_Method */
    $rate->setCarrier($this->_code);
    $rate->setCarrierTitle($this->getConfigData('title'));
    $rate->setMethod('express');
    $rate->setMethodTitle('Express (Next day)');
    $rate->setPrice(12.99);
    $rate->setCost(0);
    return $rate;
}

To make this shipping rate appear next to the standard rate that we created earlier, we will need to modify the code in the collectRates method to append the new rate. Add the following before the return statement:


$result->append($this->_getExpressShippingRate());

Finally, add the shipping method to the allowed methods array in getAllowedMethods:


public function getAllowedMethods()
{
    return array(
        'standard' => 'Standard',
        'express' => 'Express',
    );
}

Free Shipping

Many websites offer free shipping when a customer spends over a certain amount or satisfies certain conditions. We need to be able to do the same here. In Magento, you can set up a “Shopping Cart Rule.” With it, you can specify a set of conditions and define actions if those conditions are met; one of those actions is free shipping.

If free shipping is available for a customer, then the request object will populated with is_free_shipping set to 1. We need to check for and handle this possibility in our shipping method. Add the following before the return statement in the collectRates method:


if ($request->getFreeShipping()) {
    /**
     *  If the request has the free shipping flag,
     *  append a free shipping rate to the result.
     */
    $freeShippingRate = $this->_getFreeShippingRate();
    $result->append($freeShippingRate);
}

Add the following code to app/code/local/SmashingMagazine/MyCarrier/Model/Carrier.php:


protected function _getFreeShippingRate()
{
    $rate = Mage::getModel('shipping/rate_result_method');
    /* @var $rate Mage_Shipping_Model_Rate_Result_Method */
    $rate->setCarrier($this->_code);
    $rate->setCarrierTitle($this->getConfigData('title'));
    $rate->setMethod('free_shipping');
    $rate->setMethodTitle('Free Shipping (3 - 5 days)');
    $rate->setPrice(0);
    $rate->setCost(0);
    return $rate;
}

Remember to add the method to the allowed methods array:


public function getAllowedMethods()
{
    return array(
        'standard' => 'Standard',
        'express' => 'Express',
        'free_shipping' => 'Free Shipping',
    );
}

Taking It A Bit Further

Tracking Deliveries

Tracking numbers may be added to shipments through either the admin panel or an API. But to make our shipping methods visible in the admin panel, we will have to overwrite the isTrackingAvailable method in the abstract to return true.

Add the following method to the end of SmashingMagazine_MyCarrier_Model_Carrier.


public function isTrackingAvailable()
{
    return true;
}

You should now see the shipping carriers and methods available in the delivery courier drop-down menu when you try to place a shipment in the admin panel.

Using The Weight

Earlier, we added a more expensive express shipping method. But heavier items that require complex shipping arrangements might not be available for next-day delivery. We can check for this using the weight attribute of the request object by wrapping the code that appends the shipping method to the shipping result:


// ...
$expressWeightThreshold =
    $this->getConfigData('express_weight_threshold');

$eligibleForExpressDelivery = true;
foreach ($request->getAllItems() as $_item) {
    if ($_item->getWeight() > $expressWeightThreshold) {
        $eligibleForExpressDelivery = false;
    }
}

if ($eligibleForExpressDelivery) {
    $result->append($this->_getExpressShippingRate());
}
// ...

Notice that we have added a reference to the configuration. To make this appear in the admin panel, we need to add the following XML to the app/code/local/SmashingMagazine/MyCarrier/etc/system.xml file in the fields node:


<express_weight_threshold translate="label">
    <label>Express Weight Threshold</label>
    <frontend_type>text</frontend_type>
    <sort_order>100</sort_order>
    <show_in_default>1</show_in_default>
    <show_in_website>1</show_in_website>
    <show_in_store>0</show_in_store>
</express_weight_threshold>

Summary

With a relatively small amount of code, we have been able to define our own shipping logic that integrates with checkout, the admin panel and even the shopping-cart promotions. You can learn much more about creating shipping modules in Magento by looking at the examples in the core files — namely, Mage_Usa and Mage_Shipping.

The code from this tutorial can be downloaded here.

I welcome any questions and would love to hear your feedback in the comments area below.

Further Reading

Smashing Editorial (al, ea, mrn)