Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
5fe9e1f
docs: add plugin scaffolding troubleshooting guide
lasomethingsomething Aug 21, 2026
8c94554
Update scaffolding-and-generator-troubleshooting.md
lasomethingsomething Aug 21, 2026
4a7ef72
docs: document storefront JavaScript scaffolding runtime wiring
lasomethingsomething Aug 21, 2026
636784e
docs: distribute scaffolding troubleshooting by extension point
lasomethingsomething Aug 21, 2026
0592308
docs: reframe scaffolding findings as feature lifecycles
lasomethingsomething Aug 21, 2026
e1878f6
docs: align lifecycle pages with navigation conventions
lasomethingsomething Aug 21, 2026
ce83ba8
docs: add Storefront controller lifecycle and plugin feature wiring docs
lasomethingsomething Aug 21, 2026
496e6fa
docs: replace misspelled words with alternatives
Copilot Aug 21, 2026
d568a44
docs: fix controller lifecycle spellcheck text
lasomethingsomething Aug 21, 2026
9d20fff
Update storefront-controller-lifecycle.md
lasomethingsomething Aug 24, 2026
1de5d52
docs: consolidate plugin wiring guidance
lasomethingsomething Aug 24, 2026
89f76a4
Update add-store-api-route.md
lasomethingsomething Aug 24, 2026
6058b7d
Update add-custom-module.md
lasomethingsomething Aug 24, 2026
e924191
Update add-scheduled-task.md
lasomethingsomething Aug 24, 2026
bb015ed
docs: reframe generator wiring as troubleshooting reference
lasomethingsomething Aug 24, 2026
e51fa31
docs: clarify generated output is troubleshooting context
lasomethingsomething Aug 24, 2026
0a632e3
docs: keep generated wiring reference out of primary navigation
lasomethingsomething Aug 24, 2026
de1e006
docs: fold generator context into existing plugin guides
lasomethingsomething Aug 24, 2026
02815ee
docs: keep generator context on existing pages
lasomethingsomething Aug 24, 2026
ffcf185
docs: fix generator guide wording
lasomethingsomething Aug 24, 2026
9c59820
docs: fold generator context into feature guides
lasomethingsomething Aug 24, 2026
58187ca
Update add-store-api-route.md
lasomethingsomething Aug 24, 2026
1dffc27
Potential fix for pull request finding
lasomethingsomething Aug 25, 2026
d9a190a
Potential fix for pull request finding
lasomethingsomething Aug 25, 2026
1332a0d
fix/grammar-tables-and-order
Isengo1989 Aug 26, 2026
f341d65
adjust/sentence-for-clarity
Isengo1989 Aug 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@

Now your module's `index.js` will be executed.

::: info
For generated Administration modules, `main.js`, module registration, routes/components, snippets, and the Administration build are parts of the same feature. The generated entry point connects the module to the build; `Shopware.Module.register()` describes the module; its routes and components provide the UI; and snippets provide the text. This guide shows the pieces separately so their relationship is clear.

Check warning on line 44 in guides/plugins/plugins/administration/module-component-management/add-custom-module.md

View workflow job for this annotation

GitHub Actions / LanguageTool

[LanguageTool] guides/plugins/plugins/administration/module-component-management/add-custom-module.md#L44

Add a space between sentences. (SENTENCE_WHITESPACE) Suggestions: ` Module` Rule: https://community.languagetool.org/rule/show/SENTENCE_WHITESPACE?lang=en-US Category: TYPOGRAPHY
Raw output
guides/plugins/plugins/administration/module-component-management/add-custom-module.md:44:229: Add a space between sentences. (SENTENCE_WHITESPACE)
 Suggestions: ` Module`
 Rule: https://community.languagetool.org/rule/show/SENTENCE_WHITESPACE?lang=en-US
 Category: TYPOGRAPHY

Check warning on line 44 in guides/plugins/plugins/administration/module-component-management/add-custom-module.md

View workflow job for this annotation

GitHub Actions / LanguageTool

[LanguageTool] guides/plugins/plugins/administration/module-component-management/add-custom-module.md#L44

Use a comma before ‘so’ if it connects two independent clauses (unless they are closely connected and short). (COMMA_COMPOUND_SENTENCE_2[1]) Suggestions: `, so` URL: https://languagetool.org/insights/post/comma-before-and/ Rule: https://community.languagetool.org/rule/show/COMMA_COMPOUND_SENTENCE_2?lang=en-US&subId=1 Category: PUNCTUATION
Raw output
guides/plugins/plugins/administration/module-component-management/add-custom-module.md:44:381: Use a comma before ‘so’ if it connects two independent clauses (unless they are closely connected and short). (COMMA_COMPOUND_SENTENCE_2[1])
 Suggestions: `, so`
 URL: https://languagetool.org/insights/post/comma-before-and/ 
 Rule: https://community.languagetool.org/rule/show/COMMA_COMPOUND_SENTENCE_2?lang=en-US&subId=1
 Category: PUNCTUATION
