Shopware 6 has long distinguished itself through a highly modular, plugin-ready architecture, but that modularity only functions well because of its event-driven core. Whether you are building a payment gateway, syncing inventory with an external warehouse, or intercepting checkout behavior without touching framework files, custom events and subscribers form the backbone of safe, upgrade-resilient development. In Shopware 6.7, the underlying Symfony EventDispatcher remains robust, but modern PHP standards, stricter dependency injection rules, and compiler optimizations have refined how we approach extension architecture. This guide walks you through implementing custom events and subscribers that align with Shopware 6.7+ best practices, emphasizing clean code, performance, and maintainability.

Understanding the Publish-Subscribe Pattern in Shopware

At its core, Shopware’s event system follows a classic publish-subscribe model. Your code publishes an event at a specific lifecycle boundary. Registered subscribers react to that event asynchronously or synchronously, depending on your design. This decoupling is what allows third-party plugins to modify behavior without forking or patching framework files. In Shopware 6.7, events are still plain PHP objects, but the ecosystem strongly encourages immutability, explicit typing, and separation of concerns. Subscribers implement Symfony\Contracts\EventDispatcher\EventSubscriberInterface and declare their interest via getSubscribedEvents(). The framework then wires everything together at compile time through the Dependency Injection Container (DIC).

Crafting a Custom Event Class

Events should be lightweight data carriers. They must not contain business logic, database queries, or heavy service dependencies. In Shopware 6.7, custom events typically extend Shopware\Core\Framework\Event\AbstractEvent, which provides convenient helpers for entity hydration and metadata access, though it is technically optional if you only need raw payloads.

<?php declare(strict_types=1);

namespace Your\Vendor\ExtensionName\Framework\Event;

use Shopware\Core\Framework\Event\AbstractEvent;

final class CustomOrderPlacedEvent extends AbstractEvent
{
    public function __construct(
        private readonly string $orderId,
        private readonly string $email,
        private readonly float $totalAmount
    ) {}

    public function getOrderId(): string { return $this->orderId; }
    public function getEmail(): string { return $this->email; }
    public function getTotalAmount(): float { return $this->totalAmount; }
}

Notice the use of PHP 8.1+ constructor property promotion and readonly properties. Shopware 6.7 runs on PHP 8.2+, so immutability is both encouraged and safe. Avoid injecting repositories or heavy services directly into events. Pass identifiers or serialized data instead, and resolve dependencies inside your subscriber to prevent circular references and unnecessary memory consumption during dispatch.

Writing the Subscriber

Subscribers live in src/Subscriber/ and implement EventSubscriberInterface. The contract requires a static getSubscribedEvents() method that maps event classes to listener methods. You can optionally assign execution priority (higher values run earlier). In Shopware 6.7, explicit type hints are mandatory for reliable service resolution and IDE support.

<?php declare(strict_types=1);

namespace Your\Vendor\ExtensionName\Framework\Subscriber;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Your\Vendor\ExtensionName\Framework\Event\CustomOrderPlacedEvent;

final class OrderNotificationSubscriber implements EventSubscriberInterface
{
    public static function getSubscribedEvents(): array
    {
        return [
            CustomOrderPlacedEvent::class => ['onOrderPlaced', 10],
        ];
    }

    public function onOrderPlaced(CustomOrderPlacedEvent $event): void
    {
        $orderId = $event->getOrderId();
        $total   = $event->getTotalAmount();

        // Delegate heavy work to a dedicated service or command queue.
        // Never block the event dispatcher with synchronous API calls.
        $this->syncToExternalSystem($orderId, $total);
    }

    private function syncToExternalSystem(string $orderId, float $total): void
    {
        // Implementation: message bus dispatch, HTTP client call, etc.
    }
}

The priority 10 ensures this subscriber runs before default-ordered listeners but after high-priority system hooks. If you need to modify or cancel the original request (rare with custom events), return data from the listener; Shopware 6.7 treats subscribers as fire-and-forget by design unless they listen to stoppable framework events.

Registering via Dependency Injection

Shopware does not auto-discover subscribers. You must register them in src/Resources/config/services.xml. In Shopware 6.7, the XML structure remains consistent, but you should avoid autowire="true" for custom extensions unless you explicitly want constructor injection guessing. Explicit <argument> tags improve readability and prevent runtime DI failures.

<?xml version="1.0" ?>
<container xmlns="http://symfony.com/schema/dic/services"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xsi:schemaLocation="http://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd">

    <defaults public="false" autowire="false" autoconfigure="true" />

    <service id="YourVendorExtensionName\Framework\Subscriber\OrderNotificationSubscriber">
        <tag name="kernel.event_subscriber" />
    </service>

</container>

The kernel.event_subscriber tag triggers Symfony’s compiler pass to inspect getSubscribedEvents() and register listeners. Shopware 6.7 optimizes this at cache compile time, meaning subscribers are resolved without reflection overhead during runtime. Always clear var/cache/ after adding new subscribers.

Dispatching Custom Events Safely

Never instantiate events manually in controllers or repositories. Instead, inject Symfony\Contracts\EventDispatcher\EventDispatcherInterface into your service and dispatch through the container. This ensures event listeners can be intercepted by debugging tools, rate limiters, and message queue adapters.

$event = new CustomOrderPlacedEvent($orderId, $customerEmail, $cart->getPrice()->getTotalPrice());
$this->dispatcher->dispatch($event);

Pass only what is necessary. Large payloads trigger memory spikes during synchronous dispatch chains. If you need to react to DAL operations (e.g., after a write), prefer Shopware’s native EntityWrittenContainerEvent over custom alternatives unless you strictly require decoupled business boundaries.

Shopware 6.7 Specific Recommendations

  1. Namespace Strictly: Place events in src/Event/ and subscribers in src/Subscriber/. Follow Shopware\Core\... conventions for consistency.
  2. Avoid Subscriber Flooding: Register only one subscriber per logical domain. If multiple listeners exist, group them into a single class to reduce DIC overhead.
  3. Leverage PHP 8.2+ Features: Use readonly properties, intersection types where appropriate, and strict scalar declarations. Shopware’s compiler respects these during service compilation.
  4. Debugging in 6.7: Use symfony/console’s event-dispatcher:dump to verify registered subscribers. Check var/log/ for unhandled event exceptions, which now surface clearer stack traces in newer debug environments.
  5. Message Queue Integration: For async workflows, dispatch events to Shopware’s message queue via Symfony\Component\Messenger\MessageBusInterface instead of the event dispatcher. Keep the subscriber lightweight and let the queue handle retries and scaling.

Conclusion

Custom events and subscribers are not just a technical pattern in Shopware; they are an architectural discipline. By embracing immutability, explicit dependency injection, and strict separation between event definition and reaction logic, your extensions will remain stable across framework upgrades, scale under load, and integrate cleanly into larger e-commerce ecosystems. Shopware 6.7 raises the baseline for clean code without breaking backward compatibility—it simply expects developers to write with intention. Start small: define a focused event, wire a single subscriber, dispatch it at a clear boundary, and measure the architectural clarity it brings. The payoffs in maintainability, testability, and upgrade safety will quickly justify the upfront discipline.