Shipping is often treated as a backend utility, yet it directly impacts conversion rates, customer satisfaction, and operational logistics. While Shopware 6 ships with highly capable default carriers, businesses frequently require tailored solutions: warehouse-specific routing, regional restrictions, dynamic pricing tied to cart attributes, or third-party carrier APIs.
With the release of Shopware 6.7, the platform has further solidified its architectural direction. Legacy controller-based routing and direct repository manipulation have been officially deprecated in favor of a strict, typed Service Layer API. This guide walks you through creating a production-ready custom shipping method that leverages Shopware 6.7’s modern patterns, ensuring upgrade safety, optimal performance, and clean extensibility.
The Architectural Shift in Shopware 6.7
Prior to the 6.x era, developers often hooked into route events or bypassed entity boundaries to inject custom carriers. Shopware 6.7 enforces a clear separation of concerns:
- Service Layer over Controllers: All core operations now flow through dedicated service handlers and definitions.
- Typed Payloads: Raw arrays are replaced by structured write commands and definition-aware payloads.
- Rule-Centric Design: Shipping visibility is decoupled from pricing. Rules evaluate cart state, while prices handle currency and tax logic.
- Improved Caching: Shipping calculations are now aggressively cached per sales channel context. Bypassing this cache requires deliberate intervention.
Understanding these pillars ensures your custom method integrates seamlessly without introducing technical debt.
Step 1: Plugin Foundation & Installation Script
Begin by scaffolding a plugin (bin/create-plugin.sh). Instead of relying on XML seeds or direct SQL, use an installation handler that leverages dependency injection and Shopware’s modern Uuid generator.
// src/Core/ShippingMethod/CustomShippingInstaller.php
declare(strict_types=1);
namespace YourPlugin\Core\ShippingMethod;
use Shopware\Core\Framework\DataAbstractionLayer\EntityRepository;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
use Shopware\Core\Framework\Uuid\Uuid;
use Shopware\Core\System\Shipping\ShippingMethodDefinition;
class CustomShippingInstaller
{
private EntityRepository $shippingMethodRepo;
public function __construct(EntityRepository $shippingMethodDefinitionRepository)
{
$this->shippingMethodRepo = $shippingMethodDefinitionRepository;
}
public function createCustomMethod(string $name, string $technicalName, float $grossPrice): void
{
$criteria = new Criteria();
$criteria->addFilter(new EqualsFilter('technicalName', $technicalName));
if (!$this->shippingMethodRepo->searchIds($criteria)->getIds()) {
$uuid = Uuid::randomHex();
$payload = [
'id' => $uuid,
'name' => $name,
'description' => 'Custom carrier integration for region-specific routing',
'active' => true,
'averageDeliveryTime' => '2-4 business days',
'technicalName' => $technicalName,
'prices' => [
[
'currencyId' => Uuid::randomHex(), // Defaults to store currency
'gross' => $grossPrice,
'net' => 0.0,
'listPrice' => null,
'linked' => false
]
]
];
$this->shippingMethodRepo->create([$payload], new \Shopware\Core\Framework\DataAbstractionLayer\Write\EntityWriteStruct());
}
}
}
Why this matters in 6.7: The payload structure now strictly validates against the ShippingMethodDefinition. Currency IDs must be valid store currencies, and the prices array expects at least one entry. Shopware 6.7 no longer auto-generates currency fallbacks during writes; explicit mapping prevents silent failures.
Step 2: Wiring Rule-Based Visibility
A shipping method is only useful if it appears for the right customers. In Shopware 6.7, visibility is driven by criteria evaluation, not hard-coded flags. Subscribe to the appropriate event to filter or inject your method dynamically.
// src/Core/ShippingMethod/CustomShippingProvider.php
declare(strict_types=1);
namespace YourPlugin\Core\ShippingMethod;
use Shopware\Core\Framework\Event\SalesChannelContextTokenRenewedEvent;
use Shopware\Core\System\Rule\RuleEntity;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class CustomShippingProvider implements EventSubscriberInterface
{
private EntityRepository $ruleRepo;
public function __construct(EntityRepository $ruleDefinitionRepository)
{
$this->ruleRepo = $ruleDefinitionRepository;
}
public static function getSubscribedEvents(): array
{
return ['salesChannelContextTokenRenewed' => 'onContextChange'];
}
public function onContextChange(SalesChannelContextTokenRenewedEvent $event): void
{
// Fetch cart state, custom attributes, or external API data here
$context = $event->getContext();
// Example: Activate only for customers with specific attribute
// In production, cache this lookup to avoid N+1 queries
}
}
Shopware 6.7 Best Practice: Use SalesChannelContextTokenRenewedEvent to recalculate eligibility when the cart or customer changes. Avoid firing database queries inside every shipping resolver; instead, inject pre-fetched criteria into your provider. The Service Layer will automatically merge valid rules with price structures.
Step 3: Storefront Flow & Cache Awareness
When a customer reaches checkout, Shopware resolves available carriers through the ShippingMethodService. Your custom method must comply with two critical behaviors:
- Tax Handling: Prices marked as
grossare processed by Shopware’s tax renderer. If you sendnet, ensure your cart context includes tax calculation rules. - Cache Bypass Control: Shopware 6.7 caches shipping lists per sales channel ID and currency. If your method depends on dynamic factors (e.g., real-time carrier APIs, warehouse stock), register a cache invalidation listener or use
SalesChannelContext::setShippingMethodLocked(true)to force fresh evaluation.
Register your provider in config/services.yaml:
services:
YourPlugin\Core\ShippingMethod\CustomShippingProvider:
tags: ['kernel.event_subscriber']
YourPlugin\Core\ShippingMethod\CustomShippingInstaller:
autowire: true
public: true
Step 4: Testing & Debugging in 6.7+
Validation is straightforward but requires the right tools:
- Use
bin/console custom:shipping-methods:listto verify registration. - Check the profiler for
shopware.shipping.resolvertags to ensure your provider isn’t duplicate-filtering existing carriers. - Test with multiple currency contexts using the storefront’s switcher; Shopware 6.7 validates price arrays per active currency.
Conclusion
Building a custom shipping method in Shopware 6.7 is no longer about hacking around default resolvers. By embracing the Service Layer API, respecting rule-driven visibility, and aligning with modern Uuid and pricing structures, your implementation remains upgrade-safe, performant, and fully compliant with Shopware’s evolving ecosystem. Deploy carefully, monitor cache behavior under load, and let Shopware 6.7 handle the heavy lifting while you focus on business logic.