UPGRADE-2.1.md
2.1.13 TO 2.1.14Translations for admin-managed resources (such as product options, attributes, associations) are now resolved using the current admin locale instead of the default %locale% parameter.
Previously, translation-aware query builders relied on a static %locale% parameter, which could lead to inconsistencies when the admin user was working in a different language than the application default.
This has been updated to dynamically resolve the locale from the admin context.
sylius_grid:
grids:
sylius_admin_product_association_type:
driver:
name: doctrine/orm
options:
class: "%sylius.model.product_association_type.class%"
repository:
method: createListQueryBuilder
arguments: ["%locale%"]
sylius_grid:
grids:
sylius_admin_product_association_type:
driver:
name: doctrine/orm
options:
class: "%sylius.model.product_association_type.class%"
repository:
method: createListQueryBuilder
arguments: ["expr:service('sylius.context.locale').getLocaleCode()"]
A missing page title has been restored to the admin resource show pages (e.g. product, order, customer, shipment, catalog promotion).
Added separated show routes for each resource with dedicated show page templates.
The Sylius\Bundle\CoreBundle\Validator\Constraints\OrderPaymentMethodEligibilityValidator and
Sylius\Bundle\ApiBundle\Validator\Constraints\OrderPaymentMethodEligibilityValidator now checks also if
payment method is assigned to the channel of the order.
2.1.12 TO 2.1.13Telemetry has been improved with per-query database timeouts to prevent slow queries from blocking admin panel requests. Timeouts are applied automatically using platform-specific mechanisms (MySQL, MariaDB, PostgreSQL).
The global query timeout (in milliseconds, default: 60000, minimum: 1000) is configurable via environment variable:
SYLIUS_TELEMETRY_QUERY_TIMEOUT=30000
Additionally, telemetry collection is now rate-limited — it will be skipped if it was already triggered within the last hour, preventing redundant data collection on rapid admin page loads.
2.1.11 TO 2.1.12This is a security release addressing multiple vulnerabilities. Updating is strongly recommended.
An unauthenticated DQL injection vulnerability has been fixed in the following API order filters:
Sylius\Bundle\ApiBundle\Filter\Doctrine\ProductPriceOrderFilterSylius\Bundle\ApiBundle\Filter\Doctrine\TranslationOrderNameAndLocaleFilterPreviously, user-supplied sort direction values (e.g. order[price], order[translation.name]) were passed directly
into DQL ORDER BY clauses without validation, allowing an attacker to inject arbitrary DQL expressions.
Both filters now define an ALLOWED_DIRECTIONS whitelist (['asc', 'desc']) and validate the input against it
before applying it to the query. Invalid values are silently ignored.
Changes in ProductPriceOrderFilter:
final class ProductPriceOrderFilter extends AbstractContextAwareFilter
{
+ private const ALLOWED_DIRECTIONS = ['asc', 'desc'];
+
protected function filterProperty(/* ... */)
{
// ...
+ $direction = strtolower($value['price']);
+ if (!in_array($direction, self::ALLOWED_DIRECTIONS, true)) {
+ return;
+ }
// ...
- ->orderBy('channelPricing.price', $value['price'])
+ ->orderBy('channelPricing.price', $direction)
}
}
Changes in TranslationOrderNameAndLocaleFilter:
final class TranslationOrderNameAndLocaleFilter extends AbstractContextAwareFilter
{
+ private const ALLOWED_DIRECTIONS = ['asc', 'desc'];
+
protected function filterProperty(/* ... */)
{
// ...
- $direction = $value['translation.name'];
+ $direction = strtolower($value['translation.name']);
+ if (!in_array($direction, self::ALLOWED_DIRECTIONS, true)) {
+ return;
+ }
// ...
- ->orderBy('translation.name', $value['translation.name'])
+ ->orderBy('translation.name', $direction)
}
}
No action required — this fix is applied automatically upon updating. If you have extended or overridden either of these filters, verify that your custom implementation also validates the sort direction.
A race condition has been fixed where concurrent orders could exceed a promotion's usage limit. When multiple checkouts
completed simultaneously, the non-atomic read-then-write of Promotion::$used allowed the counter to be incremented
beyond usageLimit.
A new class has been introduced:
Sylius\Bundle\CoreBundle\Doctrine\ORM\Promotion\Modifier\AtomicOrderPromotionsUsageModifier
This class implements Sylius\Component\Core\Promotion\Modifier\OrderPromotionsUsageModifierInterface and uses
atomic SQL statements (UPDATE ... WHERE used < usage_limit and SELECT ... FOR UPDATE) to enforce promotion
and coupon usage limits at the database level, preventing race conditions.
Its constructor accepts a Doctrine\DBAL\Connection:
public function __construct(Connection $connection)
The new service decorates the existing sylius.promotion_usage_modifier service:
<service
id="sylius.promotion_usage_modifier.atomic"
class="Sylius\Bundle\CoreBundle\Doctrine\ORM\Promotion\Modifier\AtomicOrderPromotionsUsageModifier"
decorates="sylius.promotion_usage_modifier"
>
<argument type="service" id="doctrine.dbal.default_connection" />
</service>
If you have overridden or decorated the sylius.promotion_usage_modifier service, review your customizations
to ensure compatibility with the new decorator chain.
A new exception class has been introduced:
Sylius\Component\Core\Promotion\Exception\PromotionUsageLimitReachedException
This exception extends Doctrine\ORM\OptimisticLockException and is thrown when a promotion or coupon usage
limit has been reached during checkout. It provides two named constructors:
PromotionUsageLimitReachedException::withPromotionCode(string $code): self
PromotionUsageLimitReachedException::withCouponCode(string $code): self
If you have custom error handling around the checkout completion workflow (e.g. in state machine callbacks or event listeners), you may want to catch this exception to display an appropriate message to the customer.
A stored XSS vulnerability has been fixed in the admin panel's taxon tree and autocomplete components. Taxon names containing malicious HTML/JavaScript were rendered without escaping.
Affected files:
Sylius\Bundle\AdminBundle\Resources\assets\controllers\ProductTaxonTreeController.js — taxon name is now escaped
via textContent before insertion into the DOM template.Sylius\Bundle\AdminBundle\Resources\assets\controllers\TaxonTreeController.js — uses textContent assignment
instead of string interpolation with replaceAll('__TAXON_NAME__', name).Sylius\Bundle\AdminBundle\Resources\assets\scripts\autocomplete-xss-protection.js — intercepts
autocomplete:pre-connect events and escapes label fields before rendering.No action required — this fix is applied automatically upon updating. If you have custom JavaScript that renders
taxon names or autocomplete labels using innerHTML or string templates, review your code for similar XSS vectors.
A stored XSS vulnerability has been fixed in the shop breadcrumbs template. Breadcrumb labels were rendered using
Twig's |raw filter, allowing injected HTML in taxon or order names to execute in the browser.
Changes in shared/breadcrumbs.html.twig:
- <a class="link-reset" href="{{ item.path }}">{{ item.label|raw }}</a>
+ <a class="link-reset" href="{{ item.path }}">{{ item.label }}</a>
...
- <span class="text-body-tertiary text-break">{{ item.label|raw }}</span>
+ <span class="text-body-tertiary text-break">{{ item.label }}</span>
If you have overridden the shared/breadcrumbs.html.twig template, ensure you are not using the |raw filter
on user-controllable label values.
A DOM-based XSS vulnerability has been fixed in ApiLoginController.js. The server error message was inserted
using innerHTML, allowing malicious content in the response to execute JavaScript.
- errorElement.innerHTML = response.message;
+ errorElement.textContent = response.message;
No action required — this fix is applied automatically upon updating.
An Insecure Direct Object Reference (IDOR) vulnerability has been fixed in the checkout address LiveComponent.
The addressFieldUpdated() method in Checkout\Address\FormComponent used $this->addressRepository->find($addressId),
allowing any authenticated customer to load another customer's address by manipulating the #[LiveArg] value.
The fix replaces find() with findOneByCustomer() to scope the lookup to the current customer:
- $address = $this->addressRepository->find($addressId);
+ $customer = $this->customerContext->getCustomer();
+ if (!$customer instanceof CustomerInterface) {
+ return;
+ }
+ $address = $this->addressRepository->findOneByCustomer((string) $addressId, $customer);
+ if (null === $address) {
+ return;
+ }
Additionally, SummaryComponent::refreshCart() and WidgetComponent::refreshCart() no longer accept an external
$cartId argument — they use the internally held cart reference instead, preventing cart ID manipulation.
No action required — this fix is applied automatically upon updating. If you have extended or overridden
Checkout\Address\FormComponent, Cart\SummaryComponent, or Cart\WidgetComponent, review your customizations
to ensure they do not expose similar IDOR vectors.
An authorization bypass has been fixed in AddItemToCartHandler. Previously, any authenticated user could add items
to another user's cart by providing a different cart token in the API request.
The handler now validates that the current user has access to the cart before modifying it:
final readonly class AddItemToCartHandler
{
public function __construct(
// ...
+ private ?UserContextInterface $userContext = null,
) {
}
public function __invoke(AddItemToCart $addItemToCart): OrderInterface
{
// ...
+ $this->assertCartAccessible($cart);
// ...
}
+
+ private function assertCartAccessible(OrderInterface $cart): void
+ {
+ // Validates current user matches cart owner
+ // Guest carts remain accessible
+ // Throws NotFoundHttpException if access denied
+ }
}
The UserContextInterface service is now injected into the handler via command_handlers.xml.
No action required — this fix is applied automatically upon updating. If you have overridden the
AddItemToCartHandler or its service definition, ensure the new UserContextInterface argument is included.
An open redirect vulnerability has been fixed in CurrencySwitchController, ImpersonateUserController and
StorageBasedLocaleSwitcher. These controllers no longer use the HTTP Referer header for redirects. Instead,
they use the RouterInterface to generate a redirect URL based on the _sylius.redirect route attribute
(defaulting to sylius_shop_homepage).
A new trait has been added: Sylius\Bundle\ShopBundle\Controller\RedirectTrait.
Affected classes:
Sylius\Bundle\ShopBundle\Controller\CurrencySwitchControllerSylius\Bundle\AdminBundle\Controller\ImpersonateUserControllerSylius\Bundle\ShopBundle\Locale\StorageBasedLocaleSwitcherIf your application relied on the Referer-based redirect behavior, you can customize the redirect target by overriding the route definition:
sylius_shop_switch_currency:
path: /{_locale}/switch-currency/{code}
methods: [GET]
defaults:
_controller: sylius.controller.shop.currency_switch:switchAction
_sylius:
redirect: sylius_shop_homepage
The Sylius\Bundle\AdminBundle\Controller\NotificationController has been updated to reduce the frequency and
duration of outbound requests to GUS.
The constructor of NotificationController has been modified:
public function __construct(
private ClientInterface $client,
private MessageFactory $messageFactory,
string $hubUri,
private string $environment,
+ private CacheItemPoolInterface $cache,
)
The cache.app service is injected as the new argument.
Responses are now cached for 24 hours (TTL = 86400s) using PSR-6 CacheItemPoolInterface.
Subsequent calls to getVersionAction() return the cached result without making an HTTP request.
HTTP request timeouts have been added:
-$hubResponse = $this->client->send($hubRequest, ['verify' => false]);
+$hubResponse = $this->client->send($hubRequest, [
+ 'verify' => false,
+ 'timeout' => 2,
+ 'connect_timeout' => 1,
+]);
If you have overridden the sylius.controller.admin.notification service or its arguments, update your
configuration to include the new CacheItemPoolInterface argument.
2.1.8 TO 2.1.9Sylius 2.1.9 introduces anonymous telemetry to help us understand how Sylius is used and improve the platform.
What data is collected:
No sensitive data is ever collected - no customer information, no order details, no personal data.
Configuration:
Telemetry is enabled by default and uses a default salt for hashing the installation ID.
To disable telemetry, set the following environment variable in your .env file:
SYLIUS_TELEMETRY_ENABLED=0
To change the salt, set the SYLIUS_TELEMETRY_SALT environment variable:
SYLIUS_TELEMETRY_SALT=your-custom-salt
Database migration (optional):
This release includes an optional database migration that adds an index to improve telemetry query performance. The telemetry system works without this index, but adding it will make data collection faster, especially for stores with large order volumes.
To run the migration:
php bin/console doctrine:migrations:migrate
If you want to skip this migration:
php bin/console doctrine:migrations:version 'Sylius\Bundle\CoreBundle\Migrations\Version20251126120000' --add --no-interaction
# For PostgreSQL:
php bin/console doctrine:migrations:version 'Sylius\Bundle\CoreBundle\Migrations\Version20251126120001' --add --no-interaction
For more details, see the Telemetry documentation.
choice_value => 'code' option has been removed from autocomplete form types (ProductAutocompleteType,
ProductVariantAutocompleteType, ProductAttributeAutocompleteType, TaxonAutocompleteType,
ProductOptionAutocompleteType) to fix performance issue with large datasets (#17953).
Autocomplete fields now use entity IDs instead of codes as their internal values.The Sylius\Bundle\AdminBundle\Twig\Component\Dashboard\StatisticsComponent constructor now requires
a Symfony\Component\Clock\ClockInterface as the third argument. Not passing it is deprecated
and will be prohibited in Sylius 3.0.
The $clock argument in Sylius\Bundle\AdminBundle\Notification\HubNotificationProvider constructor is deprecated
and will be removed in Sylius 3.0.
The priorities of hookables in sylius_admin.order.update.content.form.shipping_address hook have been adjusted
to match sylius_admin.order.update.content.form.billing_address and fix duplicate priority values.
If you have customized these hooks or added custom hookables with priorities between the old values,
please review your priority configuration:
| Hookable | Old Priority | New Priority |
|---|---|---|
| company | 700 | 800 |
| first_name | 600 | 700 |
| last_name | 500 | 600 |
| country | 400 | 500 |
| phone_number | 300 | 400 |
| street_address | 200 | 300 |
2.1.7 TO 2.1.8after callback for updating average ratings when a review is accepted has been disabled to prevent duplicate calculations.
This change affects the sylius_product_review state machine configuration in the accept transition.The average rating updater callback had priority -100 and was being executed twice, which has now been fixed by disabling this specific callback.
callbacks:
after:
sylius_update_rating:
on: ["accept"]
do: ["@sylius.updater.product_review.average_rating", "updateFromReview"]
args: ["object"]
priority: -100
+ disabled: true
2.1.5 TO 2.1.62.1.2 TO 2.1.3Symfony\Component\Routing\Matcher\UrlMatcherInterface instance as the last argument to the Sylius\Bundle\ShopBundle\Locale\StorageBasedLocaleSwitcher constructor is deprecated and will be required in Sylius 3.0.sylius_shop.context.locale.storage_based service's tag sylius.context.locale has been changed from -64 to 64 to ensure proper execution order in the locale context chain.2.0 TO 2.1The sylius_admin_customer_orders_statistics route has been deprecated.
The minimum version of Symfony 7 packages has been bumped from Symfony ^7.1 to ^7.2
The tabler package has been updated to version ^1.3.0. Please pay attention to the accordion element in final applications, as its implementation has changed.
The sylius_admin.dashboard.index.content.latest_statistics.new_customers hook has been deprecated and disabled.
It has been replaced by the sylius_admin.dashboard.index.content.latest_statistics.pending_actions.
The history, cancel and resend_confirmation_email hookables from 'sylius_admin.order.show.content.header.title_block.actions'
hook have been deprecated and disabled. Now these templates are located in 'sylius_admin.order.show.content.header.title_block.actions.list' hook.
'sylius_shop.account.address_book.index.content.main.buttons' hook has been deprecated and disabled. Content
of this hook has been moved to 'sylius_shop.account.address_book.index.content.main.header' section.
'sylius_shop.account.address_book.index.content.main.buttons.add_address' hook has been deprecated and disabled.
Content of this hook has been moved to 'sylius_shop.account.address_book.index.content.main.header.buttons.add_address' section.
The price, original_price, minimum_price hookables from 'sylius_admin.product.update.content.form.sections.channel_pricing'
hook have been deprecated and disabled. Now these templates are located in 'sylius_admin.product.create.content.form.sections.channel_pricing.info'.
Sylius has modernized its asset management system with these key improvements:
All core controllers now use standardized bundle prefixes:
Admin Controllers
| Old Path | New Path |
|---|---|
slug | @sylius/admin-bundle/slug |
taxon-slug | @sylius/admin-bundle/taxon-slug |
taxon-tree | @sylius/admin-bundle/taxon-tree |
delete-taxon | @sylius/admin-bundle/delete-taxon |
product-attribute-autocomplete | @sylius/admin-bundle/product-attribute-autocomplete |
product-taxon-tree | @sylius/admin-bundle/product-taxon-tree |
save-positions | @sylius/admin-bundle/save-positions |
compound-form-errors | @sylius/admin-bundle/compound-form-errors |
tabs-errors | @sylius/admin-bundle/tabs-errors |
Shop Controller
api-login → @sylius/shop-bundle/api-login
Configuration Files
assets/
admin/
controllers.json # Admin controller configurations
shop/
controllers.json # Shop controller configurations
controllers.json # Shared controllers configurations imported via flex
Example Configuration:
{
"@sylius/admin-bundle/slug": {
"enabled": true,
"fetch": "lazy"
}
}
Key Actions:
Automatic Discovery
./controllers/ directory[name]_controller.jsJSON Configuration
controllers.json filesManual Registration
// In bootstrap.js:
import CustomController from './custom_controller_dir/custom_controller';
app.register('custom', CustomController);
Use for:
Note: The old mechanism will remain functional until you actively migrate to the new system.