---
title: Dependency Injection and Services | Shopware Community Hub
description: >-
  Learn how to use dependency injection, create and register services, and apply
  best practices for structuring business logic in Shopware plugins.
canonical_url: 'https://hub.shopware.com/learn/unit/dependency-injection-and-services'
---

# Dependency Injection and Services

<LearningObjectives>

- Understand the concept of dependency injection in the context of Symfony/Shopware.
- Learn how to create and register services using the service container.
- Implement and inject service dependencies via constructor injection.
- Understand how service decoration works and when to use it.
- Learn how service tags influence how services are processed by the framework.
- Apply best practices for designing reusable, and testable services.

</LearningObjectives>

# Dependency Injection and Services

Dependency Injection (DI) is a fundamental concept in modern PHP applications, including Shopware. It helps create loosely coupled, maintainable, and testable code.

This learning unit will guide you through creating and using services in your Shopware plugins.

## Understanding Dependency Injection

### Reminder: What is Dependency Injection?

Dependency Injection is a design pattern where a class **does not create** the objects it depends on. Instead, those dependencies are **provided from the outside**.

In Shopware, the Symfony service container is responsible for creating objects and injecting their dependencies. You define it in your plugin's `services.xml` file.

In practice, this usually means: **you declare your dependencies in a constructor**, and Shopware injects them when the service is instantiated.

<Callout title="Refer Back" type="info">

