---
name: shopware6
description: >
Shopware 6 development conventions and patterns for client projects.
Trigger whenever the user works on Shopware 6 code, plugins, or configuration.
version: "3.0.0"
sw_versions: "6.6.x, 6.7.x"
references:
- url: https://developer.shopware.com/docs/guides/plugins/plugins/
topic: Official plugin development docs
- url: https://symfony.com/doc/7.4/service_container.html
topic: Symfony 7.4 service container, autowiring, attributes
related_skills:
- ddev/SKILL.md
---
# Shopware 6 — Development Skill
This skill provides conventions, architectural decisions, and gotchas for
Shopware 6 client projects. For standard "how to" questions, refer to the
official docs above — they are comprehensive. This file focuses on what the
docs don't tell you.
---
## 1. Code Conventions
Apply these to all plugin PHP code:
- **No `else` / `elseif`** — guard clauses and early returns only
- **`instanceof`** checks over loose truthy checks
- **Typed class constants**: `public const string NAME = 'value';` (PHP 8.3+)
- **Constructor promotion**: `private readonly FooService $foo`
- **No inline `style=`** in Twig — use Bootstrap utility classes (`d-none`, `d-flex`, etc.) in storefront, Shopware grid system in admin
- **No `!important`** in CSS/SCSS — use more specific selectors instead
---
## 2. Plugin Structure
### Location and installation
All plugins live in `custom/static-plugins/` and **must be Composer-installed**.
```json
"repositories": [
{ "type": "path", "url": "custom/static-plugins/*", "options": { "symlink": true } }
]
```
Then: `composer require myvendor/my-plugin:*`
**Do not use `custom/plugins/`.** Plugins there require a database to resolve,
which breaks `shopware-cli project ci` and any build pipeline without DB access.
### Composer version constraint
Target the minor versions you support:
```json
"require": {
"shopware/core": "~6.6.0 || ~6.7.0"
}
```
### Theme plugins
Implement `ThemeInterface` on the base class. Configuration goes in
`Resources/theme.json`, not PHP. Refer to:
https://developer.shopware.com/docs/guides/plugins/themes/
---
## 3. Dependency Injection
Reference: https://symfony.com/doc/7.4/service_container/autowiring.html
**Use autowiring for everything.** Symfony 7.4 eliminates the need for explicit
service XML in virtually all cases.
### Standard services.xml (the only one you need)
```xml
../../Resources
../../MyPlugin.php
```
### Entity repositories autowire by name
Shopware autowires repositories by parameter naming convention —
`$Repository`:
```php
public function __construct(
private readonly EntityRepository $productRepository, // product.repository
private readonly EntityRepository $orderLineItemRepository, // order_line_item.repository
private readonly EntityRepository $myCustomEntityRepository, // my_custom_entity.repository
) {}
```
This works for all entity repositories including custom ones. No `#[Autowire]`
attribute or XML needed.
### Decorating services
Use PHP attributes, not XML:
```php
use Symfony\Component\DependencyInjection\Attribute\AsDecorator;
use Symfony\Component\DependencyInjection\Attribute\AutowireDecorated;
#[AsDecorator(decorates: ProductDetailRoute::class)]
class MyProductDetailRoute extends AbstractProductDetailRoute
{
public function __construct(
#[AutowireDecorated]
private readonly AbstractProductDetailRoute $inner,
) {}
}
```
### Event listeners via attribute
```php
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
#[AsEventListener(event: ProductPageLoadedEvent::class)]
class MyListener
{
public function __invoke(ProductPageLoadedEvent $event): void { /* ... */ }
}
```
Or implement `EventSubscriberInterface` — `autoconfigure` handles the tag.
---
## 4. Key Architectural Decisions
### Entity extension vs custom fields
Reference: https://developer.shopware.com/docs/guides/plugins/plugins/framework/data-handling/add-complex-data-to-existing-entities.html
**Prefer entity extensions.** Custom fields are translatable by default, which
adds overhead (extra joins on `_translation` tables) and causes unwanted effects
for attributes that don't need translation (flags, external IDs, numeric values).
Use custom fields only when:
- The data must be merchant-editable via admin UI (custom field sets)
- The data genuinely needs per-language values
Use entity extensions for everything else:
- Relational data (associations to other entities)
- Data that needs Criteria filtering or sorting
- Non-translatable attributes (flags, external IDs, technical config)
- Performance-sensitive fields queried frequently
### Store API route vs storefront controller
| Situation | Use |
|-----------|-----|
| Headless / PWA consumers | Store API route (`_routeScope: store-api`) |
| Server-rendered Twig pages | Storefront controller (`_routeScope: storefront`) |
| Admin panel integrations | Admin API route (`_routeScope: api`) |
| AJAX from storefront JS | Store API route (preferred) or storefront with `XmlHttpRequest: true` |
### Message queue vs synchronous
Dispatch to the message queue for anything that:
- Takes > 1 second (API calls, bulk writes, email sending)
- Can be retried independently
- Doesn't need to block the user response
---
## 5. Twig Template Overrides
### Block tracking convention
When overriding a Shopware Twig block, add a comment with the original block's
content hash and the Shopware version. This makes it easy to detect when an
upstream change breaks your override:
```twig
{# shopware-block: a1b2c3d4e5f6@v6.7.5 #}
{% sw_extends '@Storefront/storefront/page/product-detail/index.html.twig' %}
{% block page_product_detail_buy %}
{{ parent() }}
...
{% endblock %}
```
Generate the hash: `sha256sum` of the original block content, first 12 chars.
### CSRF tokens
Shopware 6.7+ removed CSRF tokens entirely. Do not add them to forms.
### Product data in templates
When working with product data via Store API includes or in Twig, always include
both `name` and `translated` fields. The `translated` object contains the
resolved translation; the root `name` may be the parent product's value
(variant inheritance).
---
## 6. Migrations
Reference: https://developer.shopware.com/docs/guides/plugins/plugins/plugin-fundamentals/database-migrations.html
Key conventions beyond the docs:
- IDs: `BINARY(16)` — Shopware UUIDs
- Timestamps: `DATETIME(3)` — millisecond precision
- Always include `created_at DATETIME(3) NOT NULL` and `updated_at DATETIME(3) NULL`
- Use `CREATE TABLE IF NOT EXISTS` / `ADD COLUMN IF NOT EXISTS` for idempotent migrations
- Foreign key naming: `fk..`
- Never put business logic in migrations — only schema changes and essential data seeding
---
## 7. Search & Indexing Gotchas
- Default tokeniser splits on **whitespace only** — hyphens and special chars are NOT split. Partial product number matching (e.g. `AB-123` → `AB`) requires decorating `ProductSearchBuilderInterface`
- `product_search_keyword` is NOT updated on incremental writes — a full reindex is needed
- Never run `es:index` during business hours on catalogs > 50k products
- `throw_exception: false` in OpenSearch config is recommended for production — it falls back to DAL on ES failure
- Always set `Criteria::setLimit()` — unbounded queries on large catalogs will time out
---
## 8. Configuration Patterns
### Plugin settings via config.xml
Reference: https://developer.shopware.com/docs/guides/plugins/plugins/plugin-fundamentals/add-plugin-configuration.html
Read in PHP:
```php
// Key format: PluginTechnicalName.config.fieldName
$value = $this->systemConfigService->get('MyPlugin.config.apiKey');
$value = $this->systemConfigService->get('MyPlugin.config.apiKey', $salesChannelId);
```
### Production shopware.yaml essentials
```yaml
shopware:
deployment:
runtime_extension_management: false # prevent plugin installs from admin
```
### Redis cache + sessions (framework.yaml)
```yaml
framework:
cache:
app: cache.adapter.redis
default_redis_provider: '%env(REDIS_URL)%'
session:
handler_id: '%env(REDIS_URL)%'
```
### S3 filesystem
For S3/object storage configuration, refer to:
https://developer.shopware.com/docs/guides/hosting/infrastructure/filesystem.html
---
## 9. Common Gotchas
| Trap | What happens | Fix |
|------|-------------|-----|
| `Context::createDefaultContext()` in storefront | Bypasses sales channel rules, prices, permissions | Use injected `SalesChannelContext` |
| `BLUE_GREEN_DEPLOYMENT=1` without two DB users | Silent failures on deploy | Set to `0` or configure two DB users |
| Editing compiled CSS directly | Overwritten on next `theme:compile` | Edit SCSS source files |
| No `--time-limit` on messenger workers | Memory leaks, eventual OOM kill | Always set `--time-limit=300` + supervisor restart |
| Running `es:index` during peak traffic | Locks tables, degrades performance | Schedule during off-hours |
| `customFields` for relational data | No joins, no Criteria filtering, poor performance | Use entity extension |
| Assuming `product.name` is translated | Returns parent value on variants | Use `product.translated.name` |
---
## 10. CLI Quick Reference
```bash
# Build (CI/CD only — never run locally, it removes source files)
# shopware-cli project ci # use in Dockerfile / pipeline only
# Plugin lifecycle
bin/console plugin:refresh
bin/console plugin:install --activate MyPlugin
# Cache & assets
bin/console cache:clear
bin/console theme:compile
bin/console assets:install
# Database
bin/console database:migrate --all
bin/console database:migrate --all MyPlugin
# Queue
bin/console messenger:consume async low_priority --time-limit=300 --memory-limit=512M
bin/console scheduled-task:run
# Search
bin/console es:index
bin/console dal:refresh:index
# Debug
bin/console debug:container | grep product
bin/console debug:event-dispatcher
bin/console debug:router | grep my-plugin
```