:::

## Registering the module

Your `index.js` is still empty now, so let's get going to actually create a new module.
Expand Down Expand Up @@ -177,7 +181,7 @@

As mentioned above, Shopware 6 is looking for a `main.js` file in your plugin.
Its contents get minified into a new file named after your plugin and will be moved to the `public` directory of Shopware 6 root directory.
Given this plugin would be named "AdministrationNewModule", the bundled and minified javascript code for this example would be located under `<plugin root>/src/Resources/public/administration/js/administration-new-module.js`, once you run the command following command in your shopware root directory:
Given this plugin would be named "AdministrationNewModule", the bundled and minified JavaScript code for this example would be located under `<plugin root>/src/Resources/public/administration/js/administration-new-module.js`, once you run the following command in your shopware root directory:

<Tabs>
<Tab title="Template">
Expand All @@ -201,6 +205,10 @@
Your plugin has to be activated for this to work.
:::

::: info
The Administration build is the build boundary, not the whole feature lifecycle. A successful build means the generated assets are compiled; the module still depends on its entry-point import, registration, routes/components, and snippets being connected correctly at runtime.
:::

Make sure to also include that file when publishing your plugin!
A copy of this file will then be put into the directory `<shopware root>/public/bundles/administration/administrationnewmodule/administration/js/administration-new-module.js`.

Expand Down
48 changes: 25 additions & 23 deletions guides/plugins/plugins/creating-plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,40 +73,42 @@ This is the recommended starting point when you already know what your plugin ne

To generate a specific example instead of all of them, pass its option. Each option can also be answered interactively:

| Option | Generates |
| --- | --- |
| `--create-storefront-controller` | Example Storefront controller, its template, and a `routes.php` entry |
| `--create-store-api-route` | Example Store API route with abstract class and response class |
| `--create-event-subscriber` | Example event subscriber |
| `--create-command` | Example console command |
| `--create-scheduled-task` | Example scheduled task |
| `--create-admin-module` | Example Administration module with snippets |
| `--create-javascript-plugin` | Example Storefront JavaScript plugin |
| `--create-custom-fieldset` | Example custom fieldset (`custom-fields.xml`) |
| `--entities=Example,Foo` | Entity definition, entity, collection, and migration per entity (UpperCamelCase, comma-separated) |
| Option | Generates |
|----------------------------------|---------------------------------------------------------------------------------------------------|
| `--create-storefront-controller` | Example Storefront controller, its template, and a `routes.php` entry |
| `--create-store-api-route` | Example Store API route with abstract class and response class |
| `--create-event-subscriber` | Example event subscriber |
| `--create-command` | Example console command |
| `--create-scheduled-task` | Example scheduled task |
| `--create-admin-module` | Example Administration module with snippets |
| `--create-javascript-plugin` | Example Storefront JavaScript plugin |
| `--create-custom-fieldset` | Example custom fieldset (`custom-fields.xml`) |
| `--entities=Example,Foo` | Entity definition, entity, collection, and migration per entity (UpperCamelCase, comma-separated) |

Every generator that needs a service definition also appends it to the plugin's service configuration in `src/Resources/config`.

The generated files are placed below the plugin root. For example, the optional
components use paths such as:

| Component | Typical generated location |
| --- | --- |
| Console command | `src/Command/` and `src/Resources/config/services.php` |
| Scheduled task | `src/ScheduledTask/` and `src/Resources/config/services.php` |
| Event subscriber | `src/Subscriber/` and `src/Resources/config/services.php` |
| Storefront controller | `src/Storefront/Controller/`, `src/Resources/views/`, and `src/Resources/config/routes.php` |
| Administration module | `src/Resources/app/administration/` |
| Storefront JavaScript plugin | `src/Resources/app/storefront/src/` |
| Custom field set | `src/Resources/config/custom-fields.xml` |
| Component | Typical generated location |
|------------------------------|---------------------------------------------------------------------------------------------|
| Console command | `src/Command/` and `src/Resources/config/services.php` |
| Scheduled task | `src/ScheduledTask/` and `src/Resources/config/services.php` |
| Event subscriber | `src/Subscriber/` and `src/Resources/config/services.php` |
| Storefront controller | `src/Storefront/Controller/`, `src/Resources/views/`, and `src/Resources/config/routes.php` |
| Administration module | `src/Resources/app/administration/` |
| Storefront JavaScript plugin | `src/Resources/app/storefront/src/` |
| Custom field set | `src/Resources/config/custom-fields.xml` |

