PHP Shipping Calculator Class...my first class as it were

Posted 7 Months Ago



So I decided it was time I dive into that mysterious world of Objected Oriented Programming.  Of course, I've worked with classes and PHP 5 and all that good stuff before.  I've used many-a-class for things like Authorize.net payment processing, Facebook applications, YouTube integration, and OpenInviter.

I had not, however, written my own class from scratch... that is until this evening.

Why a Shipping Calculator?

I must admit that I have been (and still am, come to that) a little hesitant to embrace classes.  I have not been offered a good reason for their use as opposed to well written functions.  It seems to me that you're just doing more work to create and execute more restricted functions.

However, looking through my standard website framework I decided that the three seperate shipping calculator functions (UPS, USPS, and FedEx) would probably qualify as a good candidate for one unified class so I whipped up this little class in about an hour and a half (yes, I am bragging).

Here is the result..

Features

  • Calculates shipping rates for UPS, USPS, and FedEx
  • Allows you to set package size in either inches, centimeters, or feet
  • Allows you to set package weight in either pounds, ounces, grams, or kilograms
  • Automatically converts weight and sizes
  • Handles Batch or Single Rate Calculation
  • Two ways to pass configuration
  • Built in debugging
  • Small (288 lines)

So, enough about how awesome it is, let's take a look at the actual class:


Shipping Calculator Class

<?php
class ShippingCalculator {
    // Defaults
    var $weight = 1;
    var $weight_unit = "lb";
    var $size_length = 4;
    var $size_width = 8;
    var $size_height = 2;
    var $size_unit = "in";
    var $debug = false; // Turn on to see XML sent and recieved
   
    // Config (you can either set these here or send them in a config array when creating an instance of the class)
    var $services;
    var $from_zip;
    var $from_state;
    var $from_country;
    var $to_zip;
    var $to_stat;
    var $to_country;
    var $ups_access;
    var $ups_user;
    var $ups_pass;
    var $ups_account;
    var $usps_user;
    var $fedex_account;
    var $fedex_meter;
   
    // Results
    var $rates;
   
    // Setup Class with Config Options
    function shipping_calculator($config) {
        if($config) {
            foreach($config as $k => $v) {
                $this->$k = $v;
                print $this->$k."<br />";
            }
        }
    }
   
    // Calculate
    function calculate($company = NULL,$code = NULL) {
        $this->rates = NULL;
        $services = $this->services;
        if($company and $code) $services[$company][$code] = 1;
        foreach($services as $company => $codes) {
            foreach($codes as $code => $name) {
                switch($company) {
                    case "ups":
                        $this->rates[$company][$code] = $this->calculate_ups($code);
                        break;
                    case "usps":
                        $this->rates[$company][$code] = $this->calculate_usps($code);
                        break;
                    case "fedex":
                        $this->rates[$company][$code] = $this->calculate_fedex($code);
                        break;
                }
            }
        }
       
        return $this->rates;
    }
   
