Abstracting Shipping APIs

Share this article

You have your new custom e-commerce store almost finished. The only thing left is to figure out how to calculate shipping charges for your customers. You don’t want to go with a standard flat rate to every address because you know you’ll be over charging some customers, and more importantly under charging others. Wouldn’t it be great shipping charges could be calculated based on the weight/size of the item(s) and the destination? Maybe you could even offer an accurate price quote for overnight shipping! You have a UPS account, and you’ve checked out their API, but it looks pretty complex. If you hard code your site to use the API, you’ll be up for a lot of work if you need to change shippers. Your cousin is a sales rep with FedEx and he swears he can get you better rates with them. Some of your customers only us PO Boxes, so those items have to be shipped by the Post Office. What do you do? You may have heard of database abstraction, a practice which allows you to use many different databases with a common set of commands. That’s exactly what you can do here! To solve all of these problems, you can decouple the shipping task from the rest of your code and build an abstraction layer. Once you’re finished, it won’t matter if you’re shipping a package by UPS, FedEx, or the USPS. The functions your core application will invoke will all be the same, and that makes your life a lot easier!

Getting Started with UPS

In this article I’ll focus on using the UPS API, but by writing a plugin for different shippers (such as FedEx or USPS), you can access their services as well with only negligible, if any, code changes to your core application. In order to get started with using UPS, you will need to sign up for an online account at www.ups.com using your existing shipper number. Make sure to pick a username and password that you will be comfortable using for a while as the API requires both of them for every call. Next, go to https://www.ups.com/upsdeveloperkit and register for access to the UPS API. This is where you will obtain your API key and are able to download documentation for the different API packages. (Note: There’s a known issue with this section of UPS’s site and Chrome will sometimes return a blank page. You may need to use an alternate browser.) Keep in mind that when you use the UPS API (or any shipper’s API for that matter), you agree to follow their rules and procedures. Make sure to review and follow them, especially including their instructions before using your code in production. Next download or clone the shipping abstraction layer package from GitHub at github.com/alexfraundorf-com/ship and upload it to your server that is running PHP 5.3 or later. Open the includes/config.php file. You’ll need to enter your UPS details here, and the field names should all be self-explanatory. Note that the UPS shipper address needs to match what UPS has on file for your account or an error will occur.

Defining Shipments and Packages

Now to define a Shipment object. At instantiation, it will accept an array containing the receiver’s information, and optionally a ship from address if it is different from the shipper’s information in our config file.
<?php
// create a Shipment object
$shipment = new ShipShipment($shipmentData);
Next we need some details about what we are shipping. Let’s create a Package object which accepts the weight, package dimensions, and an optional array of some basic options such as a description, if a signature is required, and the insured amount. The newly instantiated Package(s) are then added to the Shipment object. Software imitating life just makes sense: Every package belongs to a shipment, and every shipment must have at least one package.
<?php
// create a Package object and add it to the Shipment (a 
// shipment can have multiple packages)

// this package is 24 pounds, has dimensions of 10 x 6 x 12
// inches, has an insured value of $274.95, and is being
// sent signature required
$package1 = new ShipPackage(
    24,
    array(10, 6, 12),
    array(
        'signature_required' => true,
        'insured_amount' => 274.95
    )
);
$shipment->addPackage($package1);

// weight and dimensions can be integers or floats,
// although UPS always rounds up to the next whole number.
// This package is 11.34 pounds and has dimensions of
// 14.2 x 16.8 x 26.34 inches
$package2 = new ShipPackage(
    11.34,
    array(14.2, 16.8, 26.34)
);
$shipment->addPackage($package2);

Behind the Curtain: The Shipment Object

Open Awsp/Ship/Shipment.php and we’ll examine the Shipment object, which basically will hold everything that our shipper plugins need to know about the shipment. The constructor accepts an array of the shipment data (and stores it as an object property) which is the receiver’s information and optionally the ship from information if it differs from the shipper’s address. Next the constructor calls sanitizeInput() to make sure that array is safe to use, and isShipmentValid() to make sure that all required information has been provided. Besides that, we have a public method get() which accepts a field name (array key) and returns the corresponding value from the shipment data array, and the public functions addPackage() and getPackages() to, you guessed it, add a package to the shipment and retrieve the Package objects that belong to the shipment.

