Creating a Shipping Type

By Andrew Embler, concrete5 CTO.

Before you Begin

You'll need to create a directory named "core_commerce" in the models/ directory, at the root of your concrete5 install. Within this directory, create a "shipping" directory, and within this, create a "types" directory. From the root of your concrete5 install, the path to this directory should be

models/core_commerce/shipping/types/

This is your website's local shipping types directory, and will be referred to as such from within this tutorial.

Creating the Shipping Type Directory

Before you can write code for your shipping type, you will need to:

  1. Choose a handle for it. This is a short name that will match the directory that your shipping type resides in. This should be descriptive, but be completely in lowercase and contain no spaces. For example, if you wanted to create a shipping type named "UPS Custom" you'd probably want to give it the handle "ups_custom" (and name the directory the same)

  2. Create a directory with the same name as your payment method's handle in the local payment methods directory (referenced above.)

The Components of a Shipping Type

There are several items that comprise a shipping type. Some are required. These are all files that exist within the directory you created for your specific payment method.

  1. The controller file, named "controller.php" This file is required. It should contain one PHP class. Like payment methods in the eCommerce addon or blocks in the concrete5 core, the controller class must be named appropriately. It must begin with "CoreCommerce", then contain the camel-cased name of the shipping type, and finish with "ShippingTypeController." If your payment method is named "ups_custom", your class would be CoreCommerceUpsCustomShippingTypeController. The class must extend the CoreCommerceShippingController class.

  2. The additional details form for this shipping type. If it exists, it is named "type_form.php" This is displayed to site administrators when they wish to enable or disable the shipping type method, if the type method requires additional information. This form is optional. (Typically, shipping types will require certain additional information like API logins, origin postal codes, etc... This information can be stored globally in the additional details form.)

Example: The Flat Shipping Type

concrete5 eCommerce ships with a simple shipping type named the "Flat Shipipng Type." This shipping type is meant to be useful for websites which charge a simple fee for all orders, and certain additional fees based on the number of products in an order. (Additionally, products can have a per-product shipping fee attached to them. The flat shipping type supports this.)

type_form.php

When configuring the flat shipping type through the dashboard, you'll see the contents of the additional details form (type_form.php), which are simply some concrete5 configuration form elements with names like "SHIPPING_TYPE_FLAT_BASE" and "SHIPPING_TYPE_FLAT_PER_ITEM". You won't see any form tags or submit buttons in the additional details form, however, because this is handled autmomatically by concrete5, by a method in the controller file.

controller.php

The controller file for the flat shipping type contains the following components.

$shippingMethods array

Every shipping method ought to expose the name of its shipping methods through this array. This is how concrete5's eCommerce addon will know what shipping methods this shipping type makes available, and what they are named. (Note: usage of this array is encouraged, because in the future these methods may be exposed in the dashboard through the additional details form above, for renaming and/or disabling.)

type_form()

This method is automatically run when additional details form is displayed in the dashboard. These lines of code simply retrieve the two bits of data we're saving with this method form.

$pkg = Package::getByHandle('core_commerce');
$SHIPPING_TYPE_FLAT_BASE = $pkg->config('SHIPPING_TYPE_FLAT_BASE');
$SHIPPING_TYPE_FLAT_PER_ITEM = $pkg->config('SHIPPING_TYPE_FLAT_PER_ITEM');
$this->set('SHIPPING_TYPE_FLAT_BASE', $SHIPPING_TYPE_FLAT_BASE);
$this->set('SHIPPING_TYPE_FLAT_PER_ITEM', $SHIPPING_TYPE_FLAT_PER_ITEM);

This ensures that previously saved data accurately populates the additional details form.

validate()

This is automatically run when the additional details form is submitted. If you need any specific errors to return when users are configuring your shipping type through the dashboard, use the concrete5 validation error handler here. If you return an error object that has errors (e.g. you have added at least one error using ValidationErrorHelper::add()) the save will fail.

save()

This method is run when the additional details form successfully passes the validate task. This method in the flat shipping type simply saves the two parameters it uses.

getAvailableShippingMethods($currentOrder)

This method is required in your controller. It is automatically run at several points during checkout, on the current order object. This method is responsible for returning either one shipping method, or an array of methods, along with their cost.

Let's go through this method so we can understand what it does.

$pkg = Package::getByHandle('core_commerce');
$shipping = $pkg->config('SHIPPING_TYPE_FLAT_BASE');
$perItem = $pkg->config('SHIPPING_TYPE_FLAT_PER_ITEM');

Here we are simply retrieving the current parameters that the administrators have saved for the flat shipping type. We save the global shipping base to the $shipping variable.

foreach($currentOrder->getProducts() as $pr) {
    if ($pr->productRequiresShipping()) {
        $thisPerItem = $perItem;
        if ($pr->getProductShippingModifier() != '') {
            $thisPerItem += $pr->getProductShippingModifier();
        }
        $shipping += ($thisPerItem * $pr->getQuantity());
    }
}

Here are are looping through the current order, grabbing each product. If the product itself has a specific per-product shipping modifier, we apply that to the global shipping base, and the per-item modifier. Finally, we make sure that this applies to all quantity of every product. Note: we must check that the product does, in fact, require shipping.

$ecm = new CoreCommerceShippingMethod($this->getShippingType(), 'FLAT');
$ecm->setPrice($shipping);
$ecm->setName(t('Basic Shipping'));
return $ecm;

Finally, we return a CoreCommerceShippingMethod from this function. Since we've only returned one, the eCommerce addon will not give the user a choice (unless other shipping types are enabled.) If wanted to, however, we could return multiple shipping methods (as an addon for UPS, or FedEX might) as an array.

Ensure that You Honor Per-Product Options

You'll notice that the example above shows us checking core eCommerce parameters like per-product shipping and the productRequiresShipping method. That's because developers have complete control over how they code their getAvailableShippingMethods method. In almost every case, however, these core parameters should be honored.

Install and Activate the Payment Method on your Site

Finally, when your shipping type is written and the various files present within the directory are completed (or at the very least ready to be tested) you'll need to install your shipping type to make the eCommerce addon aware of it. Head to Dashboard > eCommerce > Shipping. Within the "Custom Shipping Types" section of the page, you should see your new type listed. Click install. Then click the "Edit" button next to your type, and enable it to test it on your site.

Converting the shipping type into a package

If you'd like to offer the shipping type to someone else, or on the concrete5 marketplace, you'll need to package it up. You can read more about the package format in the concrete5 developer documentation. To package up your shipping type, simply create a package (complete with a controller script and an icon.png graphic), then create a models/ directory within the package. Within this directory create a "core_commerce" directory. Then create a "shipping" and then a "types" directory beneath this directory. Finally, move your shipping type directory (which contains the PHP files) into this directory.

Your shipping type package will then be structured as follows

core_commerce_your_shipping_type_handle/models/core_commerce/shipping/types/your_shipping_type

Finally, edit your package's controller.php file, and ensure that these lines are in the install() method:

$pkg = parent::install();
Loader::model('shipping/type', 'core_commerce');
CoreCommerceShippingType::add('your_shipping_type', t('Shipping Type Name'), 0, $pkg);

This will install the shipping type when the package is installed (but it will not enable it by default.)