---
title: 'Storefront Controller: Routes, Services, and Template | Shopware Community Hub'
description: >-
  Connect a storefront controller to Shopware with route imports, service
  registration, plugin configuration, Twig output, and storefront verification.
canonical_url: >-
  https://hub.shopware.com/learn/unit/storefront-controller-routes-services-and-template
---

# Storefront Controller: Routes, Services, and Template

<LearningObjectives>

- Define routes with PHP 8 attributes and import them in `routes.xml`.
- Register the controller in `services.xml` with DI arguments.
- Render a Twig template and open the controller output in a storefront modal.

</LearningObjectives>

# Connect the Controller to the Storefront

You already have `ImageController` from the previous unit. But Shopware still needs to know how to find it, create it, and render its output.

In this learning unit, you connect the controller to the storefront step by step. You will follow six steps:

1. Check the route information in `ImageController`.
2. Import the route attributes with `routes.xml`.
3. Register `ImageController` in `services.xml`.
4. Add plugin configuration for the API provider.
5. Add the Twig template for the controller response.
6. Add a storefront link, clear the cache, and verify the route.

If you cloned the prepared example plugin, these files may already exist. In that case, use this learning unit to understand and verify them. If you generated the plugin yourself, create or update the files as shown.

## Target Result

By the end of this learning unit, you will have:

- A route named `frontend.image.show`.
- A controller that is registered as a service.
- A Twig template that receives and displays `imageUrl`.
- A storefront link that opens the controller output in a modal.

This completes the controller setup that started in the previous learning unit.

## Step 1: Check the Route Information in the Controller

The `ImageController` already contains route attributes. Before you import the controller routes with `routes.xml`, first check which route information is already written in the controller.

These attributes describe which route the controller provides and how Shopware should treat it.

The class-level route attribute defines route defaults for the controller:

```php
#[Route(defaults: [PlatformRequest::ATTRIBUTE_ROUTE_SCOPE => [StorefrontRouteScope::ID], 'XmlHttpRequest' => true])]
```

This does two important things:

- `StorefrontRouteScope::ID` marks the route as a storefront route.
- `XmlHttpRequest: true` makes the route suitable for AJAX modal loading.

Shopware has different route scopes:

- `storefront` → `StorefrontRouteScope::ID`
- `store-api` → `StoreApiRouteScope::ID`
- `api` → `ApiRouteScope::ID`
- `administration` → `AdministrationRouteScope::ID`

<Callout title="Route scopes" type="info">

Always use scope **constants**, not raw strings. They are easier to read and less error-prone.

</Callout>

The method-level route attribute defines the concrete route:

```php
#[Route(
        path: '/image',
        name: 'frontend.image.show',
        methods: ['GET']
    )]
```

This means:

- The route path is `/image`.
- The route name is `frontend.image.show`.
- The route only accepts `GET` requests.

The route name is important because you will use it later in Twig with `path('frontend.image.show')`.

## Step 2: Import the Controller Routes With `routes.xml`

The route attributes are written in the PHP controller class. Shopware still needs a route import so it knows where to scan for these attributes.

Create or verify this file: `[plugin_root]/src/Resources/config/routes.xml`

Use this content:

```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="../../Storefront/Controller/**/*Controller.php" type="attribute" />

</routes>
```

The important part is:

```xml
<import resource="../../Storefront/Controller/**/*Controller.php" type="attribute" />
```

This tells Shopware to look for controller classes in the `Storefront/Controller` directory and read their PHP route attributes.

## Step 3: Register the Controller in `services.xml`

The controller also needs to be registered as a service. Create or verify this file: `[plugin_root]/src/Resources/config/services.xml`.

Use this content:

```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="ShopwareAcademy\StorefrontController\Storefront\Controller\ImageController" public="true">
      <argument type="service" id="http_client"/>
      <argument type="service" id="Shopware\Core\System\SystemConfig\SystemConfigService"/>
      <call method="setContainer">
        <argument type="service" id="service_container"/>
      </call>
    </service>

  </services>
</container>
```

This service definition does three things:

- It registers `ImageController` as a service.
- It injects the Symfony HTTP client.
- It injects Shopware's SystemConfigService.

The `setContainer` call is needed because `ImageController` extends `StorefrontController` and uses storefront controller helper methods such as `renderStorefront()`.

## Step 4: Add Plugin Configuration

The controller reads `apiProvider` and optional `apiAccessKey` from plugin configuration by using `SystemConfigService`. Create or verify this file: `[plugin_root]/src/Resources/config/config.xml`.

Use this content:

```xml
<?xml version="1.0" encoding="UTF-8"?>

<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="https://raw.githubusercontent.com/shopware/shopware/trunk/src/Core/System/SystemConfig/Schema/config.xsd">
    <card>
        <title>API settings</title>
        <input-field type="single-select">
            <name>apiProvider</name>
            <label>Are you a cat or dog person?</label>
            <options>
                <option>
                    <id>thecatapi</id>
                    <name>I am a cat person</name>
                </option>
                <option>
                    <id>thedogapi</id>
                    <name>I am a dog person</name>
                </option>
            </options>
            <defaultValue>thedogapi</defaultValue>
        </input-field>

        <input-field type="password">
            <name>apiAccessKey</name>
            <label>Access Key</label>
            <helpText>Optional for single images. https://developers.thecatapi.com</helpText>
        </input-field>
    </card>    
</config>
```

After the plugin is installed and activated, configure the provider under **Extensions → My extensions → AcademyStorefrontController → Config**.

The controller uses these values here:

```php
$apiAccessKey = $this->systemConfigService->get('AcademyStorefrontController.config.apiAccessKey', $context->getSalesChannelId());
$apiProvider = $this->systemConfigService->get('AcademyStorefrontController.config.apiProvider', $context->getSalesChannelId());
```

## Step 5: Add the Controller Template

The controller returns a storefront Twig template with an `imageUrl` variable. The template renders that image. Create or verify this file: `[plugin_root]/src/Resources/views/storefront/page/image.html.twig`.

Use this content:

```twig
{% block base_content %}
    <h1>Some random image</h1>

    <img src="{{ imageUrl }}" alt="Random image" style="max-width: 70%;">

{% endblock %}
```

The value `imageUrl` comes from the controller:

```php
return $this->renderStorefront('@AcademyStorefrontController/storefront/page/image.html.twig', [
    'imageUrl' => $imageUrl
]);
```

So, in this case, you pass this variable from the controller (PHP, Backend) to the template (Twig, Storefront).

## Step 6: Display and Verify the Output in the Storefront

Now the controller has a route and a template. The last step is to add the storefront link that calls the route.

For this example, extend the buy-widget template by **mirroring** the core path in your plugin:

```text
src/Resources/views/storefront/component/buy-widget/buy-widget.html.twig
```

Use this content:

```twig
{% sw_extends '@Storefront/storefront/component/buy-widget/buy-widget.html.twig' %}

{% block buy_widget_ordernumber_container %}
    {{ parent() }}
    <a href="{{ path('frontend.image.show') }}" class="open-image-modal" data-ajax-modal="true"
       data-url="{{ path('frontend.image.show') }}">View Image</a>
{% endblock %}
```

The important parts are:

- `sw_extends` extends the original Shopware template.
- `parent()` keeps the existing buy-widget content.
- `path('frontend.image.show')` generates the URL to your controller route.
- `data-ajax-modal="true"` opens the route in a modal.

<Callout title="Twig Extends" type="info" small>

Use `sw_extends` (not `extends`) so Shopware resolves plugin/theme inheritance correctly.

</Callout>

![Link with modal plugin assigned](assets/images/view-image-storefront-controller.jpg)

When the link is clicked, Shopware opens the route response in a modal:

![Modal window with cat image](assets/images/cat-api.jpg)

![Modal window with dog image](assets/images/dog-api.jpg)

### Clear the Cache and Verify the Route

After changing route, service, or Twig files, clear the cache:

```bash
bin/console cache:clear
```

Then verify that Shopware knows the route:

```bash
bin/console debug:router frontend.image.show
```

The route must appear before the storefront link will work.

<Callout title="Expected Result" type="success">

**Prove it works:**

1. `bin/console debug:router frontend.image.show` lists the route.
2. On a product detail page, **View Image** appears in the buy widget.
3. Clicking it opens a modal with a cat or dog image based on the plugin configuration.

If something fails, clear the cache again, confirm the plugin is active, and check that the service ID in `services.xml` matches your controller class.

</Callout>

<ArticleQuestionnaire>
  <ArticleQuestionnaireQuestion>What value must routes.xml use to import attribute-based routes?</ArticleQuestionnaireQuestion>
  <ArticleQuestionnaireAnswer>type="annotation"</ArticleQuestionnaireAnswer>
  <ArticleQuestionnaireAnswer correct>type="attribute"</ArticleQuestionnaireAnswer>
  <ArticleQuestionnaireAnswer>type="yaml"</ArticleQuestionnaireAnswer>
</ArticleQuestionnaire>

## Summary

In this learning unit, you learned:

- How route attributes define the controller path, route name, HTTP method, and storefront scope.
- How `routes.xml` imports attribute-based routes from your controller directory.
- How `services.xml` registers the controller and injects its dependencies.
- How plugin configuration provides the API provider used by the controller.
- How a Twig template renders the controller response.
- How a storefront link can open that response in an AJAX modal.

You now have a complete storefront controller setup: the controller class exists, Shopware can discover its route, the service container can create it, and the storefront can display its output.

With this foundation, you can build custom backend features and expose them through the storefront in a controlled way.