Behind the Curtain: The Package Object(s)

Open Awsp/Ship/Package.php and we’ll examine the Package object, which basically will hold everything that our shipper plugins need to know about the individual package. Package objects are part of the Shipment object, and the Shipment can have as many Package objects as needed. The Package constructor accepts the package weight, dimensions (in any order), and an optional array of options such as description, type, insured amount, and whether a signature is required. The weight and options are set in object properties and the dimensions are put in order from longest to shortest. We then assign them in order to the object properties $length, $width, and $height. This is important to the shipper plugins because length must always be the longest dimension. It then uses isPackageValid()
to make sure all needed parameters are present and of the proper type. Finally calculatePackageSize() is used to figure out the package’s size (length plus girth), which will be used by some shipper plugins. Other public functions available from the Package object are get() which returns a property of the object, getOption() which returns a specific option’s setting, and several helper functions for converting weight and length for the shipper plugins.

Shipper Plugins

We have a shipment with packages, and now we need to access the shipper plugin that we want to use. The plugin will accept the Shipment object along with the $config array (defined in includes/config.php).
<?php
// create the shipper object and pass it the shipment
// and config data
$ups = new ShipUps($shipment, $config);
Our Ups object, or any other shipper plugin we create later, will implement the ShipperInterface, our contract that allows us to guarantee that no matter which shipper we use, the public functions (interface) will always be the same. As shown in this excerpt from ShipperInterface.php, all of our shipper plugins must have a setShipment() method to set a reference to the Shipment object, a setConfig() method to set a copy of the config array, a getRate() method to retrieve a shipping rate, and a createLabel() method to create a shipping label.
<?php
interface ShipperInterface
{
    public function setShipment(Shipment $Shipment);
    public function setConfig(array $config);
    public function getRate();
    public function createLabel();
}

Fetching Shipping Rates

In order to calculate the shipping rates for our package, we’ll call the getRate() method of our Ups object. Since it will be performing network calls, we’ll need to make sure to wrap it in a try/catch block in case something goes wrong. Assuming that there are no errors with our data, the Ups object organizes our information into a format that the UPS API recognizes, sends it off, and processes the response into a RateResponse object that will be uniform for all the shippers we incorporate.
<?php
// calculate rates for shipment - returns an instance of 
// RatesResponse
try {
    $rates = $ups->getRate();
}
catch(Exception $e) {
    exit('Error: ' . $e->getMessage());
}
We can loop through the services array then to display the available shipping options:
<!-- output rates response -->
<dl>
 <dt><strong>Status</strong></dt>
 <dd><?php echo $rates->status; ?></dd>
 <dt><strong>Rate Options</strong></dt>
 <dd>
  <ul>
<?php
foreach ($rates->services as $service) {
    // display the service, cost, and a link to create the
    // label
    echo '<li>' . $service['service_description'] . ': ' .
        '$' . $service['total_cost'] .
        ' - <a href="?action=label&service_code=' .
        $service['service_code'] . '">Create Label</a></li>';
?>
   <li>Service Message:
    <ul>
<?php
    // display any service specific messages
    foreach($service['messages'] as $message) {
        echo '<li>' . $message . '</li>';
    }
?>
    </ul>
   </li>
<?php
    // display a breakdown of multiple packages if there are
    // more than one
    if ($service['package_count'] > 1) {
?>
   <li>Multiple Package Breakdown:
    <ol>
<?php
        foreach ($service['packages'] as $package) {
            echo '<li>$' . $package['total_cost'] . '</li>';
        }
?>        
    </ol>
   </li>
<?php
    }
}
?>
  </ul>
 </dd>
</dl>

Behind the Curtain: The RateResponse Object

