Abstracting Shipping APIs

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

Free book: Jump Start HTML5 Basics

Grab a free copy of one our latest ebooks! Packed with hints and tips on HTML5's most powerful new features.

  • Thanks

    Thanks Alex. This is very helpful. I’d would like to see a USPS tutorial that could piggy-back on this one.

    • http://alexfraundorf.com Alex Fraundorf

      I’m very glad that you enjoyed the article and found it helpful. Thank you very much for your feedback. If there are enough requests for it we may do a follow up with a plugin for USPS services. Happy PHPing!

  • Great detail, Alex.

    Thank you for including so much detail about UPS. I, too, would like to see how USPS would work differently, since it’s been a few years since I modified my code to match their most recent standards.

    • http://alexfraundorf.com Alex Fraundorf

      I am very glad that you liked the article and found it useful. I was learning the UPS API as I wrote the classes and article, so it was easy to make notes along the way of potential gotchas. I’m glad you found that helpful. We’ll see if there are enough requests to warrant a follow up article for USPS, but if not I would use a third party vendor like stamps.com.
      The official USPS API is passable for getting rates, but they do not offer a way to pay for labels. You can create “live” labels through their API, but you have to use a postage meter, stamps, or buy postage for them at the post office!
      Thanks again for your feedback and happy PHPing!