    // Calculate UPS
    function calculate_ups($code) {
        $url = "https://www.ups.com/ups.app/xml/Rate";
        $data = '<?xml version="1.0"?> 
<AccessRequest xml:lang="en-US"> 
    <AccessLicenseNumber>'.$this->ups_access.'</AccessLicenseNumber> 
    <UserId>'.$this->ups_user.'</UserId> 
    <Password>'.$this->ups_pass.'</Password> 
</AccessRequest> 
<?xml version="1.0"?> 
<RatingServiceSelectionRequest xml:lang="en-US"> 
    <Request> 
        <TransactionReference> 
            <CustomerContext>Bare Bones Rate Request</CustomerContext> 
            <XpciVersion>1.0001</XpciVersion> 
        </TransactionReference> 
        <RequestAction>Rate</RequestAction> 
        <RequestOption>Rate</RequestOption> 
    </Request> 
    <PickupType> 
        <Code>01</Code> 
    </PickupType> 
    <Shipment> 
        <Shipper> 
            <Address> 
                <PostalCode>'.$this->from_zip.'</PostalCode> 
                <CountryCode>'.$this->from_country.'</CountryCode> 
            </Address> 
        <ShipperNumber>'.$this->ups_account.'</ShipperNumber> 
        </Shipper> 
        <ShipTo> 
            <Address> 
                <PostalCode>'.$this->to_zip.'</PostalCode> 
                <CountryCode>'.$this->to_country.'</CountryCode> 
            <ResidentialAddressIndicator/> 
            </Address> 
        </ShipTo> 
        <ShipFrom> 
            <Address> 
                <PostalCode>'.$this->from_zip.'</PostalCode> 
                <CountryCode>'.$this->from_country.'</CountryCode> 
            </Address> 
        </ShipFrom> 
        <Service> 
            <Code>'.$code.'</Code> 
        </Service> 
        <Package> 
            <PackagingType> 
                <Code>02</Code> 
            </PackagingType> 
            <Dimensions> 
                <UnitOfMeasurement> 
                    <Code>IN</Code> 
                </UnitOfMeasurement> 
                <Length>'.($this->size_unit != "in" ? $this->convert_sze($this->size_length,$this->size_unit,"in") : $this->size_length).'</Length> 
                <Width>'.($this->size_unit != "in" ? $this->convert_sze($this->size_width,$this->size_unit,"in") : $this->size_width).'</Width> 
                <Height>'.($this->size_unit != "in" ? $this->convert_sze($this->size_height,$this->size_unit,"in") : $this->size_height).'</Height> 
            </Dimensions> 
            <PackageWeight> 
                <UnitOfMeasurement> 
                    <Code>LBS</Code> 
                </UnitOfMeasurement> 
                <Weight>'.($this->weight_unit != "lb" ? $this->convert_weight($this->weight,$this->weight_unit,"lb") : $this->weight).'</Weight> 
            </PackageWeight> 
        </Package> 
    </Shipment> 
</RatingServiceSelectionRequest>';
       
        // Curl
        $results = $this->curl($url,$data);
       
        // Debug
        if($this->debug == true) {
            print "<xmp>".$data."</xmp><br />";
            print "<xmp>".$results."</xmp><br />";
        }
       
        // Match Rate
        preg_match('/<MonetaryValue>(.*?)</MonetaryValue>/',$results,$rate);
       
        return $rate[1];
    }
   
    // Calculate USPS
    function calculate_usps($code) {
        // Weight (in lbs)
        if($this->weight_unit != 'lb') $weight = $this->convert_weight($weight,$this->weight_unit,'lb');
        else $weight = $this->weight;
        // Split into Lbs and Ozs
        $lbs = floor($weight);
        $ozs = ($weight - $lbs)  * 16;
        if($lbs == 0 and $ozs < 1) $ozs = 1;
       
        $url = "http://Production.ShippingAPIs.com/ShippingAPI.dll";
        $data = 'API=RateV2&XML=<RateV2Request USERID="'.$this->usps_user.'"><Package ID="0"><Service>'.$code.'</Service><ZipOrigination>'.$this->from_zip.'</ZipOrigination><ZipDestination>'.$this->to_zip.'</ZipDestination><Pounds>'.$lbs.'</Pounds><Ounces>'.$ozs.'</Ounces><Size>REGULAR</Size><Machinable>TRUE</Machinable></Package></RateV2Request>';
       
        // Curl
        $results = $this->curl($url,$data);
       
        // Debug
        if($this->debug == true) {
            print "<xmp>".$data."</xmp><br />";
            print "<xmp>".$results."</xmp><br />";
        }
       
        // Match Rate
        preg_match('/<Rate>(.+?)</Rate>/',$results,$rate);
       
        return $rate[1];
    }
   
    // Calculate FedEX
    function calculate_fedex($code) {
        $url = "https://gatewaybeta.fedex.com/GatewayDC";
        $data = '<?xml version="1.0" encoding="UTF-8" ?>
<FDXRateRequest xmlns:api="http://www.fedex.com/fsmapi" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FDXRateRequest.xsd">
    <RequestHeader>
        <CustomerTransactionIdentifier>Express Rate</CustomerTransactionIdentifier>
        <AccountNumber>'.$this->fedex_account.'</AccountNumber>
        <MeterNumber>'.$this->fedex_meter.'</MeterNumber>
        <CarrierCode>'.(in_array($code,array('FEDEXGROUND','GROUNDHOMEDELIVERY')) ? 'FDXG' : 'FDXE').'</CarrierCode>
    </RequestHeader>
    <DropoffType>REGULARPICKUP</DropoffType>
    <Service>'.$code.'</Service>
    <Packaging>YOURPACKAGING</Packaging>
    <WeightUnits>LBS</WeightUnits>
    <Weight>'.number_format(($this->weight_unit != 'lb' ? convert_weight($this->weight,$this->weight_unit,'lb') : $this->weight), 1, '.', '').'</Weight>
    <OriginAddress>
        <StateOrProvinceCode>'.$this->from_state.'</StateOrProvinceCode>
        <PostalCode>'.$this->from_zip.'</PostalCode>
        <CountryCode>'.$this->from_country.'</CountryCode>
    </OriginAddress>
    <DestinationAddress>
        <StateOrProvinceCode>'.$this->to_state.'</StateOrProvinceCode>
        <PostalCode>'.$this->to_zip.'</PostalCode>
        <CountryCode>'.$this->to_country.'</CountryCode>
    </DestinationAddress>
    <Payment>
        <PayorType>SENDER</PayorType>
    </Payment>
    <PackageCount>1</PackageCount>
</FDXRateRequest>';
       
        // Curl
        $results = $this->curl($url,$data);
       
        // Debug
        if($this->debug == true) {
            print "<xmp>".$data."</xmp><br />";
            print "<xmp>".$results."</xmp><br />";
        }
   
        // Match Rate
        preg_match('/<NetCharge>(.*?)</NetCharge>/',$results,$rate);
       
        return $rate[1];
    }
   