For a more detailed introduction to DI, refer back to the learning unit in the related [Essentials Learning Path](https://hub.shopware.com/learn/unit/introduction-to-the-basic-architectural-pattern#whatisdependencyinjection).

</Callout>

### Benefits of DI

Dependency Injection provides several practical benefits that are especially important in larger Shopware projects.

1. **Testability**
   By injecting dependencies instead of creating them directly, services can be tested in isolation.
   Dependencies can easily be replaced with mocks or stubs, which means make unit tests simpler and more reliable

2. **Maintainability**
   Dependency Injection makes dependencies explicit and reduces tight coupling between classes.
   This improves readability and makes refactoring easier because changes in one part of the system are less likely to affect others.

3. **Reusability**
   Services that rely on dependency injection can be reused across different parts of a plugin, such as controllers, event subscribers, or scheduled tasks.
   This leads to better code organization and helps avoid duplicated logic.

## Creating Services

When developing with Shopware, it’s best practice to move business logic into dedicated service classes instead of placing it directly in controllers.

This keeps controllers clean and focused solely on handling HTTP requests and responses, while services **encapsulate** and organize the actual logic and dependencies.

Not only does this make your application easier to maintain and test, but it also allows you to reuse the same services across different endpoints, such as custom Store API routes.

This ensures consistent behavior for both **headless** and **storefront** scenarios.

As a general rule, strive to keep controllers as lightweight as possible and delegate business processes to services.

### Basic Service Structure

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

namespace Swag\ExamplePlugin\Service;

use Psr\Log\LoggerInterface;
use Shopware\Core\Framework\DataAbstractionLayer\EntityRepository;

class ProductService
{
    public function __construct(
        private EntityRepository $productRepository,
        private LoggerInterface  $logger
    ) {
    }

    public function updateProduct(string $productId, array $data): void
    {
        // Business logic here
    }
}
```

This example shows `LoggerInterface`. It allows the service to log important information such as errors or warnings without being tightly coupled to a specific logging implementation.

In Shopware, the logger is provided by Symfony's logging system (based on Monolog), which implements the PSR-3 standard. For more details, see the [Symfony documentation](https://symfony.com/doc/current/logging.html).

### Registering Services

Services are registered in your plugin's `services.xml` file:

```xml
<services>
    <service id="Swag\ExamplePlugin\Service\ProductService">
        <argument type="service" id="product.repository"/>
        <argument type="service" id="monolog.logger"/>
    </service>
</services>
```

Now you have see dependency injection in action: You defined the dependencies in the constructor, described their meaning in the `services.xml` file. Shopware (Symfony) injects them automatically when the service was instantiated.

## Service Decoration

Service decoration allows you to modify the behavior of an existing service **without changing its original implementation**.

Instead of replacing a service completely, a decorator **wraps** the original service.

When you decorate a concrete core service such as `SystemConfigService`, the decorator must still stay compatible with the original service. This means it must keep the parent constructor intact and delegate the public methods it wants to preserve to the inner service.

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

namespace Swag\ExamplePlugin\Service;

use Doctrine\DBAL\Connection;
use Shopware\Core\Framework\Adapter\Cache\CacheTagCollector;
use Shopware\Core\Framework\Bundle;
use Shopware\Core\System\SystemConfig\AbstractSystemConfigLoader;
use Shopware\Core\System\SystemConfig\SymfonySystemConfigService;
use Shopware\Core\System\SystemConfig\SystemConfigService;
use Shopware\Core\System\SystemConfig\Util\ConfigReader;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;

class DecoratedSystemConfigService extends SystemConfigService
{
    public function __construct(
        private readonly SystemConfigService $innerService,
        Connection $connection,
        ConfigReader $configReader,
        AbstractSystemConfigLoader $loader,
        EventDispatcherInterface $dispatcher,
        SymfonySystemConfigService $symfonySystemConfigService,
        CacheTagCollector $cacheTagCollector
    ) {
        parent::__construct(
            $connection,
            $configReader,
            $loader,
            $dispatcher,
            $symfonySystemConfigService,
            $cacheTagCollector
        );
    }

    public function get(string $key, ?string $salesChannelId = null)
    {
        $value = $this->innerService->get($key, $salesChannelId);

        // Add custom logic before or after the original method.
        return $value;
    }

    public function getString(string $key, ?string $salesChannelId = null): string
    {
        return $this->innerService->getString($key, $salesChannelId);
    }

    public function getInt(string $key, ?string $salesChannelId = null): int
    {
        return $this->innerService->getInt($key, $salesChannelId);
    }

    public function getFloat(string $key, ?string $salesChannelId = null): float
    {
        return $this->innerService->getFloat($key, $salesChannelId);
    }

    public function getBool(string $key, ?string $salesChannelId = null): bool
    {
        return $this->innerService->getBool($key, $salesChannelId);
    }

    public function all(?string $salesChannelId = null): array
    {
        return $this->innerService->all($salesChannelId);
    }
}
```

Register the decoration in the `services.xml` file:

```xml
<?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 http://symfony.com/schema/dic/services/services-1.0.xsd">
  <services>
    <service id="Swag\ExamplePlugin\Service\DecoratedSystemConfigService"
             decorates="Shopware\Core\System\SystemConfig\SystemConfigService">
      <argument type="service" id=".inner"/>
      <argument type="service" id="Doctrine\DBAL\Connection"/>
      <argument type="service" id="Shopware\Core\System\SystemConfig\Util\ConfigReader"/>
      <argument type="service" id="Shopware\Core\System\SystemConfig\SystemConfigLoader"/>
      <argument type="service" id="event_dispatcher"/>
      <argument type="service" id="Shopware\Core\System\SystemConfig\SymfonySystemConfigService"/>
      <argument type="service" id="Shopware\Core\Framework\Adapter\Cache\CacheTagCollector"/>
    </service>
  </services>
</container>
```

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

Since Shopware `6.6.10.x`, you can shorten `id="Swag\ExamplePlugin\Service\DecoratedSystemConfigService.inner"` to `id=".inner"`.

</Callout>

The key difference between registering "normal" services and decorating them in the `services.xml` file are:

- You add the `decorates` attribute to the decoration service, which points to the original core service ID.
- You inject the original service via the `.inner` suffix, so your decorator can delegate calls to it.
- Because `SystemConfigService` is a concrete class, the decorator still needs the constructor dependencies of the parent service. In this example, these are added explicitly instead of relying on autowiring.
- If you decorate a concrete service like this, you should forward every public method your plugin relies on. Otherwise, inherited methods may behave unexpectedly.

This ensures that Shopware injects the decorator instead of the original service, while still keeping the original implementation available as the inner service.

<CollapsibleGroup>

<CollapsibleSection title="How to Verify the Decorator Example">

If you build this example in a local test plugin, you can verify it with a small console command. For example, you could add this command class:

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

namespace Swag\ExamplePlugin\Command;

use Shopware\Core\System\SystemConfig\SystemConfigService;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

#[AsCommand(
    name: 'swag-example-plugin:fixed-decorator-demo',
    description: 'Shows the corrected SystemConfigService decorator behavior'
)]
class FixedDecoratorDemoCommand extends Command
{
    public function __construct(
        private readonly SystemConfigService $systemConfigService
    ) {
        parent::__construct();
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $value = $this->systemConfigService->get('core.listing.allowBuyInListing');
        $all = $this->systemConfigService->all();

        $output->writeln('Delegated get(): ' . var_export($value, true));
        $output->writeln('Delegated all(): worked');
        $output->writeln('Known key exists in all(): ' . var_export(array_key_exists('core', $all), true));

        return Command::SUCCESS;
    }
}
```

And register it in your `services.xml` file:

```xml
<service id="Swag\ExamplePlugin\Command\FixedDecoratorDemoCommand"/>
```

Then run:

```shell
bin/console swag-example-plugin:fixed-decorator-demo
```

The expected result is similar to:

- `Delegated get(): ...`
- `Delegated all(): worked`

This shows that the decorator not only extends `get()`, but also keeps other delegated methods working correctly.

</CollapsibleSection>

</CollapsibleGroup>

## Service Tags

Besides injecting dependencies, services can be **tagged** in the `services.xml` file.

Service tags are a way to tell Symfony (and Shopware) that a service should be **registered or processed in a specific way**.

The tag itself does not change the service. Instead, the framework scans the service container for specific tags and then performs additional setup internally, as required for the given use case.

Which tag you use depends on what the service class actually does.

**Examples:**

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

<service id="Swag\ExamplePlugin\Core\Content\YourCustomEntity\YourCustomEntityDefinition">
    <tag name="shopware.entity.definition"/>
</service>
```

