---
title: Event System and Subscribers | Shopware Community Hub
description: >-
  Learn how to work with Shopware’s event system, create custom subscribers, and
  handle events effectively.
canonical_url: 'https://hub.shopware.com/learn/unit/event-system-and-subscribers'
---

# Event System and Subscribers

<LearningObjectives>

- Understand the architecture of Shopware's/Symfony's event system.
- Learn how to create and register event subscribers.
- Master event priorities and propagation.
- Understand when and how to stop event propagation, and its impact on other plugins.
- Apply best practices for handling events and designing subscribers.

</LearningObjectives>

# Event System and Subscribers

The event system in Shopware is a powerful way to extend functionality **without modifying core code**.

It is based on the **observer pattern**, allowing you via plugins to hook into various points of the application lifecycle and modify or add behavior as needed in a decoupled way.

## Understanding the Event System

In Practice, Shopware **dispatches events at defined points** during the application lifecycle. Plugins can subscribe to these events and execute custom logic when they occur.

You can think of the application lifecycle as a sequence of well-defined steps. At many of these steps, Shopware exposes events that act as **extension points**. So kinda an open door in a huge house where you can step in without breaking anything by doing required stuff.

So, by subscribing to these events, you can hook into the process and add your own logic **without breaking or replacing core functionality**.

This allows you to:

- Extend or modify behavior in a decoupled way.
- React to system actions (such as entity writes or page loads).
- And integrate custom logic without tight coupling to core code.

### What are Events?

Events **execution points** in the application lifecycle where Symfony/Shopware allows you to hook into it to add your own logic.

Events are **dispatched by Symfony/Shopware**, and with your plugins, you can **subscribe** to them using **event subscribers**.

Typical examples of events include:

- An entity being written to the database.
- A storefront page being loaded (product-detail-page, checkout-cart-page, etc.).
- An order being placed or its state is changing.
- A customer logging in.

### Event Categories (Conceptual)

Events in Shopware can generally be grouped into the following categories:

- **System events:** Dispatched by Shopware core (e.g., entity writes, state machine changes).
- **Business events:** Events that represent business actions such as checkout steps.
- **Custom events:** You can also create your own custom event in your plugin if you need to.

These categories are a **conceptional aid** to help you understand when and why events exist.

Technically, all events are handled in the same way.

### Event Types (Conceptual)

Because there are many different events, it helps to think about **when** an event is dispatched.

1. **Before Events**
   - Triggered before an action is finalized
   - They allow modification or validation of incoming data
   - Example: `BeforeLineItemAddedEvent`

2. **After Events**
   - Triggered after an action has already happened
   - Allow modification or modification for follow-up actions
   - Example: `AfterLineItemAddedEvent`

3. **Generic Events**
   - Not tied to a specific entity or business action
   - Often used for framework-level concerns, such as request or kernel lifecycle events
   - Example: `KernelEvents`

## Creating Event Subscribers

An **event subscriber** is a class that listens to one or more events and reacts when those events (trigger points in the application lifecycle) are dispatched.

