Magento 2 Experten — Hyvä Theme, Tailwind CSS & SEO aus einer Hand ›

Shipping Rate Calculation: Building Custom Rate Result Objects

Shipping Rate Calculation: Building Custom Rate Result Objects

~6 Min. Lesezeit Zuletzt aktualisiert am August 9, 2026

Chapter 67 already used Method and Result without explaining either in detail. This chapter catches up - in particular the difference between price and cost, and what changes once a carrier should offer more than one method at a time.

Two factories, two objects

MethodFactory creates exactly one Magento\Quote\Model\Quote\Address\RateResult\Method object - a single shipping option offered to the customer. ResultFactory creates the surrounding container, Magento\Shipping\Model\Rate\Result, which collects any number of Method objects via append(). FreeShippingByPoints::collectRates() calls append() exactly once - but nothing forces a carrier to offer only a single method:

// Illustrative only - FreeShippingByPoints (chapter 67) intentionally offers
// exactly one method. This is how a carrier with several tiers would look.
foreach ($this->getAvailableTiers($customerId) as $tierCode => $tierData) {
    $method = $this->rateMethodFactory->create();
    $method->setCarrier($this->_code);
    $method->setCarrierTitle($this->getConfigData('title'));
    $method->setMethod($tierCode);
    $method->setMethodTitle($tierData['label']);
    $method->setPrice($tierData['price']);
    $method->setCost($tierData['cost']);

    $result->append($method);
}

Tipp: Both MethodFactory and ResultFactory are wired in via constructor injection, like every generated factory in this module - never called directly via ObjectManager::create(). Same principle as PointsLedgerInterfaceFactory (chapter 30) or PointsBalanceWidget's dependencies (chapter 56).

price vs. cost: two different numbers

setPrice() is the amount the customer actually sees and pays in checkout. setCost() is a purely internal, accounting-oriented value - relevant for shipping cost reporting and some shipping module integrations (e.g. actual carrier costs on table-rate-style carriers), but never shows up in the frontend. FreeShippingByPoints deliberately sets both to 0 - shipping is just as free internally for Magento as it is visibly free for the customer, there's no real carrier cost difference that would need capturing.

false instead of an empty result

collectRates() in chapter 67 consistently returns false on every rejection path - inactive, country not allowed, not enough points - never a Result object with no methods attached. Magento's Magento\Shipping\Model\Shipping::collectRates() treats both cases similarly in the end (no method visible in checkout), but only false skips object instantiation entirely and makes it immediately obvious in log/debug output that the carrier deliberately said "no," rather than technically "yes, but empty."

getAllowedMethods() beyond collectRates()

getAllowedMethods() from chapters 66/67 gets called independently of collectRates() - for example by the admin config page, to display all theoretically available method codes for a carrier, or by table-rate-style comparison tools. For FreeShippingByPoints with only one method that's trivial; for a multi-method carrier like the example above, getAllowedMethods() must consistently return every tier code, not just the ones currently applicable to the logged-in customer.

Achtung: collectRates() potentially runs on every cart and checkout page, freshly on every address change. A CustomerRepositoryInterface::getById() call per request is acceptable for this tutorial module, but under very high traffic it's worth checking Magento's built-in per-request object caching of customer repository results - a dedicated in-memory cache like CacheLedgerListPlugin (chapter 39) would be the next step, should profiling actually show this as a bottleneck.

Both the payment method and the shipping method are now fully implemented. Chapter 69 checks whether they actually work together in checkout.