Custom Carrier Model
Complete Carrier Implementation
namespace Vendor\Shipping\Model\Carrier;
use Magento\Shipping\Model\Carrier\AbstractCarrier;
use Magento\Shipping\Model\Carrier\CarrierInterface;
use Magento\Shipping\Model\Rate\Result;
use Magento\Quote\Model\Quote\Address\RateResult\Factory as RateResultFactory;
use Magento\Quote\Model\Quote\Address\RateResult\MethodFactory as RateMethodFactory;
class CustomCarrier extends AbstractCarrier implements CarrierInterface
{
protected $_code = 'customcarrier';
protected $_isFixed = false;
private RateResultFactory $rateResultFactory;
private RateMethodFactory $rateMethodFactory;
private Config $carrierConfig;
public function __construct(
\Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig,
RateResultFactory $rateResultFactory,
RateMethodFactory $rateMethodFactory,
Config $carrierConfig,
array $data = []
) {
$this->rateResultFactory = $rateResultFactory;
$this->rateMethodFactory = $rateMethodFactory;
$this->carrierConfig = $carrierConfig;
parent::__construct($scopeConfig, $data);
}
public function collectRates(\Magento\Shipping\Model\Request\Data $request): Result
{
$result = $this->rateResultFactory->create();
if (!$this->getConfigData('active')) {
return $result;
}
$this->carrierConfig->load($request);
$methods = $this->carrierConfig->getMethods();
foreach ($methods as $methodCode => $methodConfig) {
if (!$this->isMethodAvailable($methodCode, $request)) {
continue;
}
$rate = $this->rateMethodFactory->create();
$rate->setCarrier($this->_code);
$rate->setMethod($methodCode);
$rate->setCarrierTitle($this->getConfigData('title'));
$rate->setMethodTitle($methodConfig['title']);
$rate->setPrice($this->calculateRate($methodCode, $request));
$rate->setCost($this->calculateCost($methodCode, $request));
$result->append($rate);
}
return $result;
}
public function isAvailable(\Magento\Quote\Model\Quote\Address $request): bool
{
return $this->getConfigData('active');
}
}
Rate Model
Rate Calculation Model
namespace Vendor\Shipping\Model\Rate;
class RateCalculator
{
private ConfigInterface $config;
public function calculate(
string $methodCode,
\Magento\Shipping\Model\Request\Data $request
): float {
$methodConfig = $this->config->getMethodConfig($methodCode);
$baseRate = (float) $methodConfig['base_rate'];
$rateType = $methodConfig['rate_type'];
return match ($rateType) {
'weight' => $this->calculateWeightRate($baseRate, $request),
'price' => $this->calculatePriceRate($baseRate, $request),
'flat' => $baseRate,
'dimensional' => $this->calculateDimensionalRate($baseRate, $request),
};
}
private function calculateWeightRate(
float $baseRate,
\Magento\Shipping\Model\Request\Data $request
): float {
$weight = $request->getPackageWeight();
return $baseRate * $weight;
}
private function calculatePriceRate(
float $baseRate,
\Magento\Shipping\Model\Request\Data $request
): float {
$subtotal = $request->getPackageValue();
return $subtotal * ($baseRate / 100);
}
private function calculateDimensionalRate(
float $baseRate,
\Magento\Shipping\Model\Request\Data $request
): float {
$packages = $request->getPackages();
$totalDimWeight = 0;
foreach ($packages as $package) {
$dimWeight = ($package['length'] * $package['width'] * $package['height']) / 139;
$actualWeight = $package['weight'];
$totalDimWeight += max($dimWeight, $actualWeight);
}
return $baseRate * $totalDimWeight;
}
}
Rate Table
namespace Vendor\Shipping\Model\Rate;
class RateTable
{
private array $table = [
'US' => [
'weight' => [
['min' => 0, 'max' => 1, 'rate' => 5.00],
['min' => 1, 'max' => 5, 'rate' => 10.00],
['min' => 5, 'max' => 999, 'rate' => 20.00],
],
],
'CA' => [
'weight' => [
['min' => 0, 'max' => 1, 'rate' => 10.00],
['min' => 1, 'max' => 5, 'rate' => 20.00],
['min' => 5, 'max' => 999, 'rate' => 35.00],
],
],
];
public function getRate(string $country, string $type, float $value): ?float
{
foreach ($this->table[$country][$type] ?? [] as $tier) {
if ($value >= $tier['min'] && $value < $tier['max']) {
return $tier['rate'];
}
}
return null;
}
}
Tracking Implementation
Tracking Model
namespace Vendor\Shipping\Model\Carrier\Tracking;
class TrackingProvider implements \Magento\Shipping\Model\Carrier\TrackingInterface
{
private TrackingGatewayInterface $gateway;
private TrackingRepositoryInterface $trackingRepo;
public function getTrackingInfo(
\Magento\Shipping\Model\Tracking\Info $trackingInfo
): \Magento\Shipping\Model\Tracking\Result {
$result = $this->trackingResultFactory->create();
$trackingNumber = $trackingInfo->getTrackingNumber();
$carrierCode = $trackingInfo->getCarrierCode();
// Get tracking from gateway
$trackingData = $this->gateway->track($carrierCode, $trackingNumber);
$tracking = $this->trackFactory->create();
$tracking->setTracking($trackingNumber);
$tracking->setStatus($trackingData->getStatus());
$tracking->setStatusDetail($trackingData->getDescription());
$tracking->setCarrier($carrierCode);
$tracking->setCarrierTitle($this->getCarrierTitle($carrierCode));
// Add tracking history
$history = [];
foreach ($trackingData->getEvents() as $event) {
$history[] = [
'status' => $event->getStatus(),
'location' => $event->getLocation(),
'date' => $event->getDate(),
'description' => $event->getDescription(),
];
}
$result->append($tracking);
return $result;
}
}
Tracking Gateway
namespace Vendor\Shipping\Gateway\Tracking;
interface TrackingGatewayInterface
{
public function track(string $carrierCode, string $trackingNumber): TrackingData;
}
class HttpTrackingGateway implements TrackingGatewayInterface
{
private HttpClientInterface $httpClient;
private ConfigInterface $config;
public function track(string $carrierCode, string $trackingNumber): TrackingData
{
$endpoint = $this->config->getTrackingEndpoint($carrierCode);
$response = $this->httpClient->get($endpoint, [
'query' => [
'tracking' => $trackingNumber,
'api_key' => $this->config->getApiKey($carrierCode),
],
]);
return $this->parseResponse($response);
}
}
Carrier Configuration
Admin Configuration
<!-- app/code/Vendor/Shipping/etc/system.xml -->
<config>
<section id="carriers" translate="label" sortOrder="320">
<group id="customcarrier" translate="label" type="text" sortOrder="100">
<label>Custom Carrier</label>
<field id="active" type="select" sortOrder="1">
<label>Enabled</label>
<source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
</field>
<field id="title" type="text" sortOrder="10">
<label>Title</label>
</field>
<field id="name" type="text" sortOrder="20">
<label>Method Name</label>
</field>
<field id="apikey" type="obscure" sortOrder="30">
<label>API Key</label>
<backend_model>Magento\Config\Model\Config\Backend\Encrypted</backend_model>
</field>
<field id="sandbox_mode" type="select" sortOrder="40">
<label>Sandbox Mode</label>
<source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
</field>
<field id="handling_type" type="select" sortOrder="50">
<label>Handling Fee Type</label>
<source_model>Magento\Shipping\Model\Config\Source\Handling\Type</source_model>
</field>
<field id="handling_fee" type="text" sortOrder="60">
<label>Handling Fee</label>
</field>
<field id="sallowspecific" type="select" sortOrder="70">
<label>Ship to Applicable Countries</label>
<source_model>Magento\Shipping\Model\Config\Source\Allspecificcountries</source_model>
</field>
<field id="specificcountry" type="multiselect" sortOrder="80">
<label>Ship to Specific Countries</label>
<source_model>Magento\Directory\Model\Config\Source\Country</source_model>
</field>
<field id="showmethod" type="select" sortOrder="90">
<label>Show Method if Not Applicable</label>
<source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
</field>
</group>
</section>
</config>
Carrier Registration
<!-- app/code/Vendor/Shipping/etc/di.xml -->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
<type name="Magento\Shipping\Model\CarrierPool">
<arguments>
<argument name="carriers" xsi:type="array">
<item name="customcarrier" xsi:type="object">Vendor\Shipping\Model\Carrier\CustomCarrier</item>
</argument>
</arguments>
</type>
</config>
config.xml Defaults
<!-- app/code/Vendor/Shipping/etc/config.xml -->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Store:etc/config.xsd">
<default>
<carriers>
<customcarrier>
<active>0</active>
<title>Custom Carrier</title>
<name>Standard Shipping</name>
<sandbox_mode>1</sandbox_mode>
<handling_type>F</handling_type>
<handling_fee>0</handling_fee>
<sallowspecific>0</sallowspecific>
<showmethod>0</showmethod>
</customcarrier>
</carriers>
</default>
</config>
Quiz
1. What does _isFixed control?
2. How do you register a custom carrier?
3. What does config.xml provide?
Flashcards
Question
How to register a carrier?
Click to reveal answer
Answer
Add to di.xml CarrierPool arguments
Question
What is config.xml?
Click to reveal answer
Answer
Provides default values for system.xml fields
Question
What does TrackingInterface provide?
Click to reveal answer
Answer
Tracking information for shipped orders
Question
What is _isFixed?
Click to reveal answer
Answer
Boolean indicating fixed vs variable pricing
Revision Notes
Key Takeaways
- 1. Custom carriers extend AbstractCarrier and implement CarrierInterface
- 2. Rate calculation supports weight, price, flat, and dimensional methods
- 3. TrackingInterface provides shipment tracking capabilities
- 4. Carriers register via di.xml CarrierPool argument
- 5. config.xml provides default values for admin configuration
Interview Tips
- • Explain the complete carrier implementation lifecycle
- • Discuss different rate calculation methods
- • Describe how to add tracking to a carrier
- • Talk about carrier configuration hierarchy
Cheat Sheet
Custom Carrier:
extends AbstractCarrier
implements CarrierInterface
collectRates() → Rate\Result
isAvailable() → bool
Rate Types:
weight → rate × weight
price → rate × percentage
flat → fixed amount
dimensional → L×W×H / divisor
Registration:
di.xml → CarrierPool arguments
config.xml → default values
system.xml → admin fields