How to add a custom shipping calculator?

Sylius comes with several built-in shipping fee calculators like flat rate or per unit, but in real-world projects, you often need more domain-specific logic.

This guide walks you through building a custom shipping calculator that multiplies the total shipment weight by a per-channel shipping rate.


Use Case

We want to define shipping prices that scale with the total weight of the shipment. For example, if:

  • A product weighs 10 (unit-less),

  • The per-weight unit rate is $10.00 for a channel,

  • Then the shipment cost = 10 × $10 = $100.


1. Create the Calculator Class

<?php

// src/Shipping/Calculator/WeightBasedRateCalculator.php

namespace App\Shipping\Calculator;

use Sylius\Component\Core\Exception\MissingChannelConfigurationException;
use Sylius\Component\Shipping\Calculator\CalculatorInterface;
use Sylius\Component\Shipping\Model\ShipmentInterface;
use Webmozart\Assert\Assert;

final class WeightBasedRateCalculator implements CalculatorInterface
{
    public function calculate(ShipmentInterface $subject, array $configuration): int
    {
        Assert::isInstanceOf($subject, \Sylius\Component\Core\Model\ShipmentInterface::class);

        $channelCode = $subject->getOrder()->getChannel()->getCode();

        if (!isset($configuration[$channelCode])) {
            throw new MissingChannelConfigurationException(sprintf(
                'Channel %s has no amount defined for shipping method %s',
                $subject->getOrder()->getChannel()->getName(),
                $subject->getMethod()->getName(),
            ));
        }

        $rate = (int) $configuration[$channelCode]['amount'];
        $totalWeight = array_sum(array_map(
            fn($unit) => $unit->getShippable()->getWeight(),
            iterator_to_array($subject->getUnits())
        ));

        return $rate * $totalWeight;
    }

    public function getType(): string
    {
        return 'weight_based_rate';
    }
}

2. Register the Calculator as a Service

# config/services.yaml

services:
    sylius.calculator.shipping.weight_based_rate:
        class: App\Shipping\Calculator\WeightBasedRateCalculator
        tags:
            - { name: sylius.shipping_calculator, calculator: weight_based_rate, form_type: App\Form\Type\Shipping\Calculator\ChannelBasedWeightRateConfigurationType, label: sylius.form.shipping_calculator.weight_based_rate.label }

3. Create the Configuration Form Type

<?php

// src/Form/Type/Shipping/Calculator/ChannelBasedWeightRateConfigurationType.php

namespace App\Form\Type\Shipping\Calculator;

use Sylius\Bundle\CoreBundle\Form\Type\ChannelCollectionType;
use Sylius\Component\Core\Model\ChannelInterface;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\OptionsResolver\OptionsResolver;

final class ChannelBasedWeightRateConfigurationType extends AbstractType
{
    public function configureOptions(OptionsResolver $resolver): void
    {
        $resolver->setDefaults([
            'entry_type' => WeightRateConfigurationType::class,
            'entry_options' => fn (ChannelInterface $channel): array => [
                'label' => $channel->getName(),
                'currency' => $channel->getBaseCurrency()->getCode(),
            ],
        ]);
    }

    public function getParent(): string
    {
        return ChannelCollectionType::class;
    }

    public function getBlockPrefix(): string
    {
        return 'sylius_channel_based_shipping_calculator_weight_based_rate';
    }
}
<?php

// src/Form/Type/Shipping/Calculator/WeightRateConfigurationType.php

namespace App\Form\Type\Shipping\Calculator;

use Sylius\Bundle\MoneyBundle\Form\Type\MoneyType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

final class WeightRateConfigurationType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder->add('amount', MoneyType::class, [
            'label' => 'sylius.ui.amount',
            'currency' => $options['currency'],
        ]);
    }

    public function configureOptions(OptionsResolver $resolver): void
    {
        $resolver
            ->setDefaults(['data_class' => null])
            ->setRequired('currency')
            ->setAllowedTypes('currency', 'string');
    }

    public function getBlockPrefix(): string
    {
        return 'sylius_shipping_calculator_weight_rate';
    }
}

4. Register the Form Type

# config/services.yaml

services:
    sylius.form.type.shipping.calculator.channel_based_weight_based_rate_configuration:
        class: App\Form\Type\Shipping\Calculator\ChannelBasedWeightRateConfigurationType
        tags: ['form.type']

5. Add translations

# translations/messages.en.yaml

sylius:
    form:
        shipping_calculator:
            weight_based_rate:
                label: 'Rate per weight unit'

Example Setup

  • Product "Adventurous Aurora Cap" has a weight of 10.

  • Shipping method "UPS" uses the new Rate per weight unit calculator. Admin sets $10.00 per weight unit for the channel.

💡 The amount is defined in the smallest currency unit (e.g., cents for USD/EUR). If you configure $10.00, Sylius stores it as 1000.

  • At checkout, the shipping cost is calculated as 10 × $10 = $100.


🧠 Notes

Last updated

Was this helpful?