The RateResponse object is essentially a simple object that contains our rate data in a standardized format, so that no matter which shipper plugin we use, the object (and therefore how we interface with it) will always be the same. That is the true beauty of abstraction! If you open Awsp/Ship/RateResponse.php you will see that the object simply holds a property called $status which will always be ‘Success’ or ‘Error’ and an array called $services. There will be an element in this array for each shipping option returned by the shipper plugin, and each element will contain ‘messages’, ‘service_code’, ‘service_description’, ‘total_cost’, ‘currency’, ‘package_count’, and an array called ‘packages’ that holds the following data for each package: ‘base_cost’, ‘option_cost’, ‘total_cost’, ‘weight’, ‘billed_weight’ and ‘weight_unit’. With the data contained in and easily extracted from the RateResponse object, you should have everything you need to supply your customer with the shipping rate options.

Creating a Shipping Label

Because of the abstracted API, your customer was impressed by the customized shipping options you were able to provide, and they made a purchase. You have processed their order and are ready to create the shipping label. Ideally, all we need to do is call createLabel()
on the Shipper object, and pass it the desired shipping option.
<?php
// set label parameters
$params['service_code'] = '03'; // ground shipping

// send request for a shipping label 
try {
    // return the LabelResponse object
    $label = $ups->createLabel($params);
}
catch (Exception $e){
    exit('Error: ' . $e->getMessage());
}
Unless there was a problem with the data, a LabelResponse object will be returned containing the status of the request, the total cost of the shipment, and an array containing a tracking number and base-64 encoded image of the label, and the type of image (GIF in the case of UPS) for each shipping label.

Behind the Curtain: The LabelResponse Object

Similar to the RateResponse object, the LabelResponse object is a simple object that contains our label data in a standardized format, so that no matter which shipper plugin we use, the object (and therefore how we interface with it) will always be the same. Abstraction is awesome! If you open Awsp/Ship/LabelResponse.php you will see that the object simply holds properties called $status which will always be ‘Success’ or ‘Error’, $shipment_cost which is the total cost of the shipment, and an array called $labels. There will be an element in this array for each label, each of which is an array containing ‘tracking_number’ and ‘label_image’ which is the base-64 encoded label image, and ‘label_file_type’ indicating the type of image it is (our UPS labels are GIF images). With the data contained in and easily extracted from the LabelResponse object, you will have everything you need to extract, print and save your tracking number(s) and label(s).

Behind the Curtain: The UPS Shipper Plugin

The job of the shipper plugin, Awsp/Ship/Ups.php in our case, is to take our standardized input in the Package and Shipment objects and convert it into a form that is understood by the shipper API. UPS offers their API in two flavors, SOAP and XML-RPC and updates them as needed in July and December of each year. This plugin uses the December 2012 version of the SOAP API and you will need to make sure that the SoapClient class is enabled in your PHP installation. After accepting and processing the Shipment object, which contains the Package object(s), and the $config array (from includes/config.php), the constructor sets some object properties and some values common to all API requests. The other public functions getRate() and createLabel() handle the work of assembling all of that data into a complex array that UPS will understand. Each of these methods then calls on sendRequest() to send the SOAP request to the UPS API and retrieve the response. An assortment of protected functions then do the dirty work of translating the SOAP response into our standardized RateResponse or LabelResponse objects depending on what was requested.

Conclusion

That was a lot to read, but you made it! With a simple set of calls, you can request rates and create labels through the UPS API, or any shipper API with a suitable plugin. All of the mystery and complexities of the APIs get abstracted away allowing us to keep it decoupled from the rest of the codebase and save a lot of future maintenance headaches. When the shipper updates their API, the only file you will need to change should be the shipper plugin. UPS has done a very good job of documenting their API’s, and there are MANY features and options that I have not included in this simple example for the sake of brevity. If you need to extend the shipper plugin, the UPS documentation should always be your first stop. Would you like to see a plugin for a different shipper? Please let me know in the comments below. If there is enough demand for it, we may do a follow up to this article. A little free advice, however: If you would like to integrate shipping through the U.S. Post Office, save yourself a BIG headache, and don’t waste your time using their official API. Visit stamps.com or another USPS approved vendor instead. Please feel free to download a copy or fork this abstraction library on my GitHub page at github.com/alexfraundorf-com/ship and submit issue reports for any bugs that you find. I’ll do my best to fix them and handle updates as quickly as possible. Thanks for reading, and happy PHPing! Image via Fotolia