Subscribers implement [Symfony's](https://symfony.com/doc/7.4/components/event_dispatcher.html#using-event-subscribers) `EventSubscriberInterface` and define:

- Which events they listen to.
- Which methods should be executed when those events are executed in the application lifecycle.

The interface requires a single method, `getSubscribedEvents()`, which returns an array of event names mapped to their corresponding handler method names.

### Basic Subscriber Structure

```php
<?php declare(strict_types=1);

namespace Swag\ExamplePlugin\Subscriber;

use Shopware\Core\Framework\DataAbstractionLayer\Event\EntityWrittenEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

class ProductSubscriber implements EventSubscriberInterface
{
    public static function getSubscribedEvents(): array
    {
        return [
            'product.written' => 'onProductWritten'
        ];
    }

    public function onProductWritten(EntityWrittenEvent $event): void
    {
        // Handle the event
    }
}
```

Here we have a subscriber that listens to the `product.written` event.

When that event is dispatched, the `onProductWritten()` method is executed (you have to create this method).

The method name must match the mapping defined in `getSubscribedEvents()` exactly.

If the method does not exist or the name does not match, Symfony/Shopware cannot call the handler method.

### Registering Subscribers

Subscribers are registered through the service container in your plugin's `services.xml` file:

```xml
<service id="Swag\ExamplePlugin\Subscriber\ProductSubscriber">
    <tag name="kernel.event_subscriber"/>
</service>
```

The `kernel.event_subscriber` tag tells Symfony/Shopware that this service should be treated as an **event subscriber**.

When the service container is built, Symfony/Shopware automatically:

- Detects services with the `kernel.event_subscriber` tag.
- Reads the events defined in the `getSubscribedEvents()` method.
- Registers the subscriber with the event dispatcher.

Without this tag, the class would **never receive any events**, because it would not be registered with the event system.

## Event Priorities

When multiple subscribers listen to the **same event**, event priorities determine **the order in which they are executed**.

Each subscribed event can define a **priority value** (optional parameter).

- Higher numbers are executed first.
- Lower numbers are executed later.
- If no priority is defined, the default priority is `0`.

```php
public static function getSubscribedEvents(): array
{
    return [
        'product.written' => [
            ['onProductWritten', 1000], // High priority, executed first
            ['onProductWrittenSecond'] // Default priority (0)
            ['onProductWrittenLast', -1000] // Low priority, executed last
        ]
    ];
}
```

In this example:

- The `onProductWritten()` method runs first (priority `1000`).
- The `onProductWrittenSecond()` method runs next (priority `0`).
- The `onProductWrittenLast()` method runs last (priority `-1000`).

<Callout title="Important" type="info">

Event priorities only affect the execution order of subscribers of the **same event**.

The distinction between **before** and **after** events is based on **when the event is dispatched (executed) in the application lifecycle**, not on priority.

</Callout>

### When to Use Priorities

Use event priorities when your subscriber must run **before** or **after** other subscribers of the same event.

In real-world projects, multiple plugins often listen to the same event and implement their own business logic.

In such cases, priorities can be used to ensure that your plugin logic is executed in the correct order and that one plugin does not unintentionally interfere with or break the logic of another plugin.
This becomes especially important in larger plugin ecosystems, where multiple plugins are built on or modify the same behavior.

Avoid using priorities when the execution order does not matter or the logic is independent of other subscribers.

As best practice, only rely on priorities when necessary and make sure it is clear (for you and your team) why a specific priority is required.

## Finding the Right Events

In Shopware, there are many events that you can subscribe to. Often, you need a reference to decide which event to subscribe to. Here's a list of the most common events:

### Storefront Page Events

- `ProductPageLoadedEvent`: Is triggered when the product detail page is loaded.
- `CheckoutCartPageLoadedEvent`: Is triggered when the checkout cart page is loaded (`/checkout/cart`).
- `CheckoutConfirmPageLoadedEvent`: Is triggered when the checkout confirm page is loaded (`/checkout/confirm`).
- `CheckoutOrderPlacedEvent`: Is triggered when an order is placed (step before checkout finish).
- `CheckoutFinishPageLoadedEvent`: Is triggered when the checkout finish page is loaded (`/checkout/finish`).

### Account & Customer Events

- `CustomerLoginEvent`: Is triggered when a customer logs in.
- `AccountOverviewPageLoadedEvent`: Is triggered when the account overview page is loaded.
- `AccountOrderPageLoadedEvent`: Is triggered when the order history is called from the user's account page.

There are many more events in Shopware, but these are some of the most commonly used ones.

If you need to find additional events, it's important to know where to look in the Shopware core codebase:

- [DAL Events](https://github.com/shopware/shopware/tree/trunk/src/Core/Framework/DataAbstractionLayer/Event)
- [Storefront page-related events](https://github.com/shopware/shopware/tree/trunk/src/Storefront/Page)
- [Storefront pagelet-related events](https://github.com/shopware/shopware/tree/trunk/src/Storefront/Pagelet)

In more advanced customizations, knowing where events are defined is essential. Instead of guessing event names, you should inspect the Shopware core to understand which events exist, when they are triggered, and which data they provide.

If you see a `LoadedEvent.php` file in the core, it usually indicates that there is an event you can subscribe to.

## Stopping Event Propagation

In some cases, you may want to stop an event from being processed by other subscribers. This is done using the `stopPropagation()` method, but it comes with important implications.

### How to Stop Events

Events that extend Symfony's `Event` class (or Shopware's event classes that extend it) support stopping propagation:

```php
<?php declare(strict_types=1);

namespace Swag\ExamplePlugin\Subscriber;

use Shopware\Core\Framework\DataAbstractionLayer\Event\EntityWrittenEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

class ProductSubscriber implements EventSubscriberInterface
{
    public static function getSubscribedEvents(): array
    {
        return [
            'product.written' => 'onProductWritten'
        ];
    }

    public function onProductWritten(EntityWrittenEvent $event): void
    {
        // Check some condition
        if ($this->shouldStopEvent($event)) {
            $event->stopPropagation();
            return;
        }
        
        // Continue processing...
    }
    
    private function shouldStopEvent(EntityWrittenEvent $event): bool
    {
        // Your logic to determine if event should be stopped
        return false;
    }
}
```

### Impact on Other Subscribers

**Critical:** When you call `stopPropagation()`, **all subsequent subscribers will not receive the event**, regardless of their priority. This includes:

- Other subscribers in your own plugin
- Subscribers from other plugins
- Core Shopware subscribers (if they have a lower priority)

**Example scenario:**

```php
// Plugin A - Priority 1000
public function onProductWritten(EntityWrittenEvent $event): void
{
    if ($someCondition) {
        $event->stopPropagation(); // Stops here!
    }
}

// Plugin B - Priority 500 (will NOT execute if Plugin A stops propagation)
public function onProductWritten(EntityWrittenEvent $event): void
{
    // This will never run if Plugin A stopped propagation
}

// Plugin C - Priority -1000 (will NOT execute if Plugin A stops propagation)
public function onProductWritten(EntityWrittenEvent $event): void
{
    // This will also never run
}
```

### When to Stop Events

**Use `stopPropagation()` sparingly and only when:**

1. **You're handling a "Before" event** and want to prevent the original action
   - Example: Preventing a product save based on validation
   - Example: Canceling an order before it's processed

2. **You're certain no other plugin needs the event**
   - This is rarely the case in a plugin ecosystem
   - Consider that other plugins might depend on the event

3. **You're implementing security or critical business logic**
   - Example: Blocking unauthorized access
   - Example: Enforcing strict business rules

**Avoid stopping events when:**

- You're just modifying data (use the event data instead)
- Other plugins might need the event
- You're unsure about the impact
- You're handling "After" events (the action already happened)

<Callout title="Important" type="info">

If a previous subscriber (with a higher priority) stopped propagation, later subscribers are **not executed at all**.

</Callout>

### Best Practice: Use Event Data Instead

Instead of stopping events, consider modifying the event data or using a different approach:

```php
// ❌ Bad: Stopping event prevents other plugins from working
public function onBeforeProductSave(BeforeProductSaveEvent $event): void
{
    if ($this->shouldPreventSave($event)) {
        $event->stopPropagation(); // Blocks other plugins!
    }
}

// ✅ Better: Modify the event data or throw an exception
public function onBeforeProductSave(BeforeProductSaveEvent $event): void
{
    if ($this->shouldPreventSave($event)) {
        // Option 1: Modify data to prevent save
        $event->getProductData()['active'] = false;
        
        // Option 2: Throw an exception (if appropriate)
        throw new \RuntimeException('Product save prevented by validation');
    }
}
```

### Real-World Example: When Stopping Makes Sense

Here's a scenario where stopping an event is appropriate:

```php
<?php declare(strict_types=1);

namespace Swag\SecurityPlugin\Subscriber;

use Shopware\Core\Checkout\Order\OrderEvents;
use Shopware\Core\Checkout\Order\Event\OrderStateMachineStateChangeEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

class OrderSecuritySubscriber implements EventSubscriberInterface
{
    public static function getSubscribedEvents(): array
    {
        return [
            OrderEvents::STATE_MACHINE_STATE_CHANGE => [
                ['onOrderStateChange', 1000] // High priority to check first
            ]
        ];
    }

    public function onOrderStateChange(OrderStateMachineStateChangeEvent $event): void
    {
        $order = $event->getOrder();
        
        // Critical security check: Prevent order completion if fraud detected
        if ($this->isFraudulentOrder($order)) {
            // Stop the state change - this is critical security logic
            $event->stopPropagation();
            
            // Log the attempt
            $this->logger->warning('Fraudulent order prevented', [
                'orderId' => $order->getId()
            ]);
            
            return;
        }
    }
    
    private function isFraudulentOrder($order): bool
    {
        // Your fraud detection logic
        return false;
    }
}
```

In this case, stopping the event is appropriate because:

- It's a security-critical operation
- The order state change should not proceed if fraud is detected
- It's a "before" event where stopping prevents the action

## Best Practices

1. **Keep Subscribers Focused**
   - Each subscriber should handle one specific concern
   - Avoid complex logic in subscribers
   - Use services for business logic

2. **Event Naming**
   - Use clear, descriptive names
   - Follow the pattern: `entity.action.event`
   - Example: `product.written`, `order.placed`

3. **Error Handling**
   - Always implement proper error handling
   - Log errors appropriately
   - Don't let subscriber errors break the application

4. **Performance**
   - Keep subscriber methods lightweight
   - Use async processing for heavy tasks
   - Consider event priorities carefully

5. **Event Propagation**
   - **Avoid stopping events unless absolutely necessary** (security, critical business rules)
   - Remember that `stopPropagation()` affects all subsequent subscribers, including other plugins
   - Prefer modifying event data over stopping propagation
   - Document in your plugin's README if you stop any events
   - Test your plugin alongside other plugins to ensure compatibility

## Practical Example: Review Form Subscriber

Let's examine a real-world example from the AcademyReviewExtension plugin. This subscriber handles product review submissions and automatically processes uploaded media files:

<Callout title="Follow along with the plugin" type="info">

You can clone the AcademyReviewExtension plugin to follow along with the examples:

```shell
cd custom/plugins
git clone git@github.com:ShopwareAcademy/AcademyReviewExtension.git
cd AcademyReviewExtension
```

</Callout>

```php
<?php declare(strict_types=1);

namespace AcademyReviewExtension\Subscriber;

use AcademyReviewExtension\Service\ReviewMediaService;
use Shopware\Core\Content\Product\ProductEvents;
use Shopware\Core\Framework\DataAbstractionLayer\Event\EntityWrittenEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\RequestStack;

class ReviewFormSubscriber implements EventSubscriberInterface
{
    public function __construct(
        private readonly ReviewMediaService $reviewMediaService,
        private readonly RequestStack       $requestStack
    ) { }

    public static function getSubscribedEvents(): array
    {
        return [
            ProductEvents::PRODUCT_REVIEW_WRITTEN_EVENT => 'onReviewWritten',
        ];
    }

    public function onReviewWritten(EntityWrittenEvent $event): void
    {
        $request = $this->requestStack->getCurrentRequest();
        if (null === $request) {
            return;
        }

        $reviewIds = $event->getIds();
        if (empty($reviewIds)) {
            return;
        }

        $context = $event->getContext();

        // Handle image uploads
        $imageIds = [];
        // The dedicated form uses reviewImages[] with "multiple", so Symfony provides an array of UploadedFile objects
        $imageFiles = $request->files->get('reviewImages', []);
        if (!empty($imageFiles)) {
            $imageIds = $this->reviewMediaService->collectUploadedMediaIds($imageFiles, $context);
        }

        // Handle video uploads
        $videoIds = [];
        // The dedicated form uses reviewVideos[] with "multiple", so Symfony provides an array of UploadedFile objects
        $videoFiles = $request->files->get('reviewVideos', []);
        if (!empty($videoFiles)) {
            $videoIds = $this->reviewMediaService->collectUploadedMediaIds($videoFiles, $context);
        }

        $reviewId = $reviewIds[0];

        // Save media associations
        if (!empty($imageIds) || !empty($videoIds)) {
            $this->reviewMediaService->saveReviewMedia($reviewId, $imageIds, $videoIds, $context);
        }
    }
}
```

### Key Features of This Subscriber

1. **Event Subscription**: Listens to `ProductEvents::PRODUCT_REVIEW_WRITTEN_EVENT`
2. **File Processing**: Automatically handles image and video uploads when reviews are submitted
3. **Media Management**: Creates media entities and saves files using Shopware's media system
4. **Error Handling**: Includes validation and error handling for file uploads
5. **Service Integration**: Uses dedicated services for business logic separation

### Registration in services.xml

```xml
<service id="AcademyReviewExtension\Subscriber\ReviewFormSubscriber">
    <argument type="service" id="AcademyReviewExtension\Service\ReviewMediaService"/>
    <argument type="service" id="request_stack"/>
    <tag name="kernel.event_subscriber"/>
</service>
```

This example demonstrates how subscribers can:

- React to specific business events (review submissions)
- Process complex data (file uploads)
- Integrate with multiple Shopware services
- Maintain clean separation of concerns

If you want to check the related methods in the service class, you can find them in the `ReviewMediaService.php` file [here](https://github.com/ShopwareAcademy/AcademyReviewExtension/blob/main/src/Service/ReviewMediaService.php).

Also, you can open the collapsible below.

<CollapsibleGroup>

<CollapsibleSection title="ReviewMediaService">

```php
<?php declare(strict_types=1);

namespace AcademyReviewExtension\Service;

use Shopware\Core\Content\Media\File\FileSaver;
use Shopware\Core\Content\Media\File\MediaFile;
use Shopware\Core\Framework\Context;
use Shopware\Core\Framework\DataAbstractionLayer\EntityRepository;
use Shopware\Core\Framework\Uuid\Uuid;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Throwable;

class ReviewMediaService
{
    public function __construct(
        private readonly EntityRepository $reviewImageRepository,
        private readonly EntityRepository $reviewVideoRepository,
        private readonly EntityRepository $mediaRepository,
        private readonly FileSaver        $fileSaver,
    ) {
    }

    public function saveReviewMedia(string $reviewId, array $imageIds, array $videoIds, Context $context): void
    {
        // Save image associations
        if (false === empty($imageIds)) {
            $imageData = [];
            foreach ($imageIds as $imageId) {
                $imageData[] = [
                    'reviewId' => $reviewId,
                    'mediaId' => $imageId
                ];
            }
            $this->reviewImageRepository->create($imageData, $context);
        }

        // Save video associations
        if (false === empty($videoIds)) {
            $videoData = [];
            foreach ($videoIds as $videoId) {
                $videoData[] = [
                    'reviewId' => $reviewId,
                    'mediaId' => $videoId
                ];
            }
            $this->reviewVideoRepository->create($videoData, $context);
        }
    }

    public function updateReviewMedia(string $reviewId, array $imageIds, array $videoIds, Context $context): void
    {
        // Remove existing associations
        $this->removeReviewMedia($reviewId, $context);
        
        // Add new associations
        $this->saveReviewMedia($reviewId, $imageIds, $videoIds, $context);
    }

    public function removeReviewMedia(string $reviewId, Context $context): void
    {
        // Remove all image associations for this review
        $this->reviewImageRepository->delete([
            ['reviewId' => $reviewId]
        ], $context);

        // Remove all video associations for this review
        $this->reviewVideoRepository->delete([
            ['reviewId' => $reviewId]
        ], $context);
    }
    public function collectUploadedMediaIds(array $files, Context $context): array
    {
        $mediaIds = [];
        /** @var UploadedFile $file */
        foreach ($files as $file) {
            if (false === $file->isValid()) {
                continue;
            }

            $mediaId = $this->uploadFile($file, $context);
            if (null === $mediaId) {
                continue;
            }

            $mediaIds[] = $mediaId;
        }

        return $mediaIds;
    }

    public function uploadFile(UploadedFile $file, Context $context): ?string
    {
        try {
            // Validate file
            if (false === $this->validateFile($file)) {
                return null;
            }

            $mediaId = Uuid::randomHex();
            $baseFileName = pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME);

            // Create Media Entity
            $this->mediaRepository->create([
                [
                    'id' => $mediaId,
                    'name' => $baseFileName,
                ]
            ], $context);

            // Create MediaFile object
            $mediaFile = new MediaFile(
                $file->getPathname(),
                $file->getMimeType(),
                $file->getClientOriginalExtension(),
                $file->getSize()
            );

            // Save the file
            $this->fileSaver->persistFileToMedia(
                $mediaFile,
                $baseFileName,
                $mediaId,
                $context
            );

            return $mediaId;
        } catch (Throwable $t) {
            // Log error or handle it appropriately
            return null;
        }
    }

    public function validateFile(UploadedFile $file): bool
    {
        $mimeType = $file->getMimeType();
        if (null === $mimeType) {
            return false;
        }

        $isImage = str_starts_with($mimeType, 'image/');
        $isVideo = str_starts_with($mimeType, 'video/');
        if (false === $isImage && false === $isVideo) {
            return false;
        }

        // Image validation
        if ($isImage) {
            $allowedImageTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp'];
            if (false === in_array($mimeType, $allowedImageTypes, true)) {
                return false;
            }

            $maxImageSizeLimit = 5 * 1024 * 1024; // 5MB
            if ($file->getSize() > $maxImageSizeLimit) {
                return false;
            }
        }

        // Video validation
        if ($isVideo) {
            $allowedVideoTypes = ['video/mp4', 'video/webm', 'video/ogg', 'video/avi', 'video/mov'];
            if (false === in_array($mimeType, $allowedVideoTypes, true)) {
                return false;
            }

            $maxVideoSizeLimit = 50 * 1024 * 1024; // 50MB
            if ($file->getSize() > $maxVideoSizeLimit) {
                return false;
            }
        }

        return true;
    }
} 
```

</CollapsibleSection>

</CollapsibleGroup>

## Summary

Well done! In this learning unit, you learned:

- How the Shopware event system works.
- How to create and register event subscribers using Symfony's `EventSubscriberInterface`.
- How event priorities influence execution order and when priorities should be used.
- How to find relevant events in the Shopware core codebase.
- How to design subscribers that stay lightweight and delegate business logic to services.
- Best practices for creating and using event subscribers.

You now have a solid understanding of events and subscribers in Shopware. With this knowledge, you can create robust and maintainable plugins that integrate seamlessly with Shopware's core functionality.
