A shipping module
A carrier in PrestaShop is a record in the back office under "Shipping → Carriers" with a name, a delivery time and a price table. It is usually created by hand, and no module is needed. A shipping module is written when the cost has to be calculated by code: to request a rate from a delivery service or to apply your own formula. Such a module extends CarrierModule.
The example is a separate acmeship module in the modules/acmeship folder.
The main class
<?php
// modules/acmeship/acmeship.php
if (!defined('_PS_VERSION_')) {
exit;
}
class AcmeShip extends CarrierModule
{
public $id_carrier;
public function __construct()
{
$this->name = 'acmeship';
$this->tab = 'shipping_logistics';
$this->version = '1.0.0';
$this->author = 'Acme';
parent::__construct();
$this->displayName = $this->trans('Acme courier', [], 'Modules.Acmeship.Admin');
}
public function getOrderShippingCost($cart, $shippingCost)
{
return 5.0;
}
public function getOrderShippingCostExternal($cart)
{
return $this->getOrderShippingCost($cart, 0);
}
}Two methods are mandatory. The engine calls them when it calculates shipping for a cart and takes the returned number as the cost without tax. Here shipping always costs 5 units of the shop currency. Before the call the engine writes the carrier id into the id_carrier property.
| Method | When it is called |
|---|---|
getOrderShippingCost($cart, $shippingCost) | The carrier has price ranges; the second argument is the price from the table |
getOrderShippingCostExternal($cart) | Ranges are not used |
Creating the carrier
The module alone does not show up in the shipping list: on install it must create a carrier tied to the module.
// modules/acmeship/acmeship.php
public function install(): bool
{
if (!parent::install() || !$this->registerHook('actionCarrierUpdate')) {
return false;
}
$carrier = new Carrier();
$carrier->name = 'Acme Courier';
$carrier->active = true;
$carrier->is_module = true;
$carrier->external_module_name = $this->name;
$carrier->shipping_external = true;
$carrier->need_range = true;
foreach (Language::getLanguages(false) as $language) {
$carrier->delay[(int) $language['id_lang']] = '2 days';
}
if (!$carrier->add()) {
return false;
}
return Configuration::updateValue('ACMESHIP_CARRIER_ID', (int) $carrier->id);
}Carrier is a core model. The is_module, external_module_name and shipping_external properties tie the carrier to the module: the cost will be asked from it. delay is the delivery time text in every language. The module remembers the id of the created carrier in the settings.
What else a carrier needs
For the option to be shown to a customer, the carrier is given zones, customer groups and ranges. The easiest way is to do it in the back office after installation: "Shipping → Carriers", the edit button of "Acme Courier".
| What | What for |
|---|---|
| Zones | Which regions it delivers to |
| Groups | Which customer groups may use it |
| Weight or price ranges | Mandatory with need_range, even if the code calculates the price |
Modules made for distribution do this in install() with code — the core methods and models addZone(), setGroups(), RangeWeight, RangePrice.
How to see the result
php bin/console prestashop:module install acmeshipSet up zones and ranges, put a product in the cart and go to the shipping step: "Acme Courier" is in the list with the price 5 and the time "2 days".
Calculating from the cart
// modules/acmeship/acmeship.php
public function getOrderShippingCost($cart, $shippingCost)
{
$weight = $cart->getTotalWeight();
if ($weight > 30) {
return false;
}
return 5.0 + $weight * 0.5;
}The cost now depends on the cart weight. The value false means the carrier is not available for this cart — the option is not shown.
A changed carrier id
When the shop owner edits a carrier, PrestaShop does not change the record but creates a copy with a new id — so that old orders keep pointing to the previous terms. The module must update the saved id:
// modules/acmeship/acmeship.php
public function hookActionCarrierUpdate(array $params): void
{
if ((int) $params['id_carrier'] === (int) Configuration::get('ACMESHIP_CARRIER_ID')) {
Configuration::updateValue('ACMESHIP_CARRIER_ID', (int) $params['carrier']->id);
}
}id_carrier holds the old id, carrier holds the new object.
Common mistake
getOrderShippingCost() calls an external delivery service without a cache and without a time limit. The method is called on every page with a cart, and a slow response from the service slows down the whole shop. Save the received rate for the cart-and-address pair and set a short request timeout.