Shopware plugins often fail not because of bugs, but because of architecture decisions that don’t survive updates.
Every major Shopware release exposes the same pattern:
- plugins that rely on core overrides break
- fragile hacks stop working
- maintenance costs explode
This article is a practical guide to building update-safe Shopware plugins that survive:
- minor updates
- major versions
- core refactorings
Why “update-safe” matters in Shopware
Shopware evolves fast:
- core services are refactored
- internal logic moves
- admin & storefront architectures change
If your plugin:
- overrides core classes
- depends on internal behavior
- patches logic at the wrong layer
then updates will break it — not maybe, but eventually.
Update-safe plugins:
- integrate instead of replace
- react instead of override
- extend behavior without owning it
1️⃣ Events instead of core overrides
❌ The classic mistake: overriding core classes
class CustomCartService extends CartService
{
public function add(...) { ... }
}
This looks simple — and it’s fragile.
Why?
- core services change signatures
- internal logic shifts
- parent behavior becomes unpredictable
✅ The correct approach: react via events
class CartSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents(): array
{
return [
CartChangedEvent::class => 'onCartChanged'
];
}
public function onCartChanged(CartChangedEvent $event): void
{
$cart = $event->getCart();
}
}
Benefits
- no dependency on internal implementation
- forward-compatible
- predictable execution order
Rule of thumb:
If an event exists, never override the core.
2️⃣ Services over static helpers
❌ Anti-pattern: static helper classes
class PriceHelper
{
public static function calculate(...) { ... }
}
Problems:
- no dependency injection
- hard to test
- tightly coupled logic
- difficult to extend
✅ Proper service-based architecture
services:
MyPlugin\Service\PriceCalculator:
arguments:
- '@shopware.cart.calculator'
class PriceCalculator
{
public function __construct(
private CartCalculator $calculator
) {}
public function calculate(...) { ... }
}
3️⃣ Subscriber > Decorator (most of the time)
Decorators are powerful — but risky.
Use decorators only when:
- return values must be modified
- no suitable event exists
- logic must be changed synchronously
Subscribers are safer:
- they do not replace logic
- they survive refactors better
- they don’t rely on internal call order
4️⃣ Message Bus for async & heavy logic
❌ Anti-pattern: heavy logic in request lifecycle
Examples:
- external API calls
- batch processing
- AI requests
- expensive calculations
✅ Use the Message Bus (Symfony Messenger)
class SyncProductMessage
{
public function __construct(
public string $productId
) {}
}
class SyncProductHandler
{
public function __invoke(SyncProductMessage $message): void
{
// async processing
}
}
5️⃣ Typical anti-patterns
- accessing private core services
- relying on undocumented entity fields
- overwriting Twig blocks completely
- business logic in controllers
Always extend, never replace.
6️⃣ Plugin boundaries
A stable plugin:
- owns its own logic
- does not own Shopware core behavior
- integrates via public extension points
Ask yourself:
- would this survive a major update?
- does it rely on undocumented behavior?
7️⃣ Practical rules
Do
- events over overrides
- services over helpers
- subscribers over decorators
- message bus over sync logic
Avoid
- core inheritance
- static utilities
- undocumented internals
Final thoughts
Update-safe Shopware plugins are not about clever hacks.
They are about respecting boundaries.
If your plugin reacts instead of replacing, integrates instead of controlling and stays within public APIs, updates become boring.
And boring is good.
About the author
Stefan Pilz
Shopware Freelancer & Plugin Developer
Website: https://stefanpilz.ltd
Top comments (0)