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.
Further Reading on SmashingMag:
- Introducing The Magento Layout
- The Basics Of Creating A Magento Module
- How To Create An Affiliate Tracking Module In Magento
- 35 Free High-Quality E-Commerce Templates
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 interfaceMage_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.