    // Curl
    function curl($url,$data = NULL) {
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_HEADER, 1);
        curl_setopt($ch, CURLOPT_TIMEOUT, 60); 
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); 
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0); 
        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
        if($data) {
            curl_setopt($ch, CURLOPT_POST,1); 
            curl_setopt($ch, CURLOPT_POSTFIELDS,$data);
        } 
        curl_setopt($ch, CURLOPT_USERAGENT, $_SERVER['HTTP_USER_AGENT']);
        $contents = curl_exec ($ch);
       
        return $contents;
       
        curl_close ($ch);
    }
   
    // Convert Weight
    function convert_weight($weight,$old_unit,$new_unit) {
        $units['oz'] = 1;
        $units['lb'] = 0.0625;
        $units['gram'] = 28.3495231;
        $units['kg'] = 0.0283495231;
       
        // Convert to Ounces (if not already)
        if($old_unit != "oz") $weight = $weight / $units[$old_unit];
       
        // Convert to New Unit
        $weight = $weight * $units[$new_unit];
       
        // Minimum Weight
        if($weight < .1) $weight = .1;
       
        // Return New Weight
        return round($weight,2);
    }
   
    // Convert Size
    function convert_size($size,$old_unit,$new_unit) {
        $units['in'] = 1;
        $units['cm'] = 2.54;
        $units['feet'] = 0.083333;
       
        // Convert to Inches (if not already)
        if($old_unit != "in") $size = $size / $units[$old_unit];
       
        // Convert to New Unit
        $size = $size * $units[$new_unit];
       
        // Minimum Size
        if($size < .1) $size = .1;
       
        // Return New Size
        return round($size,2);
    }
   
    // Set Value
    function set_value($k,$v) {
        $this->$k = $v;
    }
}
?>

..here's an array ($services) of the various Shipping Companies and Methods which we'll use in the configuration in a moment:

Services Array

// UPS
$services['ups']['14'] = 'Next Day Air Early AM';
$services['ups']['01'] = 'Next Day Air';
$services['ups']['65'] = 'Saver';
$services['ups']['59'] = '2nd Day Air Early AM';
$services['ups']['02'] = '2nd Day Air';
$services['ups']['12'] = '3 Day Select';
$services['ups']['03'] = 'Ground';
$services['ups']['11'] = 'Standard';
$services['ups']['07'] = 'Worldwide Express';
$services['ups']['54'] = 'Worldwide Express Plus';
$services['ups']['08'] = 'Worldwide Expedited';
// USPS
$services['usps']['EXPRESS'] = 'Express';
$services['usps']['PRIORITY'] = 'Priority';
$services['usps']['PARCEL'] = 'Parcel';
$services['usps']['FIRST CLASS'] = 'First Class';
$services['usps']['EXPRESS SH'] = 'Express SH';
$services['usps']['BPM'] = 'BPM';
$services['usps']['MEDIA '] = 'Media';
$services['usps']['LIBRARY'] = 'Library';
// FedEx
$services['fedex']['PRIORITYOVERNIGHT'] = 'Priority Overnight';
$services['fedex']['STANDARDOVERNIGHT'] = 'Standard Overnight';
$services['fedex']['FIRSTOVERNIGHT'] = 'First Overnight';
$services['fedex']['FEDEX2DAY'] = '2 Day';
$services['fedex']['FEDEXEXPRESSSAVER'] = 'Express Saver';
$services['fedex']['FEDEXGROUND'] = 'Ground';
$services['fedex']['FEDEX1DAYFREIGHT'] = 'Overnight Day Freight';
$services['fedex']['FEDEX2DAYFREIGHT'] = '2 Day Freight';
$services['fedex']['FEDEX3DAYFREIGHT'] = '3 Day Freight';
$services['fedex']['GROUNDHOMEDELIVERY'] = 'Home Delivery';
$services['fedex']['INTERNATIONALECONOMY'] = 'International Economy';
$services['fedex']['INTERNATIONALFIRST'] = 'International First';
$services['fedex']['INTERNATIONALPRIORITY'] = 'International Priority';


