Architecture

Carrier abstraction, rate collection, and class hierarchy

Carrier Abstraction Pattern

Magento's shipping architecture uses a carrier abstraction pattern that separates rate collection logic from specific carrier implementations. This allows new carriers to be added without modifying core shipping code.

                    +------------------+
                    | RateRequest      |
                    | (Quote\Address)  |
                    +--------+---------+
                             |
                             v
                    +------------------+
                    | Shipping         |
                    | (Rate Collector) |
                    +--------+---------+
                             |
            +----------------+----------------+
            |                |                |
            v                v                v
    +-------+------+  +------+-------+  +-----+--------+
    | CarrierFactory|  | CarrierFactory|  | CarrierFactory|
    +-------+------+  +------+-------+  +-----+--------+
            |                |                |
            v                v                v
    +-------+------+  +------+-------+  +-----+--------+
    | UPS Carrier  |  | USPS Carrier |  | FedEx Carrier|
    | (Online)     |  | (Online)     |  | (Online)     |
    +--------------+  +--------------+  +--------------+
            |                |                |
            v                v                v
    +-------+------+  +------+-------+  +-----+--------+
    | Rate\Result  |  | Rate\Result  |  | Rate\Result  |
    +--------------+  +--------------+  +--------------+
            |                |                |
            +----------------+----------------+
                             |
                             v
                    +------------------+
                    | CarrierResult    |
                    | (Aggregated)     |
                    +------------------+

Carrier Class Hierarchy

AbstractCarrierInterface (interface)
    |
    +-- AbstractCarrier (abstract class)
            |
            +-- AbstractCarrierOnline (abstract class)
                    |
                    +-- Ups\Model\Carrier (concrete)
                    +-- Usps\Model\Carrier (concrete)
                    +-- Fedex\Model\Carrier (concrete)
                    +-- Dhl\Model\Carrier (concrete)
            |
            +-- OfflineShipping\Model\Carrier\Flatrate (concrete)
            +-- OfflineShipping\Model\Carrier\Tablerate (concrete)
            +-- OfflineShipping\Model\Carrier\Freeshipping (concrete)

AbstractCarrier

Base class providing configuration access, handling fee calculation, country availability checks, and rate result building. Used by offline carriers (flat rate, table rate, free shipping).

AbstractCarrierOnline

Extends AbstractCarrier with API integration features: shipping labels, tracking, container types, content types, and return shipments. Used by UPS, USPS, FedEx, DHL.

Rate Collection Architecture

Shipping::collectRates()

The main orchestration method that iterates through all enabled carriers:

public function collectRates(RateRequest $request)
{
    // Set origin from store config if not provided
    if (!$request->getOrig()) {
        $request->setCountryId($this->getConfigValue('country_id'));
        $request->setRegionId($this->getConfigValue('region_id'));
        $request->setPostcode($this->getConfigValue('postcode'));
    }

    // Get all carriers or limited set
    $limitCarrier = $request->getLimitCarrier();
    if (!$limitCarrier) {
        $carriers = $this->scopeConfig->getValue('carriers');
        foreach ($carriers as $carrierCode => $config) {
            $this->collectCarrierRates($carrierCode, $request);
        }
    } else {
        foreach ((array)$limitCarrier as $carrierCode) {
            $this->collectCarrierRates($carrierCode, $request);
        }
    }

    return $this;
}

collectCarrierRates()

Prepares and invokes a single carrier:

public function collectCarrierRates($carrierCode, $request)
{
    // Create carrier via factory
    $carrier = $this->prepareCarrier($carrierCode, $request);

    // Check for package shipment mode
    if ($carrier->getConfigData('shipment_requesttype')) {
        $packages = $this->composePackagesForCarrier($carrier, $request);
        if (!empty($packages)) {
            $request->setPackages($packages);
        }
    }

    // Collect rates from carrier
    $result = $carrier->collectRates($request);

    // Append to aggregated result
    if ($result instanceof Result) {
        $this->getResult()->appendResult($result, $carrier->getConfigData('showmethod'));
    }

    return $this;
}

Rate Result Classes

Class Path Purpose
Result Model/Rate/Result.php Container for shipping methods from a single carrier
Method Model/Rate/Result/Method.php Individual shipping method with carrier, method code, price
Error Quote/Model/Quote/Address/RateResult/Error.php Error response when carrier unavailable or fails
CarrierResult Model/Rate/CarrierResult.php Aggregated result from all carriers for a request
PackageResult Model/Rate/PackageResult.php Result for multi-package shipments

Carrier Factory

The CarrierFactory creates carrier instances based on configuration. Each carrier is registered in its module's config.xml:

<!-- Example: module-ups/etc/config.xml -->
<config>
    <default>
        <carriers>
            <ups>
                <active>0</active>
                <model>Magento\Ups\Model\Carrier</model>
                <title>United Parcel Service</title>
                <!-- ... additional config ... -->
            </ups>
        </carriers>
    </default>
</config>
// CarrierFactory::create()
public function create($carrierCode, $storeId = null)
{
    $className = $this->scopeConfig->getValue(
        'carriers/' . $carrierCode . '/model',
        ScopeInterface::SCOPE_STORE,
        $storeId
    );

    $carrier = $this->objectManager->create($className);
    $carrier->setId($carrierCode);
    $carrier->setStore($storeId);

    return $carrier;
}

Tracking Architecture