Treat generated examples as starting points. Selecting an option can create several
related files and service definitions; deleting only one file later can leave broken
references or an invalid service configuration. If you are unsure whether you need
an option, use `--no-scaffold` and add the feature from its focused guide instead.
references or an invalid service configuration. If you are unsure whether you need an
option, use `--no-scaffold` and add the feature from its focused guide instead.

If generated output does not behave as expected, use the existing feature guide to understand the generated pieces in context. The generator connects source files with registration, discovery, and build/runtime wiring; a generated file being present does not by itself mean that Shopware can use it.

::: info
Generated files are tied to the Shopware version you run the command on. When your plugin supports several Shopware versions, treat the output as a starting point and verify it against the version you target.
Generated files are tied to the Shopware version you run the command on. When your plugin supports several Shopware versions, treat the output as an example for that version and compare it with the focused guide when adapting it.
:::

Make sure to adjust the namespace in the generated files as per your needs.
Expand Down
10 changes: 10 additions & 0 deletions guides/plugins/plugins/framework/store-api/add-store-api-route.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ abstract class AbstractExampleRoute

Now we can create a new class `ExampleRoute` which uses our previously created `AbstractExampleRoute`.

::: info
A generated Store API route is normally more than the route class itself. The class defines the endpoint and response, the service definition registers it with the dependency injection container, and `routes.php` imports it for discovery. These pieces form one feature; the generator creates them together so you do not normally need to assemble the wiring by hand.
:::

```php
// <plugin root>/src/Core/Content/Example/SalesChannel/ExampleRoute.php
<?php declare(strict_types=1);
Expand Down Expand Up @@ -111,6 +115,8 @@ return static function (ContainerConfigurator $configurator): void {
};
```

The service definition is what connects the route class to the dependency injection container. If generated code already added this definition, treat it as part of the route rather than as an unrelated boilerplate.

### Route response

After we have created our route, we need to create the mentioned `ExampleRouteResponse`. This class should extend from `Shopware\Core\System\SalesChannel\StoreApiResponse`, consequently inheriting a property `$object` of type `Shopware\Core\Framework\DataAbstractionLayer\Search\EntitySearchResult`. The `StoreApiResponse` parent constructor takes accepts one argument `$object` in order to set the value for the `$object` property (currently we provide this parameter our `ExampleRoute`). Finally, we add a method `getExamples` in which we return our entity collection that we got from the object.
Expand Down Expand Up @@ -142,6 +148,8 @@ class ExampleRouteResponse extends StoreApiResponse