..and here's an example of the configuration and implemenation:

Configuration and Implementation

// Config
$config = array(
    // Services
    'services' => $services,
    // Weight
    'weight' => 2, // Default = 1
    'weight_units' => 'lb', // lb (default), oz, gram, kg
    // Size
    'size_length' => 5, // Default = 8
    'size_width' => 6, // Default = 4
    'size_height' => 3, // Default = 2
    'size_units' => 'in', // in (default), feet, cm
    // From
    'from_zip' => 97210,
    'from_state' => "OR", // Only Required for FedEx
    'from_country' => "US",
    // To
    'to_zip' => 55455,
    'to_state' => "MN", // Only Required for FedEx
    'to_country' => "US",
   
    // Service Logins
    'ups_access' => '', // UPS Access License Key
    'ups_user' => '', // UPS Username 
    'ups_pass' => '', // UPS Password 
    'ups_account' => '', // UPS Account Number
    'usps_user' => '', // USPS User Name
    'fedex_account' => '', // FedEX Account Number
    'fedex_meter' => '' // FedEx Meter Number
);

// Create Class (with config array)
$ship = new ShippingCalculator ($config);
// Get Rates
$rates = $ship->calculate();

Notice: You'll have to get the various usernames, passwords, and accounts from UPS, USPS, and FedEx to run this class.  I'll try and add some information about how to do that soon.

Results

This class spits ou information via the calculate(); method.  In our example above $rates is an array of the rates we calculated.  I ran a quick example using a few shipping methods from each company; here's what the results look like:

Rates Array

Array (
    [ups] => Array (
        [12] => 15.07
        [03] => 8.35
        [11] =>
    )
    [usps] => Array (
        [PRIORITY] => 8.10
        [PARCEL] => 7.62
        [FIRST CLASS] =>
    )
    [fedex] => Array (
        [FEDEX2DAY] => 18.55
        [FEDEXEXPRESSSAVER] => 13.53
        [FEDEXGROUND] => 6.34
    )
)

You'll notice some are empty. This means that, for one reason or another, the shipping company doesn't ship your query via that method.  Perhaps it was a worldwide shipping method and your shippment is local, etc.

One more note before I leave you.  If you wanted to match these rates with their more user friendly names (as seen in the original $services array) and wanted to simultaneously print some radio buttons where a user could select their shipping method you could do something simple like this:

Printing Radio Buttons

foreach($rates as $company => $codes) {
    foreach($codes as $code => $rate) print "
<input type='checkbox' name='shipping' value='".$rate."' /> ".$services[$company][$code]."<br />";
}

And of course, here's the source code:

Download Source Code
 

Enjoy! and let me know if you have any input, find any bugs, etc.



Tags: php, class, shipping, conversion, ups, usps, fedex, rates





Comments

Name
E-mail
Website

Optional



Nate

Posted 4 Months Ago


For the record, it seems that this code is very, very slow ... looking up a UPS rate (only a UPS rate) takes an average of over 5 seconds for me with your code, whereas looking up the same rate with ups php (http://code.google.com/p/ups php/) takes an average of under half a second. Not sure what the problem is, but I'll post back here if I take the time to figure it out.

Note: for whatever reason, hyphens are stripped out of these comments. If anyone wishes to use the link above, replace the space between "ups" and "php" with a hyphen.

Nate

Posted 5 Months Ago


Yeah, that error is because he named the constructor incorrectly as "shipping_calculator" rather than "ShippingCalculator" – edit line 32 in shipping_calculator.php and that error will not occur.

Posted 7 Months Ago


The ideal object oriented structure would probably have an abstract ShippingCalculator class which declares the methods and general variables (if any), and then have concrete implementations like UPSCalculator, USPSCalculator, and FedExCalculator.

aamir

Posted 7 Months Ago


Hi,
     nice code, but when i run this i get this error
Warning: Invalid argument supplied for foreach() in C:AppServwwwshipshipping_calculator.php on line 46