+-------------------+     +-------------------+     +-------------------+
|  Tracking/Popup   | --> |  AbstractCarrier  | --> |  Carrier API      |
|  (Controller)     |     |  ::getTracking()  |     |  (UPS/FedEx/etc)  |
+-------------------+     +-------------------+     +-------------------+
                                   |
                                   v
                          +-------------------+
                          | Tracking\Result   |
                          +-------------------+
                                   |
                                   v
                          +-------------------+
                          | Result\Status     |
                          | Result\Error      |
                          +-------------------+

Tracking Result Classes

  • Tracking/Result.php
  • Tracking/Result/Status.php
  • Tracking/Result/Error.php
  • Tracking/Result/AbstractResult.php

Status Fields

  • tracking - Tracking number
  • carrier - Carrier code
  • carrier_title - Carrier display name
  • progressdetail - Array of tracking events
  • deliverydate - Expected delivery
  • signedby - Signature on delivery

Shipping Label Architecture

Shipping labels are generated via the Shipping\Labels class (admin area only). This class extends the base Shipping class and adds label creation.

// adminhtml/di.xml preference
<preference for="Magento\Shipping\Model\Shipping"
            type="Magento\Shipping\Model\Shipping\Labels" />

// Labels::requestToShipment()
public function requestToShipment(Shipment $orderShipment, Request $request)
{
    $carrier = $this->carrierFactory->create($orderShipment->getOrder()->getShippingMethod());

    // Build shipment request data
    $request->setOrderShipment($orderShipment);
    $request->setPackages($packages);

    // Get label from carrier
    $result = $carrier->requestToShipment($request);

    if ($result->hasErrors()) {
        throw new LocalizedException(__($result->getErrors()));
    }

    // Store label data on shipment
    $orderShipment->setShippingLabel($result->getShippingLabelContent());
    $orderShipment->setPackages($result->getPackages());

    return $result;
}

Note: Only carriers extending AbstractCarrierOnline support shipping labels. The isShippingLabelsAvailable() method must return true.

Package Composition

When a carrier has shipment_requesttype enabled, items are composed into packages using a first-fit decreasing (FFD) bin-packing algorithm:

// Shipping::composePackagesForCarrier()
public function composePackagesForCarrier($carrier, $request)
{
    $maxWeight = (double)$carrier->getConfigData('max_package_weight');
    $allItems = $request->getAllItems();

    // Collect weights and prices
    foreach ($allItems as $item) {
        // Skip bundles with dynamic shipping
        // Skip free shipping items
        // Handle decimal quantities
        $weightItems[] = ['weight' => $itemWeight, 'price' => $item->getBasePrice()];
    }

    // Sort by weight descending (FFD algorithm)
    usort($weightItems, fn($a, $b) => $b['weight'] <=> $a['weight']);

    // Bin-pack into packages
    return $this->_makePieces($weightItems, $maxWeight);
}

// First-fit decreasing bin packing
protected function _makePieces(array $items, float $maxWeight): array
{
    $packages = [];

    foreach ($items as $item) {
        $placed = false;
        foreach ($packages as &$package) {
            if ($item['weight'] <= $maxWeight - $package['weight']) {
                $package['weight'] += $item['weight'];
                $package['price'] += $item['price'];
                $placed = true;
                break;
            }
        }
        if (!$placed) {
            $packages[] = ['weight' => $item['weight'], 'price' => $item['price']];
        }
    }

    return $packages;
}

Directory Structure

module-shipping/
+-- Block/
|   +-- Adminhtml/
|   |   +-- Create/              # Shipment creation forms
|   |   +-- Order/
|   |   |   +-- Packaging/       # Package grid for labels
|   |   |   +-- Tracking/        # Tracking management
|   |   +-- View/                # Shipment view pages
|   +-- DataProviders/
|   |   +-- Tracking/            # Tracking block data providers
|   +-- Tracking/                # Frontend tracking blocks
|
+-- Controller/
|   +-- Adminhtml/
|   |   +-- Order/
|   |   |   +-- Shipment/        # Shipment CRUD controllers
|   |   +-- Shipment/            # Mass actions
|   +-- Tracking/                # Frontend tracking popup
|
+-- Helper/
|   +-- Data.php                 # Tracking URL helper
|
+-- Model/
|   +-- Carrier/
|   |   +-- AbstractCarrier.php
|   |   +-- AbstractCarrierOnline.php
|   |   +-- CarrierInterface.php
|   |   +-- Source/              # Config source models
|   +-- Config/                  # Carrier config readers
|   +-- Order/                   # Order tracking info
|   +-- Rate/                    # Rate result classes
|   +-- ResourceModel/           # Carrier title resources
|   +-- Shipment/                # Shipment request/item classes
|   +-- Shipping/                # Labels extension
|   +-- Tracking/                # Tracking result classes
|   +-- CarrierFactory.php
|   +-- Shipping.php             # Main rate collector
|
+-- etc/
|   +-- adminhtml/
|   |   +-- di.xml               # Admin preferences
|   |   +-- routes.xml
|   |   +-- system.xml           # Admin config
|   +-- frontend/
|   |   +-- routes.xml
|   +-- config.xml               # Default origin settings
|   +-- crontab.xml              # Shipment report aggregation
|   +-- di.xml                   # Main preferences
|   +-- module.xml
|
+-- view/
    +-- adminhtml/               # Admin templates/layouts
    +-- frontend/                # Tracking popup templates