In these examples:

- `kernel.event_subscriber` tells Symfony (and Shopware) to register this service with the event dispatcher.
- `shopware.entity.definition` registers a DAL `EntityDefinition` so Shopware can discover the entity and provide the related repository integration.

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

Do not confuse **service tags** with **service arguments**.

- `<argument ... />` is used to inject dependencies into a constructor.
- `<tag ... />` tells Symfony/Shopware how a service should be registered or processed.

</Callout>

With service tags, you indicate **how the framework should treat a service**, and the required setup is handled internally by the framework.

**Common tags in Shopware:**

- `shopware.entity.definition`: Registers a DAL `EntityDefinition` so Shopware can discover it and set up the entity/repository integration.
- `shopware.entity.extension`: Registers an `EntityExtension` so Shopware can extend existing core entities with additional fields or associations.
- `kernel.event_subscriber`: Registers the service as an event subscriber (implements `EventSubscriberInterface`) so its subscribed events are wired automatically.
- `shopware.migration.*`: Used by the **Migration Assistant** to register extension services (e.g. `shopware.migration.reader`, `shopware.migration.converter`, `shopware.migration.writer`) so they are discovered during a migration run.
- `shopware.cart.line_item.factory`: Registers a custom line item factory handler so your line item type becomes creatable via the `LineItemFactoryRegistry`.
- `shopware.cart.processor`: Registers a cart processor so it is executed during cart recalculation.
- `shopware.cart.validator`: Registers a cart validator so it is executed during the cart validation phase.
- `console.command`: Registers a Symfony console command service so it shows up in `bin/console` (often auto-registered if you follow Shopware/Symfony conventions).

## Best Practices

### Service Design

Keep services focused and single-purpose. Smaller services are easier to understand, test, and reuse.

Use interfaces where it makes sense to reduce tight coupling between implementations.