Frequently Asked Questions (FAQs) on Abstracting Shipping APIs

What is the purpose of abstracting shipping APIs?

Abstracting shipping APIs is a process that simplifies the integration of various shipping services into a single application. This process allows developers to interact with multiple shipping APIs through a unified interface, reducing the complexity of dealing with different API structures. It also saves time and resources as developers don’t need to understand the intricacies of each shipping API. Instead, they can focus on the core functionality of their application.

How does abstracting shipping APIs benefit businesses?

Businesses that rely on multiple shipping services can greatly benefit from abstracting shipping APIs. It allows them to integrate various shipping options into their systems seamlessly, providing their customers with a variety of choices. This can lead to improved customer satisfaction and potentially increased sales. Additionally, it can streamline operations by automating shipping processes, reducing manual work, and minimizing errors.

What are the challenges in abstracting shipping APIs?

Abstracting shipping APIs can be challenging due to the differences in the structure, functionality, and documentation of each shipping API. Developers need to understand these differences and create an abstraction layer that can handle them. This requires a deep understanding of each API and the ability to design a flexible and robust abstraction layer. Additionally, maintaining the abstraction layer can be challenging as shipping APIs may change over time.

How can I handle errors when abstracting shipping APIs?

Handling errors is a crucial aspect of abstracting shipping APIs. Developers should implement robust error handling mechanisms to ensure that the application can gracefully handle API errors. This can involve validating API responses, catching exceptions, and providing meaningful error messages to the users. Additionally, developers should monitor the application to identify and fix errors quickly.

Can I use third-party libraries to abstract shipping APIs?

Yes, there are third-party libraries available that can simplify the process of abstracting shipping APIs. These libraries provide pre-built abstraction layers that can handle the complexities of interacting with multiple shipping APIs. However, developers should carefully evaluate these libraries to ensure that they meet their specific requirements and are actively maintained.

How can I test the abstraction layer?

Testing is a critical part of developing an abstraction layer for shipping APIs. Developers should write unit tests to verify the functionality of the abstraction layer. They should also perform integration tests to ensure that the abstraction layer works correctly with the actual shipping APIs. Additionally, developers can use mock APIs to simulate the behavior of the shipping APIs during testing.

How can I handle rate limiting when abstracting shipping APIs?

Rate limiting is a common feature of APIs, including shipping APIs. Developers should implement mechanisms to handle rate limiting, such as retrying requests after a certain period or slowing down the rate of requests. They should also monitor the rate limit status to avoid exceeding the limit and causing disruptions to the application.

How can I secure the abstraction layer?

Securing the abstraction layer is essential to protect sensitive data, such as shipping details and API keys. Developers should implement security measures such as encrypting sensitive data, using secure communication protocols, and regularly updating the abstraction layer to fix security vulnerabilities.

How can I handle versioning when abstracting shipping APIs?

Shipping APIs may have different versions with varying functionalities. Developers should design the abstraction layer to handle different API versions. This can involve using version-specific endpoints or parameters, and providing a way for the application to specify the API version.

How can I keep the abstraction layer up-to-date with changes in the shipping APIs?

Keeping the abstraction layer up-to-date requires regular monitoring of the shipping APIs for any changes. Developers should subscribe to the API updates from the shipping services and update the abstraction layer accordingly. They should also have a process in place to test and deploy the updates quickly to minimize disruptions to the application.

Alex FraundorfAlex Fraundorf
View Author

Alex's interest in programming began at about age 9 when he started entering hundreds of lines of BASIC into his family's Radio Shack Color Computer 2 only to see it crash because it used up the available 16K of memory!  Fast-forward to 2003 when he stumbled upon Kevin Yank's Build Your Own Database Driven Website using PHP & MySQL. Ever since then, Alex has been hooked on PHP and SitePoint's books. When he isn't busy coding, he can be found spending time with his wife, Angie, and their son, Daniel.

Advanced
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week