Release notes Shopware 6.7.14.0
Abstract
Shopware 6.7.14.0 delivers 522 closed issues, strengthening the checkout and order experience while addressing storefront, CMS, customer-management, sales-channel, media, and compatibility issues. Notable additions include document v2 and automation enhancements, new APIs for headless storefronts, improved translation tooling, and broader operational metrics.
System requirements
- tested on PHP 8.2, 8.4 and 8.5
- tested on MySQL 8, MySQL 9.7, MariaDB 11 and MariaDB 12
Improvements
Features
New "Automation" administration menu entry
Rule Builder and Flow Builder are now reachable from a dedicated top-level "Automation" menu entry. The existing "Settings > Automation" entries are unchanged.
New app script hook cookie-group-collect
Apps can now modify or remove cookie consent groups and entries with an app script under Resources/scripts/cookie-group-collect/. The hook exposes the collected cookieGroups collection and the current sales channel context, and provides the services.repository, services.store and services.config script services. Scripts run after cookies from plugins and app manifests were collected, so an app can, for example, declare its cookies in the manifest and remove them when the related payment method is not active in the current sales channel — with full backwards compatibility, since older Shopware versions simply ignore scripts for unknown hooks.
API
Added experimental Store API snippet endpoint
Added new experimental Store API route GET /store-api/snippet (not part of the backwards compatibility promise yet, planned to be stable with v6.8.0), which returns the fully resolved snippets (translations) for the current sales channel context as a list of sets, each carrying a flat key-value map ({"account.loginTitle": "Log in"}). By default the list contains one set for the language of the sw-language-id header; the language fallback chain is merged server-side, so values are never null. The optional prefixes query parameter limits the result to namespace prefixes (e.g. ?prefixes=checkout,account, at most 50 distinct prefixes per request), the optional languageIds query parameter fetches multiple sales channel languages in one request. Responses carry an ETag header and support If-None-Match revalidation, so headless frontends (e.g. Composable Frontends) can bake translations at build time and revalidate them cheaply at runtime.
Number range admin action endpoints now require ACL privileges
Three admin action endpoints that previously only required authentication now enforce ACL privileges. Requests with tokens lacking the privilege receive a 403 with FRAMEWORK__MISSING_PRIVILEGE_ERROR:
GET /api/_action/number-range/reserve/{type}/{salesChannelId}requiresnumber_range:read. Withoutpreview=1this endpoint permanently advances the number range state, so it was possible for any authenticated backend account to consume invoice, order, delivery-note and credit-note numbers and create gaps in the sequence.GET /api/_action/number-range/{numberRangeId}/preview-patternand the deprecatedGET /api/_action/number-range/preview-pattern/{type}requirenumber_range:read.
Administration users are not affected: number_range:read is already part of the "Products viewer" and "Number ranges viewer" permissions, it is now also part of the "Orders editor" and "Customers creator" permissions — these are the roles whose users reserve document, order and customer numbers — and a migration grants it to existing roles that already hold one of those permissions. Integrations and API clients with manually assigned privilege lists must add number_range:read to their ACL role.
Added new shop setting endpoint
Added new Store API route GET /store-api/shop-settings, which exposes the UI- and validation-relevant, non-sensitive subset of the system configuration (grouped into general, loginRegistration, cart, listing and newsletter) resolved for the current sales channel, so headless frontends (e.g. Composable Frontends) can render the shop consistently with the administration settings.
Cache information includes registered indexers
GET /api/_action/cache_info now returns an indexers map containing the registered normal-refresh indexers and their optional child updaters. Administration clients can use this metadata when offering cache-index refresh controls; post-update-only indexers are excluded.
Order recalculation and conversion endpoints now require ACL privileges
Thirteen admin checkout endpoints that previously only required authentication now enforce ACL privileges. Requests with tokens lacking the privilege receive a 403 with FRAMEWORK__MISSING_PRIVILEGE_ERROR:
POST /api/_action/order/{orderId}/recalculate,/product/{productId},/creditItem,/lineItem,/promotion-item,/toggleAutomaticPromotionsand/applyAutomaticPromotions, plusPATCH /api/_proxy/modify-shipping-costs,/api/_proxy/disable-automatic-promotionsand/api/_proxy/enable-automatic-promotions, requireorder:update.POST /api/_action/order-address/{orderAddressId}/customer-address/{customerAddressId}andPOST /api/_action/order/{orderId}/order-addressrequireorder_address:update.POST /api/_action/order/{orderId}/convert-to-cart/requiresorder:read.
Administration users are not affected: order:update and order_address:update are part of the "Orders editor" permission that already gates every one of these actions in the order detail page, order:read is part of "Orders viewer", and the order creator role depends on both. Integrations and API clients with manually assigned privilege lists must add the respective privilege to their ACL role.
User uniqueness validation endpoints now require user read access
The POST /api/_action/user/check-email-unique and POST /api/_action/user/check-username-unique endpoints now require the existing user:read privilege. Integrations and API clients that call these endpoints must add this privilege to their ACL role.
Message queue admin endpoints now require ACL privileges
Three admin endpoints that previously only required authentication now enforce ACL privileges. Requests with tokens lacking the privilege receive a 403 with FRAMEWORK__MISSING_PRIVILEGE_ERROR:
POST /api/_action/message-queue/consumeandPOST /api/_action/scheduled-task/runrequire the newsystem:queue:processprivilege.GET /api/_action/scheduled-task/min-run-intervalrequires the existingscheduled_task:readprivilege.
Administration users are not affected: both privileges are granted to every authenticated Administration user at runtime, because these endpoints back the admin worker that processes the message queue in every admin session. Integrations and API clients calling these endpoints must have the respective privilege added to their ACL role — the runtime defaults apply to Administration users only. External workers should keep using the bin/console messenger:consume and scheduled-task:run CLI commands, which are unaffected.
Plugin filesystem metadata is read-only through the Admin API
The plugin.path and plugin.managedByComposer fields can no longer be created or changed through generic Admin API writes. Plugin discovery and extension management continue to maintain these values automatically. Integrations must not write plugin filesystem metadata directly.
SEO admin action endpoints now require ACL privileges
Four admin action endpoints that previously only required authentication now enforce ACL privileges. Requests with tokens lacking the privilege receive a 403 with FRAMEWORK__MISSING_PRIVILEGE_ERROR:
PATCH /api/_action/seo-url/canonicalrequiresseo_url:update.POST /api/_action/seo-url/create-custom-urlrequiresseo_url:create.POST /api/_action/seo-url-template/contextandGET /api/_action/seo-url-template/default/{routeName}requireseo_url_template:read.
Administration users are not affected: seo_url:update is now part of the "Products editor", "Categories editor", and "Landing pages editor" permissions in the role editor — these are the roles whose users write canonical URLs when saving — and a migration grants it to existing roles that already hold one of those permissions. The template privileges are already part of the system configuration permission that the SEO settings page requires. Integrations and API clients with manually assigned privilege lists must add the respective privilege to their ACL role.
Increment and queue-stats admin endpoints now require ACL privileges
Seven admin endpoints that previously only required authentication now enforce ACL privileges. Requests with tokens lacking the privilege receive a 403 with FRAMEWORK__MISSING_PRIVILEGE_ERROR:
POST|GET /api/_action/increment/{pool},POST /api/_action/decrement/{pool},POST /api/_action/reset-increment/{pool}, andDELETE /api/_action/delete-increment/{pool}require the newincrement:manageprivilege.GET /api/_info/queue.jsonandGET /api/_info/message-stats.jsonrequire the existingmessage_queue_stats:readprivilege.
Administration users are not affected: increment:manage is granted to every authenticated Administration user at runtime (the endpoints back module-usage tracking, which runs in every admin session), and message_queue_stats:read already is such a default privilege. Integrations and API clients calling these endpoints must have the respective privilege added to their ACL role — the runtime defaults apply to Administration users only.
Media action routes now enforce ACL privileges
The Admin API media action routes now enforce their corresponding ACL privileges. Clients must have media:create to upload new media, upload from a URL, or create an external media link; media:update to upload content to existing media or rename media; media_thumbnail:create or media_thumbnail:delete to add or remove external thumbnails; and media:read to use the media filename lookup route.
The V2 upload and upload-from-URL routes already required media:create through their media repository write, and the filename lookup route already required media:read through its repository query. The external-link, legacy upload and rename, and external-thumbnail add and delete routes now enforce permissions that their system-scoped DAL writes did not previously require. Integrations and users that call those routes must update their ACL role.
Template rendering endpoints require update privileges
The POST /api/_action/product-export/preview and POST /api/_action/product-export/validate endpoints now require the product_export:update ACL privilege. The POST /api/_action/mail-template/simulate endpoint now requires mail_template:update. Admin API integrations and users that use these endpoints must be granted the respective existing privilege.
Admin action endpoints now require ACL privileges
Four admin action endpoints that previously only required authentication now enforce ACL privileges. Requests with tokens lacking the privilege receive a 403 with FRAMEWORK__MISSING_PRIVILEGE_ERROR:
POST /api/_action/sso/invite-userrequires the existinguser:createprivilege.POST /api/app-system/shop-id/changerequires the newsystem:app:changeprivilege.POST /api/_action/trigger-event/{eventName}requires the newflow:dispatchprivilege.POST /api/_action/extension-sdk/run-actionrequiresapp.allor the app-specificapp.{appName}privilege.
The new privileges are part of the existing "Plugin maintain" (system:app:change) and "Flow editor" (flow:dispatch) permissions in the Administration role editor, and a migration grants them to roles that already hold those permissions — existing admin users keep access without manual changes. Integrations calling these endpoints must have the respective privilege added to their ACL role.
sw-expect-packages is rejected on endpoints that do not require authentication
The sw-expect-packages header is no longer evaluated on API endpoints that do not require authentication, because the failure messages disclose the installed versions of Shopware and its dependencies. Sending it to such an endpoint now returns 417 with the new error code FRAMEWORK__API_EXPECTATION_NOT_SUPPORTED instead of evaluating the constraint. Affected endpoints include GET /api/_info/health-check, POST /api/oauth/token, GET /api/app-system/shop/verify, and the POST /api/_action/user/user-recovery routes.
Send the header with an authenticated Admin API request, where the behaviour is unchanged: a violated constraint still returns 417 with FRAMEWORK__API_EXPECTATION_FAILED and the installed version. Clients that set the header as a default on their HTTP client must remove it from unauthenticated calls — most importantly from the token request, which otherwise fails before the token is issued. Requests that do not send the header are unaffected.
Store API schema documents cart totals as a separate CartPrice component
The Store API OpenAPI schema previously documented item prices and cart totals as one CalculatedPrice component, which marked the cart-level fields netPrice, positionPrice, rawTotal, and taxStatus as required on item prices such as product.calculatedPrice and lineItem.price. The schema now contains a dedicated CartPrice component used for cart.price and order.price, while CalculatedPrice only documents the fields item prices actually contain. The taxStatus enum also includes the previously missing gross value. API responses are unchanged; only clients generated from the schema are affected and now match the actual payloads.
Store API no longer offers shipping methods without a usable price
onlyAvailable=1 no longer returns active shipping methods whose prices cannot resolve a cost: an empty matrix, or rows that all lack currency values. One usable row is enough. Requests without the flag are unchanged.
Sales channel language list validation compares against the incoming default language
Assigning a new languageId to a sales channel and removing the previous default language from its languages list in the same write is now accepted. It previously failed with SYSTEM__CANNOT_DELETE_DEFAULT_LANGUAGE_ID, and the two steps had to be sent as separate requests.
Removing the language that the same write assigns as the new default is now rejected with that error code instead of being applied. Such a write previously succeeded and left the sales channel with a default language that was missing from its language list.
Core
Document V1/V2 file compatibility
Document V1 and Document V2 can now open and download each other's files, including legacy files in the V2 archive download.
An active shipping method must keep at least one usable price
Removing, reassigning or emptying the last usable shipping_method_price, or activating a method without one, now returns a 400 (active_shipping_method_without_price). Creating a method without prices still works. To remove a matrix, deactivate the method in an earlier request first.
Replace a matrix in a single request, so the method is never priceless in between:
[
{ "key": "delete-prices", "entity": "shipping_method_price", "action": "delete", "payload": [{ "id": "…" }] },
{ "key": "write-prices", "entity": "shipping_method_price", "action": "upsert", "payload": [{ "id": "…", "shippingMethodId": "…", "calculation": 1, "quantityStart": 0, "currencyPrice": [{ "currencyId": "…", "net": 0, "gross": 0, "linked": false }] }] }
]E-invoice line positions state the correct price base quantity
ZUGFeRD invoices previously wrote the product's purchase unit (product.purchaseUnit, the package content used for base price display) as the item price base quantity (BT-149). Recipients validating against EN16931 saw PEPPOL-EN16931-R120 violations for every line whose product has a purchase unit other than 1, and Peppol access points may have rejected such invoices. Line positions now always state a base quantity of 1, matching the per-unit item net price. Additionally, the item net price (BT-146) is now written with 4 decimals instead of 2, so the line amount calculation stays within the rule's rounding tolerance for higher quantities.
New shop settings route classes
- Added
Shopware\Core\System\SystemConfig\SalesChannel\AbstractShopSettingsRouteas a decoratable extension point. - Added
Shopware\Core\System\SystemConfig\SalesChannel\ShopSettingsRoute. - Added
Shopware\Core\System\SystemConfig\SalesChannel\ShopSettingsRouteResponseand the structsShopSettings,ShopGeneralSettings,ShopLoginRegistrationSettings,ShopCartSettings,ShopListingSettings,ShopNewsletterSettingsin the same namespace.
Plugins can customize version cleanup
Plugins can subscribe to the new CleanupVersionEvent to protect version records from scheduled cleanup. The event provides the cleanup cutoff through getCleanupTime(), allowing plugins to apply retention rules consistently with the scheduled cleanup task.
Force thumbnail regeneration and deletion via --force
The media:generate-thumbnails command now accepts a --force (-f) option that regenerates thumbnails for all configured sizes even when a thumbnail already exists — for example after changing the thumbnail quality or sizes of a media folder. The option works with both synchronous and --async execution. Previously, existing thumbnails were always skipped.
Note that regenerated thumbnails are written to the same physical path, so their URLs do not change: browsers and CDNs may keep serving the previously cached files until their cache expires or is invalidated manually.
The media:delete-local-thumbnails command also accepts a new --force (-f) option that deletes all thumbnail records and files even when remote thumbnails are disabled — previously the command refused to run in that case. The command now removes the whole physical thumbnail directory, which also cleans up orphaned files without a database record (e.g. left behind after their media was uploaded again), and prints the number of deleted files. Note that the storefront is missing thumbnails until they are regenerated; prefer media:generate-thumbnails --force to replace them without such a gap.
A new --orphans (-o) option deletes only those orphaned files. Referenced thumbnails and their records are kept, so this cleanup is safe in every setup and works regardless of the remote thumbnail configuration. The two options cannot be combined.
ThumbnailService::updateThumbnails() accepts a matching optional $force argument; classes overriding this method must add the parameter with Shopware 6.8 (see UPGRADE-6.8.md).
New Criteria::resetFields() and Criteria::resetExcludedFields()
Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria::resetFields() drops an allowlist added via addFields(), Criteria::resetExcludedFields() drops a denylist added via excludeFields(). Both selections are mutually exclusive, so the new methods also allow switching a criteria from one to the other.
They affect the database read, unlike includes and excludes, which only shape the API response.
Failed payment mail flow
Shopware now ships a default Flow Builder flow, flow template, and mail template for state_enter.order_transaction.state.failed.
GARAN commercial guarantee label and EU legal guarantee notice
- Products get a new
guaranteeMonthsfield for an optional commercial durability guarantee beyond the statutory two years (must be empty, or a half-year value greater than 24 months). - New Store API route
GET /store-api/product/{productId}/garan-labelrenders the EU-harmonised GARAN label as SVG (full and nested variants) for products with a manufacturer and complete label data. - New Twig filters
sw_garan_label,sw_garan_label_nested,sw_garan_label_data_uri,sw_garan_label_nested_uri,sw_garan_label_durationrender the label as inline SVG or as a base64data:URI — the latter for mail clients, which strip inline<svg>markup. - Separately, new Store API route
GET /store-api/legal-guarantee-noticerenders the EU-harmonised statutory legal guarantee notice, translated into all 24 official EU languages, toggleable via the newcore.cart.showLegalGuaranteeNoticesystem config, and exposed viasw_legal_guarantee_notice/sw_legal_guarantee_notice_linkTwig filters. - Both labels/notices are wired into the storefront (checkout confirmation, buy-widget, cart line items) and into the Administration product detail page (new guarantee form).
- The
order_confirmation_mailtemplate is updated to include both the GARAN label (as a data URI) and the legal guarantee notice link. This update only applies to shops whose order confirmation mail template is still the unmodified system default — i.e. new installations, and existing shops that never edited that template. Merchants who have customized their order confirmation mail template must add{{ nestedItem.productId|sw_garan_label_nested_uri(context) }}and{{ context.languageId|sw_legal_guarantee_notice_link }}to their template manually if they want these notices.
Category and SEO URL indexing cause fewer database deadlocks
Concurrent category writes through the Sync API frequently hit InnoDB deadlocks in the SEO URL and child-count updaters (1213 Deadlock found when trying to get lock). Three changes reduce the lock footprint while keeping the default REPEATABLE READ isolation: the seo_url table is written with INSERT ... ON DUPLICATE KEY UPDATE instead of REPLACE INTO, so a colliding row is updated in place — keeping its id and created_at — instead of being deleted and re-inserted with the wide next-key locks that REPLACE takes on both unique indexes; rows whose is_deleted flag already holds the target value are no longer rewritten; and child counts are computed with a non-locking read followed by a primary-key-only update instead of a self-joined update. No configuration or database changes are required.
Deprecated XML configuration
Loading Symfony configuration from XML files is deprecated for Shopware bundles, plugins, and the project-level config/ directory of an installation, and will stop working with Shopware 6.8, because Symfony 8 removes XML configuration support entirely. This covers service definitions (Resources/config/services.xml, services_test.xml, config/services.xml), route definitions (Resources/config/routes.xml, routes_<env>.xml, routes_overwrite.xml, and any XML file below a routes/ config directory), and package configuration (packages/**/*.xml). Symfony already logs a runtime deprecation for every loaded XML file since Symfony 7.4; Shopware now additionally reports which file — and for bundles and plugins, which bundle — is affected. Shopware-specific XML formats such as config.xml, custom-fields.xml, or app manifests are not affected.
Plugin authors: migrate your services.xml to services.php using Symfony's ContainerConfigurator and your routes.xml to routes.php using the RoutingConfigurator; package configuration can move to YAML or PHP. PHP configuration has been fully supported by the plugin system for years, service ids and wiring stay identical, and both formats can coexist during the transition. YAML definitions remain supported. See the migration example in UPGRADE-6.8.md.
Document templates use the DocumentV2 VAT display condition behind the 6.8 feature flag
The document templates now use the shipping-based intraCommunityDelivery condition for displaying VAT information when the v6.8.0.0 feature flag is active, matching the DocumentV2 document generation path. With the feature flag disabled, the existing billing-address and configured delivery-country behavior remains unchanged. Extensions and themes that render or assert the legacy document templates should test their output with the feature flag enabled before upgrading to Shopware 6.8.
Line item rule conditions only evaluate product line items
Product specific line item rule conditions (manufacturer, category, tags, properties, dimensions, stock, list price, and similar) now skip non-product goods such as custom product options. Those line items carry no product data, so evaluating them could produce false matches.
EntitySearchResult retains its entity name in v6.8.0
EntitySearchResult keeps the $entity constructor argument, property, and getEntity() method in v6.8.0. Removing the constructor argument would not have provided a forward-compatible migration path: extensions could not construct a result today that also works after the major update. The required call-site changes were therefore disproportionate to the benefit.
The property becomes readonly; use the constructor rather than the deprecated setEntity() method to provide the entity name. For a non-empty collection, the constructor asserts that the supplied entity name matches the collection's entity name.
Media path cache busting is configurable
The new shopware.cdn.path_cache_buster setting defaults to true, preserving timestamped media paths. Set it to false to keep paths stable for future media uploads and replacements while retaining ?ts= query-string cache busting. Configure the CDN to include query strings in its cache key. Existing media paths are not migrated.
product-export:generate --force now regenerates scheduler-managed exports
--force promises to ignore the cache and force generation, but for scheduler-managed exports it was a no-op. This aligns the flag with its documented behavior.
Elasticsearch index updates schedule a reindex when analysis settings change
When updating an Elasticsearch/OpenSearch mapping references an analyzer/normalizer that the live index's analysis settings do not define (for example after an update introduced a new analyzer), putMapping fails with analyzer [...] has not been configured in mappings. Analysis settings are fixed at index creation and cannot be added to a live index, so this is now handled like the other unrecoverable mapping errors: the affected entity is scheduled for a reindex into a freshly created index, which rebuilds it with the current analysis settings instead of leaving the outdated mapping in place.
Built-in translation system configurable via shopware.translation
The built-in translation system's configuration (previously only editable by decorating AbstractTranslationConfigLoader) can now be overridden through the standard Symfony configuration in config/packages. Add a shopware.translation section to override individual options; any option left unset falls back to the shipped defaults in translation.yaml:
# config/packages/translation.yaml
shopware:
translation:
repository_url: 'https://example.com/translations'
metadata_url: 'https://example.com/crowdin-metadata.json'
community_translations_url: 'https://translate.shopware.com'
documentation_url_snippet_key: 'sw-settings-language.addModal.docsUrl'
completeness_threshold: 90
plugins:
- 'MyPlugin'
excluded_locales:
- 'de-DE'
- 'en-GB'
pseudo_locales:
- 'ach-UG'
plugin_mapping:
- plugin: 'MyPlugin'
name: 'MySnippetName'
languages:
- name: 'Deutsch'
locale: 'de-DE'List options (plugins, excluded_locales, pseudo_locales, plugin_mapping, languages) replace the shipped default entirely rather than merging; provide the full list you want. Setting a list to [] clears the shipped default. Decorating AbstractTranslationConfigLoader continues to work; a decorator that fully replaces load() bypasses these config overrides.
Product export body media URLs are RFC 3986 encoded
Product export body templates now receive RFC 3986-encoded MediaEntity::url and MediaThumbnailEntity::url values from their data context. This applies to media URLs such as product.cover.media.url and product.media.*.media.url in built-in and custom body templates, so feeds such as Google Merchant Center exports can use them without manually encoding their paths.
Other URL-valued strings, including custom fields, are unchanged. Custom body templates that render those values can explicitly encode them with the sw_encode_url Twig filter.
Community translations auto-update can be configured per language
The translation.update scheduled task (UpdateTranslationsTaskHandler) refreshes the community translations of installed languages. It can now be controlled per language via the new translationAutoUpdate flag.
- The
languageentity gains a booleantranslationAutoUpdatefield (API-aware, enabled by default). A migration adds thelanguage.translation_auto_updatecolumn automatically, defaulting to on, so existing languages keep being updated as before. - Only linked languages are considered: a language flagged for auto-update whose translation is not installed, or whose locale is not part of the translation set (for example built-in or custom languages), is ignored and never triggers a request.
- The flag can be toggled per language on the Administration language detail page ("Snippet updates" card), letting shops opt individual languages out of automatic updates.
Product migrations no longer fail on MySQL 8.4 with non-standard foreign keys
Migration DDL now retries once with restrict_fk_on_non_standard_key relaxed when MySQL 8.4 rejects a statement through MySQL bug #118151. The MigrationStep DDL helpers do this automatically; raw DDL statements in extension migrations should go through MigrationStep::executeDdlStatement(). The method is @internal only because it will be removed once MySQL fixes the bug; it is safe to call in the meantime.
Polyfill packages are installed as declared dependencies
The shopware/core and shopware/platform package manifests no longer replace Symfony polyfill packages or paragonie/random_compat. Composer now installs the polyfills required by the resolved dependency graph instead of treating them as supplied by Shopware. Extension projects that depend on these packages continue to work; their production dependency tree can gain the required polyfill packages. Projects that guarantee the required native PHP functionality can add relevant packages to their own root replace section to avoid installing them and reduce their vendor directory size; they must not replace symfony/polyfill-mbstring, which Core requires for mb_ltrim() on PHP 8.2 and 8.3.
OpenAPI generation uses swagger-php 6.4
Shopware now requires zircote/swagger-php 6.4 for OpenAPI 3.2 generation. Most extensions are not affected: OpenAPI annotations and attributes continue to be read by swagger-php 6, and extensions that only define OpenApi\Annotations or OpenApi\Attributes metadata usually do not need code changes.
Extensions or development tools that call swagger-php programmatically should check for removed v4/v5 APIs such as OpenApi\Generator::scan() and OpenApi\Util::finder(). The migration is usually straightforward because the instance API OpenApi\Generator::generate() is available in swagger-php 4, 5, and 6. See UPGRADE-6.7.md for concrete examples.
Locale-aware sorting for product property group options
Shopware\Core\Content\Product\AbstractPropertyGroupSorter::sort() is deprecated and will be removed with Shopware 6.8. Use the new sortUsingLocaleCode() method instead, which sorts property group options using locale-aware (ICU) collation.
MCP server no longer requires the MCP_SERVER feature flag
The MCP server is now always enabled. The MCP_SERVER feature flag has been removed, so the /api/_mcp and /store-api/_mcp endpoints are available without setting any flag. The MCP classes stay marked @experimental until 6.8.0, so the API may still change before then.
MCP entity tools validate association read privileges
The experimental MCP tools shopware-entity-read, shopware-entity-search, and shopware-entity-aggregate now validate the supplied criteria with the same association ACL checks as the Admin API. Loading, filtering, sorting, or aggregating over an association now requires the <association-entity>:read privilege in addition to the top-level <entity>:read privilege — for example, reading order with the orderCustomer association requires both order:read and order_customer:read. Integrations that relied on the missing check must add the association read privileges to their ACL role; requests without such criteria are unaffected.
MCP tools can be enabled per session through toolsets
The experimental MCP server now advertises only its default meta-tools until a client enables additional toolsets for the current MCP session. Clients can call shopware-toolsets-list to inspect available toolsets and shopware-toolset-enable to enable one. Enabling a toolset emits notifications/tools/list_changed so clients that support MCP list-change notifications can refresh tools/list.
Tool execution is still bounded by the configured MCP allowlist. Enabling a toolset only changes which allowlisted tools are advertised for that session.
MCP clients are notified when app capabilities change
The experimental MCP server now queues notifications/*/list_changed messages for active sessions when an app's MCP tools, resources, or prompts change (install, update, activation, deactivation, deletion), so clients can refresh their discovered capabilities.
MCP tools can be discovered on demand
A fresh MCP session advertises only the three server-owned discovery meta-tools in tools/list: shopware-tool-search, shopware-toolsets-list, and shopware-toolset-enable. Every other tool is deferred and reachable in two ways: shopware-tool-search returns relevant tool definitions inline for a free-text query, and shopware-toolset-enable enables a whole toolset for the session (see the toolset section above). Tool visibility is derived solely from the tool's group — the discovery group is the always-advertised surface — so a tool opts into the default surface via #[McpToolGroup('discovery')], not a per-tool flag.
The per-integration MCP allowlist remains the call-time security boundary. shopware-tool-search only returns tools that are already allowed for the current integration, and tools outside the allowlist remain uncallable.
MCP list responses apply allowlists before pagination
MCP tools/list, resources/list, and prompts/list responses now apply the current integration allowlist before protocol pagination is calculated. Clients using nextCursor receive full pages of allowed capabilities instead of pages that may be partially or completely empty because hidden capabilities were filtered after paging.
MCP tools expose a group for operator-facing selection
MCP tool metadata now includes a group value in the /_action/mcp/tools and /_action/mcp/capabilities responses. The Administration MCP allowlist UI and bin/console debug:mcp use this value to group tools for operators without changing tool names or call behaviour.
Tools without an explicit group derive one from the longest name prefix they share with another tool at a hyphen boundary, so tools such as swag-my-plugin-orders and swag-my-plugin-products use swag-my-plugin without being combined with tools from another swag-* extension. Existing core, plugin, and app tools continue to work.
SEO URLs for headless sales channels
Headless (API type) sales channels can now generate SEO URLs and be used for product export feeds. Products, categories and landing pages generate SEO URLs for headless channels via dedicated store-api routes (store-api.product.detail, store-api.category.detail, store-api.landing-page.detail).
- The
seo_url_templateentity gained anis_headlessflag that discriminates the storefront (frontend.*) and headless (store-api.*) route families. Storefront channels resolve the non-headless templates, headless channels the headless ones — the two inheritance chains are kept separate. - Three default
store-api.*templates are seeded (one per entity), mirroring the relative template of their storefront counterpart. Headless channels inherit these defaults just like storefront channels inherit thefrontend.*defaults; the resolved path is prefixed with the host of the external storefront domain. A per-channel template may be an absolute URL (https://…), which is then used as-is. - The
sales_channel_domainentity gained anis_external_storefrontflag (defaultfalse). For headless sales channels, SEO URLs are only generated for domains flagged as external storefront, and the resolved relative path is prefixed with that domain's URL (one SEO URL per matching domain). The flag can be set per domain in the Administration when editing a headless sales channel's domains. - Product export feeds now accept both Storefront and Headless sales channels.
New BC-change attributes for planned API changes
Shopware previously used @deprecated tag:vX.Y.Z - reason:* PHPDoc annotations to document planned backwards-compatibility-affecting changes that are not actual deprecations, such as return type narrowing, new optional parameters, or classes becoming internal or final. In plugin projects these annotations surfaced as Call to deprecated method errors in static analysis, although there is no replacement API to migrate to.
Such changes are now documented with dedicated PHP attributes under Shopware\Core\Framework\Deprecation\BCChange, for example #[ReturnTypeNarrowing], #[NewOptionalParameter], or #[BecomesFinal]. For your project this means:
- Static analysis no longer reports deprecation errors for core methods that merely carry a BC-planning note, so these errors disappear from your pipelines without configuration changes.
- A
@deprecatedannotation on core code is now always an actual deprecation: the functionality will be removed or replaced, and you should migrate as described in the annotation. - When a core symbol you use carries a BC-change attribute, the attribute tells you whether your project can be affected: attributes implementing
CallSiteCompatibilityChangeconcern code calling the symbol (for example a parameter type being narrowed, or a parameter you pass as a named argument being renamed), attributes implementingExtenderCompatibilityChangeconcern classes in your project that extend or override the symbol (for example a return type being narrowed or a class becoming final). Each attribute states the version in which the change happens and the new declaration, so you can prepare ahead of the next major. - If your code does not use the annotated symbol in the affected way, there is nothing to do.
All existing reason:* BC-planning annotations in the core have been migrated to these attributes; the remaining @deprecated annotations are actual deprecations.
Product export scheduling decoupled from the cache timestamp
Cron-driven product export generation no longer derives the next run from generatedAt, which also anchors the cache validity of the generated feed file. A new nextGenerationAt field on the product_export entity is set when the first export chunk starts, and the scheduler prefers it over the legacy generatedAt + interval calculation. This keeps the schedule anchored to the export start time without making storefront requests treat in-flight exports as stale. The database column is added automatically by a migration; exports generated before the update fall back to the previous generatedAt-based scheduling until their next run. No action is required.
debug:mcp lists Store API capabilities
bin/console debug:mcp was wired to the Admin MCP server only, so no capability of the Store API MCP server (/store-api/_mcp) appeared in its output, and looking one up by name reported "No capability found". Store API tools registered with shopware.store_api_mcp.tool were therefore invisible to the standard debugging command.
The command now inspects both MCP servers and groups the output per server, with every section heading naming the server it belongs to (Store API: Tools (17)). A new --scope option limits it to one of them:
bin/console debug:mcp # both servers
bin/console debug:mcp --scope=store-api # /store-api/_mcp only
bin/console debug:mcp --scope=api # /api/_mcp only
bin/console debug:mcp shopware-store-api-contextLooking up a capability by name resolves across both servers and the detail view names the owning server. --integration still applies to the Admin server only, because integration allowlists are not evaluated for Store API requests; a note points this out when both servers are listed.
Whole-phrase product-search matches rank higher
Elasticsearch product search now adds an explicit phrase-proximity boost for multi-word searches, weighted above single-word matches. A product whose field contains the full search phrase in order now ranks above one that only contains the individual words scattered around. The same documents still match — this only re-ranks — but _score values and borderline orderings shift, which can affect a configured core.search.minScore. The per-match-type boosts are configurable via elasticsearch.search.boost.*.
Product export pagination changed to keyset; getTotal() deprecated
The product export now paginates products by an autoIncrement keyset cursor instead of LIMIT/OFFSET, removing the getTotalCount() timeout on large catalogs.
ProductExportResult::getTotal()and its$totalconstructor argument are deprecated; the export no longer computes a grand total. UsehasNextBatch()andgetOffset()instead.- The read buffer size is now configurable via
shopware.product_export.read_buffer_size(default 200). Raise it to reduce per-batch overhead, lower it if a batch hits the worker memory limit.
SalesChannelRepositoryIterator supports autoIncrement keyset pagination
SalesChannelRepositoryIterator now seeks by an autoIncrement keyset instead of OFFSET when the entity has an autoIncrement field and the criteria defines no sorting (mirroring RepositoryIterator); a criteria with its own sorting keeps offset iteration. SalesChannelRepository::getDefinition() was added for parity with EntityRepository.
Cross-selling by dynamic product group excludes the whole variant family
A cross-selling that uses a dynamic product group no longer returns the product it is displayed on. For variants, the complete variant family is excluded — the currently viewed variant, its parent, and all sibling variants — because variant grouping and main variant resolution would otherwise resolve a sibling back to the viewed product. Previously only the product the cross-selling is assigned to was excluded, which had no effect on variants that inherit their cross-sellings from the parent.
Cross-sellings with a manual product assignment are unchanged. Extensions that need the old result can adjust the criteria in ProductCrossSellingStreamCriteriaEvent.
Changing an SEO URL template regenerates the existing SEO URLs
Writing the template field of a seo_url_template row now regenerates the SEO URLs of the affected route automatically, instead of leaving them on the old template until the SEO indexer is run manually. The new SeoUrlTemplateChangeSubscriber queues SeoUrlTemplateIndexingMessage on the async transport, and the handler walks the route's entities in batches of 250, chaining one message per batch.
This affects every write path, not just Settings > Shop > SEO:
- Both storefront routes (registered in the
SeoUrlRouteRegistry) and headless store-api routes (taggedshopware.entity.seo_url.route) are covered. - Writes that do not change the stored
templatevalue do not queue anything: update commands request a DAL change set, so an idempotent Sync API push of an identical template stays inert. Inserts with an empty ornulltemplate are skipped as well. - Extensions and deployment scripts that write
seo_url_templaterows on every install or update will therefore queue a full regeneration pass for the affected route each time. Guard such writes with a value comparison if that is not intended.
Newsletter route methods keep the StoreApiResponse return type
subscribeWithResponse(), confirmWithResponse() and unsubscribeWithResponse() keep StoreApiResponse as their return type in the abstract newsletter routes, in the next major as well. This withdraws the return type change announced with 6.7.9.0.
A decorator that puts its logic in these methods answers with the response containing the status field, and keeps working on 6.8, where the deprecated subscribe(), confirm() and unsubscribe() are removed. Those are still required in 6.7 and have to answer with NoContentResponse.
Admin search falls back to the database for entities without an Elasticsearch admin indexer
With Elasticsearch for the Administration enabled, POST /api/_admin/es-search silently returned no results for entities that have no admin search indexer, because those entities were dropped from the request. They are now searched over the DAL instead, so an entity registered in the Administration search (searchTypeService.upsertType() or a module defaultSearchConfiguration) is findable without shipping an indexer. Registering an AbstractAdminIndexer for the entity is still the faster option.
Administration
Admin Worker loads correctly when the Administration is hosted under a base path
The Vite asset-path-plugin now also prefixes the literal Worker/SharedWorker script URLs that Vite bakes into the Administration bundle with window.__sw__.assetPath, the same prefix that is already applied to every other admin asset through the assetsURL() helper. Previously these worker URLs were emitted as absolute paths against the domain root (e.g. new SharedWorker("/bundles/administration/administration/assets/adminWorker-*.js")) and never passed through assetsURL(), so on any Shopware install served from a subdirectory / base path — including Shopware-hosted staging environments mounted under a subpath such as /staging — the Admin Worker's SharedWorker failed to load (404 against the domain root instead of the base path). This broke admin-worker-driven message-queue and scheduled-task processing in the Administration. Operators running the Administration under a base path now get a working Admin Worker; no action is required.
Vue Single File Components for Administration extensions
Administration components can now be written as Vue Single File Components. .vue files were previously not supported at all: components were registered through the component factory with Twig templates, and there was no way to author or override one as an SFC.
A build-time transform lowers these files onto the Composition-API extension system before Vue compiles them, so an override receives the base component's state instead of replacing its implementation. It runs in every extension build — no configuration needed in your plugin.
Filename decides both identity and role: sw-my-component.vue (or sw-my-component/index.vue) declares the base component sw-my-component, and sw-my-component.override.vue overrides it. Two markers declare the extension surface, and both are mandatory in their mode — pass an empty object when there is nothing to declare:
<!-- sw-my-component.vue -->
<script setup lang="ts">
import { ref } from 'vue';
const count = ref(0);
const internalValue = ref('private');
swDefinePublic({ count }); // `count` is overrideable, `internalValue` is not
</script><!-- sw-my-component.override.vue -->
<script setup lang="ts">
import { computed } from 'vue';
const previousState = useSwPreviousState();
const count = computed(() => previousState.count.value * 2);
swDefineOverride({ count });
</script>A few things to know before you start:
- Every
.vuefile in your own source needs a<script setup>block. A plain<script>(Options API) SFC or a template-only SFC is rejected at build time, because the markers that make a component extendable only exist in<script setup>. Options-API components continue to work as before through the component factory; this applies to.vuefiles only. SFCs insidenode_modulesare exempt — a dependency's components are not yours to make extendable. - An override only works when the base component is itself native-setup.
sw-my-component.override.vueextends a base declared withswDefinePublic(); it cannot override a component registered through the component factory (Twig / Options API). The base must be authored as a native-setup SFC foruseSwPreviousState()and the override to resolve. - The API is experimental until 6.8.0. It is marked
@experimental stableVersion:v6.8.0and may still change.
Rejections surface in your editor as well as in the build: the valid-shopware-setup ESLint rule runs the same validation, and build/vue-setup-transform/templates/custom-plugin-workspace contains ESLint and TypeScript templates to copy into custom/ for local plugin development. Full authoring reference: src/Administration/Resources/app/administration/technical-docs/03-extensibility/07-native-setup-authoring.md.
SFC migration codemod now emits native setup components
The codemod:sfc-migration developer tool has been rewritten to output the native setup SFC format (<script setup> with swDefinePublic) instead of the previous createExtendableSetup() form, which the build toolchain no longer accepts. The default remains a read-only preview; --write creates validated Vue drafts only. Replacing an eligible legacy entry point requires the separate explicit --replace-originals option, and Twig templates are retained.
What changed for users of the tool: every generated file must pass the build transform and Vue's compiler before it is written; components that convert only partially receive a .vue draft with TODO(sfc-migration) comments while their original index.js + .html.twig stay in place and keep working; components using mixins or Component.extend() are skipped and reported instead of receiving an Options API <script> fallback, which the build now rejects. See src/Administration/Resources/app/administration/scripts/codemods/sfc-migration/README.md.
System config forms show validation errors for the selected sales channel scope
Extension and app configuration forms, and any settings page built on sw-system-config, now display server-side validation errors on the field that caused them, for the sales channel selected in the scope switcher. Previously these errors were returned by POST /api/_action/system-config/batch but did not reach the field: for sales-channel-specific scopes they were stored under a key that did not match the lookup, the lookup only ever used the initially passed scope, and most field types were never passed the error at all. If your config.xml uses required, minLength, maxLength, min, max or dataType, merchants now see why a save was rejected on the scope they have selected. No API changes; the error resolver and error store remain @private. (shopware/shopware#18741)
System config component exposes the selected sales channel scope
The sw-system-config component now exposes which sales channel is selected in its scope switcher. The value is seeded from the salesChannelId prop and afterwards follows the switcher; later changes to the prop are ignored. It is null while the global scope is selected.
The value is available on two surfaces, because they reach different consumers:
- Slot props: the
card-element,beforeElementsandafterElementsslots receive an additionalcurrentSalesChannelIdprop, andcard-element-last, which had no slot props, now provides it too. A template override that replaces one of these slots keeps its own copy of thev-bindexpression and will not see the new prop until it is re-synced against this version. - Injection: descendants of the component, in particular custom components rendered through a plugin's
config.xmlcomponent elements, can inject the value. Use the defaulted forms below, so your component stays warning-free and crash-free when it renders outside a system config form. With the default in place, a component rendered outside the form reads the same value as the global scope:
// Options API: the injected value is unwrapped, read it directly
inject: {
swSystemConfigCurrentSalesChannelId: { default: null },
},
// Composition API: you receive the ref itself, read `.value`
const salesChannelId = inject('swSystemConfigCurrentSalesChannelId', ref(null));The provided value is a read-only computed ref. Note that the form body is torn down and rebuilt while the configuration of a not yet visited sales channel loads, so embedded components must not assume instance continuity across a switch.
Existing slot usages keep working unchanged. Previously, following the switcher required traversing $parent into private component state or overriding sw-system-config itself, both of which break across Administration refactors. (shopware/shopware#18731)
Conditional visibility for app-registered tabs
sw.ui.tabs('<position>').addTabItem() now accepts an optional visible boolean, so an app can show or hide its own registered tab depending on the current context (for example the currently opened entity). When omitted, the tab is shown as before, so existing extensions are unaffected.
Re-calling addTabItem() for the same componentSectionId now updates the existing entry (label and visibility) instead of adding a duplicate, so an app can toggle a tab's visibility for the current context by re-registering it.
sw.ui.tabs('sw-order-detail').addTabItem({
label: 'my-plugin.tabTitle',
componentSectionId: 'my-plugin-tab',
visible: order.stateMachineState.technicalName === 'open',
});Administration caches shared user configuration and lookup data
Administration now reuses a generic cache layer for current-user configuration and frequently loaded lookup data such as the system currency, currencies, taxes, active languages, sales channel types, number range ids, and custom field sets. This reduces repeated Admin API requests when multiple Administration components need the same data.
Current-user configuration is cached per current user through userConfigService. Read individual keys from the shared cached _info/config-me response and write changes through the same service:
const userConfigService = Shopware.Service('userConfigService');
const response = await userConfigService.search(['my-plugin.config-key']);
const value = response?.data?.['my-plugin.config-key'];
await userConfigService.upsert({
'my-plugin.config-key': nextValue,
});Shared entity reads can be cached directly on repository reads by passing a stable cacheKey:
const criteria = new Shopware.Data.Criteria(1, 500);
criteria.addSorting(Shopware.Data.Criteria.sort('name', 'ASC', false));
const currencies = await Shopware.Service('repositoryFactory')
.create('currency')
.search(criteria, Shopware.Context.api, {
cacheKey: ['shared-data', 'currencies', Shopware.Context.api.languageId ?? 'default'],
ttl: 5 * 60 * 1000,
});If your plugin changes cached data and needs a fresh follow-up read, either invalidate the affected cache key prefix or force the next read to reload:
const cacheService = Shopware.Service('cacheService');
const taxRepository = Shopware.Service('repositoryFactory').create('tax');
cacheService.invalidateCaches({
// Invalidate only the cached tax entries.
cacheKey: ['shared-data', 'taxes'],
});
const freshTaxes = await taxRepository.search(criteria, Shopware.Context.api, {
cacheKey: ['shared-data', 'taxes', Shopware.Context.api.languageId ?? 'default'],
// true bypasses the cached result for this read and stores the fresh response again.
forceReload: true,
ttl: 5 * 60 * 1000,
});
cacheService.invalidateCaches({
// Custom field sets can be invalidated independently from taxes.
cacheKey: ['custom-field-sets', 'product'],
});Plugins can use the global Meteor snackbar
Administration plugins can now add and remove snackbars through Shopware.Service('snackbarService'). Use addSnackbar() with a Meteor snackbar configuration and removeSnackbar(id) to dismiss it. Composition API extensions can use the experimental useSnackbar() composable, which becomes stable with Shopware 6.8.
App action buttons in the Media Manager multiselect sidebar
Apps can now surface a custom action button when multiple media are selected in the Media Manager. Registering an action button via the Admin SDK with entity: 'media' and view: 'list' renders it in the multiselect sidebar's quick-actions list. The button is only shown when every selected media item matches the configured fileTypes (case-insensitive; omit fileTypes to always show it), and the callback receives the full list of selected media entities ({ id, url, fileName, mimeType, fileSize }). This complements the existing single-item button (view: 'item') and lets apps offer bulk operations — e.g. exporting or converting all selected files — without an extra API round-trip. No changes are required for existing single-item action buttons.
New media Quick info extension point and selected-item dataset
The media "Quick info" sidebar now exposes an extension point so apps and plugins can render their own content next to a selected media file. A new sw-extension-component-section with the position identifier sw-media-quickinfo-metadata is rendered directly below the metadata list, and the currently selected media entity is published as the sw-media-quickinfo__item dataset (Shopware.ExtensionAPI.publishData).
Extensions can register a component at the sw-media-quickinfo-metadata position and read the selected item through the data API. App (iframe) extensions must request the fields they need via selectors, for example:
const { data: media } = useDataset('sw-media-quickinfo__item', {
selectors: ['id', 'fileName', 'fileExtension', 'mimeType'],
});The dataset updates reactively as the user selects a different media file.
Storefront
Google reCAPTCHA failures no longer show an error page on non-AJAX forms
A failed Google reCAPTCHA on a non-AJAX form is now rendered as a form error instead of a 403 error page: a missing token asks the customer to retry (new CaptchaException::RECAPTCHA_TOKEN_REQUIRED_VIOLATION), other failures show a generic captcha error. Violations without a form field are flashed, field-bound ones keep rendering via formViolations. The bot-only honeypot still fails with 403.
Custom captchas should implement the new AbstractCaptcha::validate(Request $request, array $captchaConfig): ConstraintViolationList — an empty list means valid. The deprecated isValid()/getViolations() are removed in 6.8; until then the default validate() delegates to them, so a captcha extending AbstractCaptcha keeps working.
One case does change: a captcha extending a shipped captcha (BasicCaptcha, HoneypotCaptcha, GoogleReCaptchaV2, GoogleReCaptchaV3) and overriding only isValid()/getViolations() is no longer consulted, because those implement validate() themselves. Migrate it to validate() — an unmigrated check stops being applied without an error.
Theme CLI commands clean up unused theme directories
Each theme compilation writes its CSS/JS into a new seeded directory under public/theme/<hash>. Removing the now-unused previous directories was handled exclusively by the daily theme.delete_files scheduled task (Shopware\Storefront\Theme\ScheduledTask\DeleteThemeFilesTask). In environments where that task does not run reliably — e.g. bin/console theme:compile during a build/deploy step, or setups relying on the admin worker without an open Administration session — these directories accumulated without bound and could grow to many gigabytes.
bin/console theme:compile and bin/console theme:change now run the same cleanup once after compilation, deleting unused theme directories whose files are older than 24 hours (recent directories are kept so cached responses still referencing them keep working). Pass --no-cleanup to skip this step and preserve the previous behaviour.
The cleanup logic is now provided by the reusable Shopware\Storefront\Theme\UnusedThemeDirectoryDeleter service, which the commands and the scheduled task all use. The scheduled task remains unchanged as a fallback.
theme:create gains --full and granular scaffold flags
bin/console theme:create accepts new options to scaffold more than the default skeleton: --with-config generates src/Resources/config/config.xml, --with-snippets generates storefront snippet files (src/Resources/snippet/storefront.{de-DE,en-GB}.json), and --with-scss generates a starter SCSS 7-1 folder structure (abstracts/, base/, components/, layout/, pages/) referenced from base.scss. --full is shorthand for all three combined. Default theme:create output (without any of these flags) is unchanged. The generated composer.json also now sets a real package name (custom/<theme-name> instead of a hardcoded placeholder) and pins shopware/core.
PluginManager.override() now works for async plugins
Overriding a lazily loaded core storefront plugin no longer silently falls back to the core class. Three defects caused the override to be registered but never applied:
- A core plugin class that finished loading after the override was registered overwrote the override in the registry.
- An element kept the plugin instance it was first initialized with, even after the registered class had changed.
- Re-registering an async plugin with a plain (non-lazy) class left it flagged as async, so it was never initialized at all.
Overriding a plugin that is already initialized on the page now replaces its live instances instead of doing nothing. This replacement happens whenever the class registered under a plugin name differs from the class an existing instance was built with, so it is not limited to PluginManager.override() and it can happen during any later initialization pass, for example the one that runs after AJAX-loaded content.
Before an outdated instance is replaced, the PluginManager calls its destroy() method and resets its $emitter.
Be aware of the current limitation: the instance being replaced is the previously registered class, which is usually a core plugin, and most core plugins do not implement destroy() yet. Anything such an instance registered outside of itself — most importantly listeners added with addEventListener — therefore stays active, and both the replaced and the new instance react to the same event. The PluginManager logs a console.warn naming every plugin that is replaced without implementing destroy(). Registering your override before the storefront initializes its plugins, which is what a theme entry file does by default, avoids the replacement entirely and is the recommended way to override a plugin.
PluginBaseClass now declares a destroy() method. Implement it in your own plugins to clean up anything init() registered outside the instance:
export default class MyPlugin extends window.PluginBaseClass {
init() {
this._onClick = this._onClick.bind(this);
this.el.addEventListener('click', this._onClick);
}
destroy() {
this.el.removeEventListener('click', this._onClick);
}
}Note that PluginManager.override() still requires the exact selector the plugin was registered with. Overriding FormCmsHandler for example only takes effect with the selector .cms-element-form form.
PluginManager.extend() can extend plugins under a new name again
PluginManager.extend(fromName, newName, ...) threw a TypeError for every plugin, because it assigned to the non-writable prototype property of the generated class. Extending a lazily loaded plugin additionally failed because the unloaded plugin cannot be used as a base class. Both cases now work; for a lazily loaded parent, the extended class is built once the parent has been loaded.
Storefront extension bundles use their own webpack chunk loading global
Every storefront extension build inherits the core storefront webpack context and therefore used the default webpackChunk chunk loading global, the same one the core storefront and all other extensions use. Sharing that global lets one build's webpack runtime process another build's chunks, so a dynamic import() can resolve to a module from a different bundle when chunk ids collide.
Extension builds now set output.uniqueName to their technical name, which gives each of them its own global, for example webpackChunkswag_my_theme. The core storefront bundle intentionally keeps the default webpackChunk global, so its emitted runtime stays unchanged. If you relied on the shared window.webpackChunk array, for example to inject chunks into another bundle, use the extension specific global instead. Rebuild your storefront assets to pick up the change.
The "Top results" sorting label is translatable
score is a locked product sorting, so its label could not be edited in Settings > Products > Sorting and only ever existed for en-GB and de-DE. Every other language fell back to one of those two.
@Storefront/storefront/component/sorting.html.twig now renders the filter.sortByScore snippet for the score sorting instead of its database label, so it can be translated for any language through snippet management or a theme snippet file. All other sortings keep rendering the label configured in the administration.
App System
App installation recovers ambiguously failed registrations
A newly registered or rotated app secret only becomes active once the app confirms it. If an installation or secret rotation is interrupted before that confirmation — a crash, a timeout, or an unreachable app server — re-running bin/console app:install <app-name> now recovers the app instead of reporting it as already installed.
Recovery also survives an uninstall. An app that adopted a secret the shop never committed rejects everything the shop signs afterwards, including the app.deleted webhook, so it keeps its registration across an uninstall and a plain reinstall used to fail with a signature error. The unconfirmed secrets are now kept alongside the committed one when an app is removed, and reinstalling authenticates with them.
Apps can register custom fields on media folders
Apps can now register custom fields on the media_folder and media_folder_configuration entities. Previously these entities were not part of the allowed related-entities for app custom field sets, so custom fields could only be attached to entities such as product, order, or media.
Relating a custom field set to media_folder_configuration is particularly useful because it inherits Shopware's existing folder configuration inheritance: folders that inherit their parent's configuration (useParentConfiguration) automatically share these custom field values.
Define the fields in Resources/config/custom-fields.xml (the inline <custom-fields> element in manifest.xml is deprecated since 6.7.13.0):
<custom-fields xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://raw.githubusercontent.com/shopware/shopware/trunk/src/Core/System/CustomField/Schema/custom-fields-1.0.xsd">
<custom-field-set>
<name>my_app_folder_settings</name>
<label>My App</label>
<related-entities>
<media_folder_configuration/>
</related-entities>
<fields>
<bool name="my_app_enabled">
<label>Enabled</label>
</bool>
</fields>
</custom-field-set>
</custom-fields>Media folder settings modal publishes its data sets for app extensions
The administration media folder settings modal (sw-media-modal-folder-settings) now publishes its mediaFolder and configuration entities as data sets. Meteor Admin SDK apps that add a tab or component section to the modal can read and modify them — for example to render folder-level settings and persist them (as custom fields on media_folder_configuration) through the modal's native save:
sw-media-modal-folder-settings__mediaFoldersw-media-modal-folder-settings__configuration
Hosting & Configuration
Local translation files and optional automatic updates
The translation system can store downloaded translation files locally instead of on the configured private filesystem. Set shopware.translation.use_local_filesystem to true and include var/translation in the deployed release. Run translation:download during the build to populate that directory without creating language or snippet-set records.
The daily translation update task can be disabled with shopware.translation.scheduled_task.enabled: false. Use this for immutable deployments that update translation files only during builds. Both options retain their previous behavior by default.
Optional Clear-Site-Data header on customer logout
On customer logout the storefront can send a Clear-Site-Data header, so the browser drops data left over from the session. Disabled by default:
# config/packages/storefront.yaml
storefront:
security:
clear_site_data_on_logout: ['storage']Allowed directives are cache, cookies and storage; anything else is rejected at container build time. Choose them deliberately, as the header applies to the whole origin and not just to the storefront:
cookiescovers the registrable domain (eTLD+1), so on a shared domain it also logs the merchant out of the Administration and resets the cookie consent.storageclearslocalStorage,sessionStorage, IndexedDB and service workers, which breaks a PWA on the same origin.cachemakes the browser download all assets again after every logout.
The header requires a trustworthy origin (HTTPS or http://localhost) and is ignored by Safari on the logout redirect.
Fallback thumbnail sizes for remote thumbnails
When remote thumbnails are enabled, operators can optionally configure shopware.media.remote_thumbnails.fallback_sizes. It defaults to [] and accepts entries with width and height:
# config/packages/shopware.yaml
shopware:
media:
remote_thumbnails:
fallback_sizes:
- { width: 400, height: 400 }Fallback sizes apply only in remote-thumbnail mode to media in known folders whose configuration has createThumbnails: true but no assigned thumbnail sizes. Configured folder-specific sizes remain the normal source when thumbnail creation is enabled. A folder with createThumbnails: false is an explicit opt-out and receives no thumbnail URLs, even when fallback sizes are configured. Media without a known folder mapping also receives no fallback thumbnail URLs.
SERVICE_REGISTRY_URL is limited to Shopware domains in production
The service registry decides which Shopware Services a shop installs and where their code is downloaded from. With APP_ENV=prod, SERVICE_REGISTRY_URL is now only used when its host is shopware.io or a subdomain of it. Any other value is ignored and https://registry.services.shopware.io is used instead, so a mistyped registry URL no longer breaks service installation on a live shop.
Other environments are unrestricted, so local setups and tests can still point at their own registry.
Fixed bugs
- Fixed a shipping-cost calculation that could crash when a cart contained errors and could delete the customer's persisted cart (#19419).
- Fixed fixed-price promotions incorrectly discounting carts whose total was already below the configured promotion price (#18960).
- Fixed order recalculation and conversion endpoints accepting authenticated API requests without the required order permissions (#18968).
- Fixed checkout transaction amounts remaining outdated when a tax provider changes the cart total (#18623).
- Fixed MySQL 8.4 migration failures caused by non-standard foreign-key constraints, allowing affected shops to complete upgrades (#18631).
See all fixed bugs in this release: https://github.com/shopware/shopware/milestone/44?closed=1
Credits
Thanks to all diligent friends for helping us make Shopware better and better with each pull request!
See all contributors on this page: https://github.com/shopware/shopware/releases/tag/v6.7.14.0#Contributors
More resources
- Detailed diff on Github to the former version
- Changelog on GitHub for this version.
- Release News corporate blog post
- Installation overview
- Update from a previous installation
Get in touch
Discuss about decisions, bugs you might stumble upon, etc in our community discord. See you there 😉