Follow [SOLID principles](https://www.digitalocean.com/community/conceptual-articles/s-o-l-i-d-the-first-five-principles-of-object-oriented-design) to keep your code flexible and maintainable.

### Dependency Management

Use constructor injection so dependencies are explicit and easy to reason about.

Avoid the [service locator pattern](https://en.wikipedia.org/wiki/Service_locator_pattern), as it hides dependencies and makes code harder to test.

Keep dependency graphs shallow to prevent services from becoming overly complex.

### Performance

Be mindful of heavy dependencies in frequently used services.

Consider service scope if a service should not be created on every request.

Use [lazy services](https://symfony.com/doc/7.4/service_container/lazy_services.html) when a dependency is expensive and not always needed. In Shopware (Symfony), this can be configured by marking a service as `lazy="true"` in the `services.xml` file, so it is only instantiated when it is actually used.

**Example:**

```xml
<service id="App\Twig\AppExtension" lazy="true"/>
```

### Testing

Design services so they can be tested in isolation.

Use interfaces for dependencies that need to be mocked or replaced in tests.

Keep business logic in services, not in controllers or subscribers!

## Practical Example: Review Media Service

Let's examine a real-world service from the [AcademyReviewExtension](https://github.com/ShopwareAcademy/AcademyReviewExtension) plugin. This service demonstrates dependency injection with multiple repository dependencies:

<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\Service;

use Shopware\Core\Framework\Context;
use Shopware\Core\Framework\DataAbstractionLayer\EntityRepository;

class ReviewMediaService
{
    public function __construct(
        private EntityRepository $reviewImageRepository,
        private EntityRepository $reviewVideoRepository
    ) {
    }

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

        // Save video associations
        if (!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);
    }
}
```

### Key Features of This Service

1. **Multiple Dependencies**: Injects two different repositories (`reviewImageRepository` and `reviewVideoRepository`).
2. **Single Responsibility**: Focuses solely on managing review media associations.
3. **Clean Interface**: Provides clear methods for create, update, and delete operations.
4. **Context Awareness**: Uses Shopware's `Context` consistently for all operations.
5. **Error Handling**: Relies on Shopware's DAL for validation and persistence.

### Registration in the services.xml

```xml
<service id="AcademyReviewExtension\Service\ReviewMediaService">
    <argument type="service" id="academy_review_image.repository"/>
    <argument type="service" id="academy_review_video.repository"/>
</service>
```

This example demonstrates how dependency injection is applied in a real-world Shopware plugin and enables:

- **Loose Coupling**: The service doesn't create repositories, they're injected.
- **Testability**: Easy to mock repositories for unit testing.
- **Maintainability**: Clear dependencies make the code easier to understand and modify.
- **Reusability**: The service can be used anywhere in the application.

## Example: Using Services in Controllers

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

namespace Swag\ExamplePlugin\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Attribute\Route;
use Shopware\Core\Framework\Context;
use Shopware\Core\Framework\Routing\ApiRouteScope;
use Shopware\Core\PlatformRequest;
use Swag\ExamplePlugin\Service\ProductUpdateService;

#[Route(defaults: [PlatformRequest::ATTRIBUTE_ROUTE_SCOPE => [ApiRouteScope::ID]])]
class ProductController extends AbstractController
{
    public function __construct(
        private readonly ProductUpdateService $productUpdateService
    ) {
    }

    #[Route(
        path: '/api/product/{productId}', 
        name: 'api.product.update', 
        methods: ['PATCH']
    )]
    public function updateProduct(Request $request, string $productId, Context $context): JsonResponse
    {
        $data = json_decode($request->getContent(), true) ?? [];
        if (empty($data)) {
            return new JsonResponse(['success' => false]);
        }
        
        $this->productUpdateService->updateProduct($productId, $data, $context); // Service call
        
        return new JsonResponse(['success' => true]);
    }
}
```

This simple example shows how a controller stays lightweight by delegating business logic to the dedicated service.

And the definition in the `routes.xml` file:

```xml
<?xml version="1.0" encoding="UTF-8" ?>
<routes xmlns="http://symfony.com/schema/routing"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/routing
        https://symfony.com/schema/routing/routing-1.0.xsd">

  <import resource="../../*Controller.php" type="attribute" /> <!-- Path to your controller file -->
</routes>
```

And the definition in the `services.xml` file:

```xml
<service id="AcademyReviewExtension\Controller\ProductController">
    <argument type="service" id="academy_product_update"/> <!-- The ID of the service -->
</service>
```

## Summary

In this learning unit, you have learned:

- How to create and register services and inject their dependencies via the constructor.
- Why business logic should live in services and not in controllers.
- How service decoration allows you to extend existing services without modifying their original implementation.
- How service tags are used to tell the framework how a service should be processed internally.
- Best practices for designing service that are reusable, testable, and performant.

Great! Now you have a solid understanding of how to create and use services in Shopware plugins, enabling you to structure your code more clearly, reuse business logic across your plugin, and build plugins that scale well as complexity grows.