The last thing we need to do now is to tell Shopware how to look for new routes in our plugin. This is done with a `routes.php` file at `<plugin root>/src/Resources/config/` location. Take a look at the official [Symfony documentation](https://symfony.com/doc/current/routing.html) about routes and how they are registered.

The route import is the discovery boundary: a route class can be present and correctly registered as a service without becoming a Store API endpoint until Shopware imports its route attributes.

```php
// <plugin root>/src/Resources/config/routes.php
<?php declare(strict_types=1);
Expand All @@ -162,6 +170,8 @@ To check if your route was registered correctly, you can use the [Symfony route
$ ./bin/console debug:router store-api.example.search
```

The route debugger tells you whether routing discovered the endpoint; it does not by itself prove that the route's service can be constructed or that a request will succeed. Keep discovery, authentication, and endpoint behavior as separate concerns when interpreting generated output.

## Add a route to the OpenAPI schema

To add the route to the Stoplight page, a JSON file is needed in a specific [format](https://swagger.io/specification/#paths-object). It contains information about the paths, methods, parameters, and more. You must place the JSON file in `<plugin root>/src/Resources/Schema/StoreApi/` so the shopware internal OpenApi3Generator can find it (for Admin API endpoints, use `AdminApi`).
Expand Down
2 changes: 2 additions & 0 deletions guides/plugins/plugins/plugin-base-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ Most steps above can be generated instead of written by hand:
* `bin/console plugin:create` scaffolds the plugin; see the [Creating Plugins guide](creating-plugins.md)
* The [Shopware 6 Toolbox plugin](../../development/tooling/shopware-toolbox.md) generates plugins, subscribers, scheduled tasks, migrations, and Administration modules from PHPStorm, using file templates you can adapt to your own conventions

When a generator creates an optional feature, the files it produces are usually parts of one piece of framework wiring. For example, a generated controller can depend on both route configuration and service registration, while a scheduled task has separate task and handler roles. The focused guides explain those pieces in context; you do not normally need to reproduce the wiring manually.

## Upgrade readiness

Design plugins so that:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ A `ScheduledTask` and its respective `ScheduledTaskHandler` are registered in a

Here's an example `services.php` containing a new `ScheduledTask` as well as a new `ScheduledTaskHandler`:

::: info
A scheduled task has two related runtime roles. The task definition describes when work is due, while the handler processes the message that Shopware dispatches. Generators create both sides and their service wiring together; seeing the generated PHP class in the filesystem does not mean the whole feature has been connected.
:::

```php
// <plugin root>/src/Resources/config/services.php
<?php declare(strict_types=1);
Expand Down Expand Up @@ -54,6 +58,8 @@ Note the tags required for both the task and its respective handler, `shopware.s

The `services.php` file references both the task and its handler from `Service/ScheduledTask`. This directory name is a convention — you can use a different path as long as the namespace matches.

The distinction between the two classes is important when working with generated output: the task is registered with `shopware.scheduled.task`, while the handler is the Messenger consumer for that task. Registration of the task explains why it can be persisted and scheduled; the handler is what ultimately executes `run()`.

Here's an example `ScheduledTask`:

```php
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ A controller is also just a service which can be registered via the service cont
Furthermore, we have to define our `Route` with `defaults` and `_routeScope` via attributes, it is used to define which domain a route is part of and **needs to be set for every route**.
In our case the scope is `storefront`.

::: info
The controller class is only one part of the feature. The route attributes describe the HTTP endpoint, while the service and route configuration shown below connect that class to Shopware's container and routing system. Generators create these pieces together; this guide explains their roles so you can recognize how the generated files fit together.
:::

::: info
Prior to Shopware 6.4.11.0 the `_routeScope` was configured by a dedicated annotation: `@RouteScope`.
This way of defining the route scope is deprecated for the 6.5 major version.
Expand Down Expand Up @@ -138,6 +142,8 @@ class ExampleController extends StorefrontController

Next, we need to register our controller in the DI-container and make it public.

The generated service definition is the container half of the same feature: it tells Symfony how to construct the controller. If an existing `services.php` already contains other services, generated output belongs alongside those definitions rather than replacing them.

::: code-group

```php [PLUGIN_ROOT/src/Resources/config/services.php]
Expand Down Expand Up @@ -167,6 +173,8 @@ Once we've registered our new controller, we have to tell Shopware how we want i
This is done with a `routes.php` file at `<plugin root>/src/Resources/config/` location.
Take a look at the official [Symfony documentation](https://symfony.com/doc/current/routing.html) about routes and how they are registered.

The route import is the discovery half of the feature: it tells Shopware which controller files to inspect for route attributes. A controller can therefore exist and be a valid service while still not exposing a URL until its route is imported.

::: code-group

```php [PLUGIN_ROOT/src/Resources/config/routes.php]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,9 @@ Well, and then we check if this sum is bigger or equal the total size of your we

Next you have to tell Shopware that your plugin should be loaded and executed. Therefore you have to register your plugin in the PluginManager.

Shopware is automatically looking for a `main.js` file in a directory `<plugin root>/src/Resources/app/storefront/src`, which then will be loaded automatically. Consider this to be your main storefront JavaScript entrypoint.
Shopware is automatically looking for a `main.js` file in a directory `<plugin root>/src/Resources/app/storefront/src`, which then will be loaded automatically. Consider this to be your main storefront JavaScript entry point.

The entry point, plugin class, and optional DOM selector form one runtime chain. A generator creates these pieces together: `main.js` imports and registers the class, the selector connects it to rendered markup when one is used, and the Storefront build produces the asset the browser loads. Keeping that relationship in mind makes generated output easier to adapt without treating each file as an independent feature.

Create a `main.js` file inside your `<plugin root>/src/Resources/app/storefront/src` folder and get the PluginManager from the global window object. Then register your own plugin:

Expand All @@ -75,7 +77,7 @@ Create a `main.js` file inside your `<plugin root>/src/Resources/app/storefront/
// Import all necessary Storefront plugins
import ExamplePlugin from './example-plugin/example-plugin.plugin';

// Register your plugin via the existing PluginManager
// Register your custom Storefront plugin
const PluginManager = window.PluginManager;
PluginManager.register('ExamplePlugin', ExamplePlugin);
```
Expand All @@ -91,7 +93,7 @@ You can also bind your plugin to a DOM element by providing a css selector:
// Import all necessary Storefront plugins
import ExamplePlugin from './example-plugin/example-plugin.plugin';

// Register your plugin via the existing PluginManager
// Register your custom Storefront plugin
const PluginManager = window.PluginManager;
PluginManager.register('ExamplePlugin', ExamplePlugin, '[data-example-plugin]');
```
Expand All @@ -106,7 +108,7 @@ The import path can remain the same as the synchronous import.
```javascript
// <plugin root>/src/Resources/app/storefront/src/main.js

// Register your plugin via the existing PluginManager using a dynamic import
// Register your own Storefront plugin using a dynamic import
const PluginManager = window.PluginManager;
PluginManager.register('ExamplePlugin', () => import('./example-plugin/example-plugin.plugin'), '[data-example-plugin]');
```
Expand Down
Loading