With 6.6 we are marking a lot of HTTP Cache and Reverse Proxy classes as @internal and move them to the core. We are preparing a bigger cache rework in the next releases. The cache rework will be done within the v6.6 version lane and and will be released with 6.7.0 major version. The cache rework will be a breaking change and will be announced in the changelog of 6.7.0. We will provide a migration guide for the cache rework, so that you can prepare your project for the cache rework.
You can find more details about the cache rework in the shopware/shopware discussions
Since the cache is a critical component for systems, we have taken the liberty of marking almost all classes as @internal for the time being. However, we have left the important events and interfaces public so that you can prepare your systems for the changes now. Even though there were a lot of deprecations in this release, 99% of them involved moving the classes to the core domain.
But there is one big change that affects each project and nearly all repositories outside which are using phpstan.
We had to refactor the Kernel bootstrapping and the Kernel itself. When you forked our production template, or you boot the kernel somewhere by your own, you have to change the bootstrapping as follows:
#### Before #####
$kernel = new Kernel(
environment: $appEnv,
debug: $debug,
pluginLoader: $pluginLoader
);
#### After #####
$kernel = KernelFactory::create(
environment: $appEnv,
debug: $debug,
classLoader: $classLoader,
pluginLoader: $pluginLoader
);
### In case of static code analysis
KernelFactory::$kernelClass = StaticAnalyzeKernel::class;
/** @var StaticAnalyzeKernel $kernel */
$kernel = KernelFactory::create(
environment: 'phpstan',
debug: true,
classLoader: $this->getClassLoader(),
pluginLoader: $pluginLoader
);
The way how you can access the session in unit test has changed.
The session is no more accessible via the request/response.
You have to use the session.factory
service to access it or use the SessionTestBehaviour
for a shortcut
##### Before
$this->request(....);
$session = $this->getBrowser()->getRequest()->getSession();
##### After
use SessionTestBehaviour;
$this->request(....);
// shortcut via trait
$this->getSession();
// code behind the shortcut
$this->getContainer()->get('session.factory')->getSession();
Since we are moving the cache to the core, you have to change the way you can manipulate the http cache.
- If you decorated or replaced the
src/Storefront/Framework/Cache/HttpCacheKeyGenerator.php
, this will be no more possible in the upcoming release. You should use the http cache events - You used one of the http cache events --> They will be moved to the core, so you have to adapt the namespace+name of the event class. The signature is also not 100% the same, so please check the new event classes (public properties, etc.)
#### Before
<?php
namespace Foo;
use Shopware\Storefront\Framework\Cache\Event\HttpCacheGenerateKeyEvent;
use Shopware\Storefront\Framework\Cache\Event\HttpCacheHitEvent;
use Shopware\Storefront\Framework\Cache\Event\HttpCacheItemWrittenEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class Subscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
return [
HttpCacheHitEvent::class => 'onHit',
HttpCacheGenerateKeyEvent::class => 'onKey',
HttpCacheItemWrittenEvent::class => 'onWrite',
];
}
}
#### After
<?php
namespace Foo;
use Shopware\Core\Framework\Adapter\Cache\Event\HttpCacheHitEvent;
use Shopware\Core\Framework\Adapter\Cache\Event\HttpCacheKeyEvent;
use Shopware\Core\Framework\Adapter\Cache\Event\HttpCacheStoreEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class Subscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
return [
HttpCacheHitEvent::class => 'onHit',
HttpCacheKeyEvent::class => 'onKey',
HttpCacheStoreEvent::class => 'onWrite',
];
}
}
If you implement an own reverse proxy gateway, you have to change the namespace of the gateway and the event.
#### Before
class RedisReverseProxyGateway extends \Shopware\Storefront\Framework\Cache\ReverseProxy\AbstractReverseProxyGateway
{
// ...
}
#### After
class RedisReverseProxyGateway extends \Shopware\Core\Framework\Adapter\Cache\ReverseProxy\AbstractReverseProxyGateway
{
// ...
}
We deprecated all Http cache warmer, because they will be not usable with the new http kernel anymore. They are also not suitable for the new cache rework or for systems which have a reverse proxy or a load balancer in front of the shopware system. Therefore, we marked them as deprecated and will remove them in the next major version. You should use instead a real website crawler to warmup your desired sites, which is much more suitable and realistic for your system.
If you are relying on the sales_channel.analytics
association, please associate the definition directly with the criteria because we will remove autoload from version 6.6.0.0.
You can now simply adding a context state temporarily for an internal process without saving the previous scope and restore it:
<?php
namespace Examples;
use Shopware\Core\Framework\Context;
class Before
{
public function foo(Context $context)
{
$before = $context->getStates();
$context->addState('state-1', 'state-2');
// do some stuff or call some services which changed the scope
$context->removeState('state-1');
$context->removeState('state-2');
}
}
class After
{
public function foo(Context $context)
{
$context->state(function (Context $context) {
// do some stuff or call some services which changed the scope
}, 'state-1', 'state-2');
}
}
Within the v6.5 lane, the media path handling changed in a way, where we store the path in the database instead of generating it always on-demand. They will be generated, when the media is uploaded. The path can also be provided via api to handle external file uploads.
We also removed the dependency to the entity layer and allow much faster and simpler access to the media path via location structs and a new url generator.
Due to this change, the usage of the UrlGeneratorInterface
changed. The generator is deprecated and will be removed with v6.6.0. We implemented a new generator MediaUrlGenerator
which can be used instead.
<?php
namespace Examples;
use Shopware\Core\Content\Media\Core\Application\AbstractMediaUrlGenerator;use Shopware\Core\Content\Media\Core\Params\UrlParams;use Shopware\Core\Content\Media\MediaCollection;use Shopware\Core\Content\Media\MediaEntity;use Shopware\Core\Content\Media\Pathname\UrlGeneratorInterface;
class BeforeChange
{
private UrlGeneratorInterface $urlGenerator;
public function foo(MediaEntity $media)
{
$relative = $this->urlGenerator->getRelativeMediaUrl($media);
$absolute = $this->urlGenerator->getAbsoluteMediaUrl($media);
}
public function bar(MediaThumbnailEntity $thumbnail)
{
$relative = $this->urlGenerator->getRelativeThumbnailUrl($thumbnail);
$absolute = $this->urlGenerator->getAbsoluteThumbnailUrl($thumbnail);
}
}
class AfterChange
{
private AbstractMediaUrlGenerator $generator;
public function foo(MediaEntity $media)
{
$relative = $media->getPath();
$urls = $this->generator->generate([UrlParams::fromMedia($media)]);
$absolute = $urls[0];
}
public function bar(MediaThumbnailEntity $thumbnail)
{
// relative is directly stored at the entity
$relative = $thumbnail->getPath();
// path generation is no more entity related, you could also use partial entity loading and you can also call it in batch, see below
$urls = $this->generator->generate([UrlParams::fromMedia($media)]);
$absolute = $urls[0];
}
public function batch(MediaCollection $collection)
{
$params = [];
foreach ($collection as $media) {
$params[$media->getId()] = UrlParams::fromMedia($media);
foreach ($media->getThumbnails() as $thumbnail) {
$params[$thumbnail->getId()] = UrlParams::fromThumbnail($thumbnail);
}
}
$urls = $this->generator->generate($paths);
// urls is a flat list with {id} => {url} for media and also for thumbnails
}
}
class ForwardCompatible
{
// to have it forward compatible, you can use the Feature::isActive('v6.6.0.0') function
public function foo(MediaEntity $entity)
{
// we provide an entity loaded subscriber, which assigns the url of
// the UrlGeneratorInterface::getRelativeMediaUrl to the path property till 6.6
// so that you always have the relative url in the MediaEntity::path proprerty
$path = $entity->getPath();
if (Feature::isActive('v6.6.0.0')) {
// new generator call
} else {
// old generator call
}
}
}
Beside the url generator change, we also had to change the media path strategy. The strategies are no longer working with a MediaEntity
. They are now working with a MediaFile
object. This object is a simple struct, which contains the path and the updated at timestamp. The path is the same as the one stored in the database. The updated at timestamp is the timestamp, when the path was generated. This is important for the cache invalidation. The MediaFile
object is also used for the thumbnail generation. The thumbnail generation is now also working with a MediaLocation
object instead.
As foundation, we use \Shopware\Core\Content\Media\Core\Application\AbstractMediaPathStrategy
as base class and dependency injection service id:
<?php
namespace Examples;
class Before extends AbstractPathNameStrategy
{
public function getName(): string
{
return 'filename';
}
public function generatePathHash(MediaEntity $media, ?MediaThumbnailEntity $thumbnail = null): ?string
{
return $this->generateMd5Path($media->getFileName());
}
}
class After extends AbstractMediaPathStrategy
{
public function name(): string
{
return 'file_name';
}
protected function value(MediaLocationStruct|ThumbnailLocationStruct $location): ?string
{
return $location instanceof ThumbnailLocationStruct ? $location->media->fileName : $location->fileName;
}
protected function blacklist(): array
{
return ['ad' => 'g0'];
}
}
It is no more necessary to call the path hashing by your own. All cache busting and other logic is done in the abstract implementation. The functions are now seperated and can be reused in your implementation. The path is generated by 4 segments:
$paths[$location->id] = implode('/', \array_filter([
$type,
$this->md5($this->value($location)),
$this->cacheBuster($location),
$this->physicalFilename($location),
]));
If you want, you can overwrite all of this parts by your own. The strategies are now using MediaLocationStruct
s or ThumbnailLocationStruct
s.
These structs are simple structs, which contains the necessary information to generate the path. We also provide a builder class to simply generate this classes based on entity identifiers:
<?php
namespace Examples;
use Shopware\Core\Content\Media\Core\Application\AbstractMediaPathStrategy;use Shopware\Core\Content\Media\Core\Application\MediaLocationBuilder;
class Consumer
{
private MediaLocationBuilder $builder;
private AbstractMediaPathStrategy $strategy;
public function foo(array $mediaIds)
{
$locations = $this->builder->buildLocations($mediaIds);
$paths = $this->strategy->generate($locations);
}
}
If you implement your own strategy, and you require more data, you can add an event listener for the MediaLocationEvent
or ThumbnailLocationEvent
which allows data manipulation for the provided structs.
It is now possible to trigger the compilation of the storefront css and js via the message queue instead of directly inside the call that changes the theme or activates/deactivates an extension.
You can change the compilation type with the system_config key core.storefrontSettings.asyncThemeCompilation
in the
administration (settings -> system -> storefront
)
The technical name is only required in the Administration for now, but will be required in the API as well in the future (v6.7.0.0). It is used to identify the payment and shipping method in the API and in the administration.
To prevent issues with the upgrade to v6.7.0.0, please make sure to add a technical name to all payment and shipping methods:
Merchants should add a technical name to all custom created payment and shipping methods in the administration.
Plugin developers should add a technical name to all payment and shipping methods during plugin installation / update.
App developers do not need to do anything, as the technical name is automatically generated based on the app name and the payment or shipping method identifier given in the manifest.xml
.
This includes existing app installations.
change in app iFrame this
data.subscribe(
'your-cms-element-name__config-element',
yourCallback,
{ selectors: yourSelectors });
to
const elementId = new URLSearchParams(window.location.search).get('elementId');
data.subscribe(
// add elementId to data key for identifying the correct element config
'your-cms-element-name__config-element' + '__' + elementId,
yourCallback,
{ selectors: yourSelectors });
If you explicitly configure a message to be transported via the async
(default) queue, even though it implements the LowPriorityMessageInterface
which would usually be transported via the low_priority
queue, the transport is overridden for this specific message.
Example:
<?php declare(strict_types=1);
namespace Your\Custom;
class LowPriorityMessage implements LowPriorityMessageInterface
{
}
framework:
messenger:
routing:
'Shopware\Core\Framework\MessageQueue\LowPriorityMessageInterface': low_priority
'Your\Custom\LowPriorityMessage': async
The transport defaults to use Doctrine. You can use the MESSENGER_TRANSPORT_LOW_PRIORITY_DSN
environment variable to change it.
Before:
parameters:
messenger.default_transport_name: 'v65'
env(MESSENGER_TRANSPORT_DSN): 'doctrine://default?auto_setup=false'
After:
parameters:
messenger.default_transport_name: 'v65'
env(MESSENGER_TRANSPORT_DSN): 'doctrine://default?auto_setup=false'
env(MESSENGER_TRANSPORT_LOW_PRIORITY_DSN): 'doctrine://default?auto_setup=false&queue_name=low_priority'
For further details on transports with different priorities, please refer to the Symfony Docs: https://symfony.com/doc/current/messenger.html#prioritized-transports
You might consider using the new low_priority
queue if you are dispatching messages that do not need to be handled immediately.
To configure specific messages to be transported via the low_priority
queue, you need to either adjust the routing or implement the LowPriorityMessageInterface
:
framework:
messenger:
routing:
'Your\Custom\Message': low_priority
or
<?php declare(strict_types=1);
namespace Your\Custom;
class Message implements LowPriorityMessageInterface
{
}
The method \Shopware\Core\Checkout\Cart\LineItem\LineItem::replacePayload
does not do a recursive replacement of the payload anymore, but replaces the payload only on a first level.
Therefore, subarrays of the payload may reduce in items instead of being only added to.
There is a new configuration option shopware.deployment.cluster_setup
which is set to false
by default. If you are using a cluster setup, you need to set this option to true
in your config/packages/shopware.yaml
file.
We deprecated the default delayed cache invalidation storage, as it is not ideal for multi-server usage. Make sure you switch until 6.6 to the new RedisInvalidatorStorage.
shopware:
cache:
invalidation:
delay_options:
storage: cache
dsn: 'redis://localhost'
Shopware 6.5 introduces a new more flexible stock management system. Please see the ADR for a more detailed description of the why & how.
It is disabled by default, but you can opt in to the new system by enabling the STOCK_HANDLING
feature flag.
When you opt in and Shopware is your main source of truth for stock values, you might want to migrate the available_stock field to the stock
field so that the stock
value takes into account open orders.
You can use the following SQL:
UPDATE product SET stock = available_stock WHERE stock != available_stock
Bear in mind that this query might take a long time, so you could do it in a loop with a limit. See \Shopware\Core\Migration\V6_6\Migration1691662140MigrateAvailableStock
for inspiration.
If you have previously decorated \Shopware\Core\Content\Product\DataAbstractionLayer\StockUpdater
you must refactor your code. Depending on what you want to accomplish you have two options:
- You have the possibility to decorate the
\Shopware\Core\Content\Product\Stock\AbstractStockStorage::alter
method. This method is called by\Shopware\Core\Content\Product\Stock\OrderStockSubscriber
as orders are created and transitioned through the various states. By decorating you can persist the stock deltas to a different storage. For example, an API. - You can disable
\Shopware\Core\Content\Product\Stock\OrderStockSubscriber
entirely with thestock.enable_stock_management
configuration setting, and implement your own subscriber to listen to order events. You can use Shopware's stock storage\Shopware\Core\Content\Product\Stock\AbstractStockStorage
, or implement your own entirely.
Decorating \Shopware\Core\Content\Product\SalesChannel\Detail\AbstractAvailableCombinationLoader::load()
&& \Shopware\Core\Content\Product\SalesChannel\Detail\AvailableCombinationLoader::load()
If you decorated \Shopware\Core\Content\Product\SalesChannel\Detail\AvailableCombinationLoader::load()
you should instead decorate \Shopware\Core\Content\Product\SalesChannel\Detail\AvailableCombinationLoader::loadCombinations()
. The method does the same, but the signature is slightly modified.
If you extended \Shopware\Core\Content\Product\SalesChannel\Detail\AbstractAvailableCombinationLoader
, you should implement the new loadCombinations
instead of load
method.
Before:
use Shopware\Core\Content\Product\SalesChannel\Detail\AbstractAvailableCombinationLoader;
use Shopware\Core\Content\Product\SalesChannel\Detail\AvailableCombinationResult;
use Shopware\Core\Framework\Context;
class AvailableCombinationLoaderDecorator extends AbstractAvailableCombinationLoader
{
public function load(string $productId, Context $context, string $salesChannelId): AvailableCombinationResult
{
}
}
After:
use Shopware\Core\Content\Product\SalesChannel\Detail\AbstractAvailableCombinationLoader;
use Shopware\Core\Content\Product\SalesChannel\Detail\AvailableCombinationResult;
use Shopware\Core\System\SalesChannel\SalesChannelContext;
class AvailableCombinationLoaderDecorator extends AbstractAvailableCombinationLoader
{
public function loadCombinations(string $productId, SalesChannelContext $salesChannelContext): AvailableCombinationResult
{
$context = $salesChannelContext->getContext();
$salesChannelId = $salesChannelContext->getSalesChannelId();
}
}
Similarly, if you consume \Shopware\Core\Content\Product\SalesChannel\Detail\AbstractAvailableCombinationLoader
then you will need to adjust your code, to pass in \Shopware\Core\System\SalesChannel\SalesChannelContext
.
Before:
use Shopware\Core\Content\Product\SalesChannel\Detail\AbstractAvailableCombinationLoader;
use Shopware\Core\System\SalesChannel\SalesChannelContext;
class SomeService
{
public function __construct(private AbstractAvailableCombinationLoader $availableCombinationLoader)
{}
public function __invoke(SalesChannelContext $salesChannelContext): void
{
$this->availableCombinationLoader->load('some-product-id', $salesChannelContext->getContext(), $salesChannelContext->getSalesChannelId());
}
}
After:
use Shopware\Core\Content\Product\SalesChannel\Detail\AbstractAvailableCombinationLoader;
use Shopware\Core\System\SalesChannel\SalesChannelContext;
class SomeService
{
public function __construct(private AbstractAvailableCombinationLoader $availableCombinationLoader)
{}
public function __invoke(SalesChannelContext $salesChannelContext): void
{
$this->availableCombinationLoader->loadCombinations('some-product-id', $salesChannelContext);
}
}
If Shopware is not the source of truth for your stock data, you can decorate \Shopware\Core\Content\Product\Stock\AbstractStockStorage
and implement the load
method. When products are loaded in Shopware the load
method will be invoked with the loaded product ID's. You can return a collection of \Shopware\Core\Content\Product\Stock\StockData
objects, each representing a products stock level and configuration. This data will be merged with the Shopware stock levels and configuration from the product. Any data specified will override the product's data.
For example, you can use an API to fetch the stock data:
//<plugin root>/src/Service/StockStorageDecorator.php
<?php
namespace Swag\Example\Service;
use Shopware\Core\Content\Product\Stock\AbstractStockStorage;
use Shopware\Core\Content\Product\Stock\StockData;
use Shopware\Core\Content\Product\Stock\StockDataCollection;
use Shopware\Core\Content\Product\Stock\StockLoadRequest;
use Shopware\Core\Framework\Context;
use Shopware\Core\System\SalesChannel\SalesChannelContext;
class StockStorageDecorator extends AbstractStockStorage
{
public function __construct(private AbstractStockStorage $decorated)
{
}
public function getDecorated(): AbstractStockStorage
{
return $this->decorated;
}
public function load(StockLoadRequest $stockRequest, SalesChannelContext $context): StockDataCollection
{
$productsIds = $stockRequest->productIds;
//use $productIds to make an API request to get stock data
//$result would come from the api response
$result = ['product-1' => 5, 'product-2' => 10];
return new StockDataCollection(
array_map(function (string $productId, int $stock) {
return new StockData($productId, $stock, true);
}, array_keys($result), $result)
);
}
public function alter(array $changes, Context $context): void
{
$this->decorated->alter($changes, $context);
}
public function index(array $productIds, Context $context): void
{
$this->decorated->index($productIds, $context);
}
}
<!--<plugin root>/src/Resources/config/services.xml-->
<services>
<service id="Swag\Example\Service\StockStorageDecorator" decorates="Shopware\Core\Content\Product\Stock\StockStorage">
<argument type="service" id="Swag\Example\Service\StockStorageDecorator.inner" />
</service>
</services>
The product.stock
field is now a realtime representation of the product stock. When writing new extensions which need to query the stock of a product, use this field. Not the product.availableStock
field.
Before:
/** \Shopware\Core\Content\Product\ProductEntity $product */
$stock = $product->getAvailableStock();
After:
/** \Shopware\Core\Content\Product\ProductEntity $product */
$stock = $product->getStock();
If you write to the product.availableStock
field, you should instead write to the product.stock
field. However, there are no plans to remove the product.availableStock
field.
Before:
$this->productRepository->update(
[
[
'id' => $productId,
'availableStock' => $newStockValue
]
],
$context
);
After:
$this->productRepository->update(
[
[
'id' => $productId,
'stock' => $newStockValue
]
],
$context
);
You can disable \Shopware\Core\Content\Product\Stock\OrderStockSubscriber
entirely with the stock.enable_stock_management
configuration setting.
Similar to the example above "Loading stock information from a different source" you can update a different database table or service, or implement custom inventory systems such as multi warehouse by decorating the alter
method.
This method is triggered with an array of StockAlteration
's. Which contain the Product & Line Item ID's, the old quantity and the new quantity.
This method is triggered whenever an order is created or transitioned through the various states.
The BeforeDeleteEvent
has been renamed to \Shopware\Core\Framework\DataAbstractionLayer\Event\EntityDeleteEvent
. Please update your usages:
Before:
/**
* @return array<string, string>
*/
public static function getSubscribedEvents(): array
{
return [
\Shopware\Core\Framework\DataAbstractionLayer\Event\BeforeDeleteEvent::class => 'onBeforeDelete',
];
}
After:
/**
* @return array<string, string>
*/
public static function getSubscribedEvents(): array
{
return [
\Shopware\Core\Framework\DataAbstractionLayer\Event\EntityDeleteEvent::class => 'onBeforeDelete',
];
}
The upcoming release includes a crucial breaking change aimed at resolving a major issue in how repository data is handled for the Admin Extension SDK. This change will be introduced in the next minor version.
The change only affects your app if you are utilizing properties within the context. With this modification, an empty context object will be returned within your Entity. You will receive a custom context object containing your specific context changes only when you also send a custom context object to the admin.
The final context will be merged with the default context in the administration. This allows you to use the default context to access all the necessary data.
- Update your thumbnails by running command:
media:generate-thumbnails
The EntityRepository
class now has a generic type template.
This allows to define the entity type of the repository, which improves the IDE support and static code analysis.
Usage:
use Shopware\Core\Content\Product\ProductCollection;
use Shopware\Core\Framework\Context;
use Shopware\Core\Framework\DataAbstractionLayer\EntityRepository;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
class MyService
/**
* @param EntityRepository<ProductCollection> $productRepository
*/
public function __construct(private readonly EntityRepository $productRepository)
{}
public function doSomething(Context $context): void
{
$criteria = new Criteria();
$criteria->addFilter(new EqualsFilter('active', true));
$products = $this->productRepository->search($criteria, $context)->getEntities();
// $products is now inferred as ProductCollection
}
With 4457 we fixed an issue with duplicated theme images on system:update
and theme:refresh
.
This fix will only prevent future duplicates. In order to remove already existing duplicates from your setup, follow these steps:
- Open the administration media section
- Open the folder
Theme Media
check for duplicated images with the suffix(x)
, where x is a number. For exampledefaultThemePreview_(1).jpg
,defaultThemePreview_(2)
etc. - Check the id of the images without a
(x)
suffix, these are the original images - Go to the database table
theme
and check the fieldpreview_media_id
- If it is not the id of the original image, change it to that. Otherwise leave it as is.
- Check the field
base_config
. This field is a JSON field. Check if there is an UUID inside, which refers to an image/media. - Check for all these UUIDs if they refer to the original image without a
(x)
prefix. If not, change the UUIDs to match the original image. - Check the database table
theme_media
for associations for the theme with duplicated images (with the suffix) and delete all of them. - Now you should be able to delete these duplicates in the administration media section in the folder
Theme Media
- Now do a
composer theme:refresh
This comment on github could also be helpful: github how to clean theme media The images should not be doubled again.
Since v6.5.2.0, we can define the flow custom trigger and the flow app action in one XML file.
To do that, we add the Shopware\Core\Framework\App\Flow\Schema\flow-1.0.xsd
to support defining both of them.
- Example
<flow-extensions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="flow-1.0.xsd">
<flow-events>
<flow-event>...</flow-event>
</flow-events>
<flow-actions>
<flow-action>...</flow-action>
</flow-actions>
</flow-extensions>
With 6.6 the ProductListingFeaturesSubscriber
is removed. This is currently responsible for evaluating the listing request parameters and applying them to the criteria.
In the future this will no longer happen via the corresponding events but via the AbstractListingProcessor
, which follows a more service oriented approach.
If you dispatch one of the following events yourself, and expect the subscriber to process the corresponding data, you should now call the CompositeProcessor
instead:
// before
class MyClass
{
public function load(Criteria $criteria, SalesChannelContext $context) {
$this->dispatcher->dispatch(
new ProductListingCriteriaEvent($criteria, $context, $request)
);
$result = $this->loadListing($request, $criteria, $context);
$this->dispatcher->dispatch(
new ProductListingResultEvent($result, $context, $request)
);
return $result;
}
}
// after
class MyClass
{
public function __construct(private readonly CompositeProcessor $listingProcessor) {}
public function load(Criteria $criteria, SalesChannelContext $context)
{
$this->listingProcessor->prepare($request, $criteria, $context);
$result = $this->loadListing($request, $criteria, $context);
$this->listingProcessor->process($request, $result, $context);
return $result;
}
}
The deserialize method of src/Core/Content/ImportExport/DataAbstractionLayer/Serializer/Entity/MediaSerializer.php was changed such that the filename will be urldecoded before saving it to the cacheMediaFiles. Previously, encoded url raised an error during validateFileNameDoesNotContainForbiddenCharacter when importing them, because they contained % signs. On the other hand, not encoding urls raised an "Invalid media url" exception.
This change allows importing medias with filenames that contain special characters by supplying an encoded url in the import file.
- Changed behavior of text block element, which is able to switch between languages in product detail page
The context
property is used instead of contextData
property in src/Core/Content/Media/Message/GenerateThumbnailsMessage
due to the context
data is serialized in context source
Shopware now uses Symfony version 6.3, please make sure your plugins are compatible.
The event is dispatched before the flow storer restores the data, so you can customize the criteria before passing it to the entity repository
Reference: Shopware\Core\Content\Flow\Events\BeforeLoadStorableFlowDataEvent
Examples:
class OrderStorer extends FlowStorer
{
public function restore(StorableFlow $storable): void
{
...
$criteria = new Criteria();
$criteria->addAssociations([
'orderCustomer',
'lineItems.downloads.media',
]);
$event = new BeforeLoadStorableFlowDataEvent(
OrderDefinition::ENTITY_NAME,
$criteria,
$context,
);
$this->dispatcher->dispatch($event, $event->getName());
$order = $this->orderRepository->search($criteria, $context)->get($orderId);
...
}
}
class YourBeforeLoadStorableFlowOrderDataSubscriber implements EventSubscriberInterface
public static function getSubscribedEvents()
{
return [
'flow.storer.order.criteria.event' => 'handle',
];
}
public function handle(BeforeLoadStorableFlowDataEvent $event): void
{
$criteria = $event->getCriteria();
// Add new association
$criteria->addAssociation('tags');
}
}
If you are relying on the association import_export_log.file
, please associate the definition directly with the criteria because we will remove autoload from version 6.6.0.0.
- Renamed error code from
FRAMEWORK__STORE_CANNOT_DOWNLOAD_PLUGIN_MANAGED_BY_SHOPWARE
toFRAMEWORK__STORE_CANNOT_DELETE_COMPOSER_MANAGED
If you are relying on these associations:
order.stateMachineState
order_transaction.stateMachineState
order_delivery.stateMachineState
order_delivery.shippingOrderAddress
order_transaction_capture.stateMachineState
order_transaction_capture_refund.stateMachineState
tax_rule.type
please associate the definitions directly with the criteria because we will remove autoload from version 6.6.0.0.
We deprecated diverse flow storer interfaces in favor of the new ScalarValuesAware
class. The new ScalarValuesAware
class allows to store scalar values much easier for flows without implementing own storer and interface classes.
If you implemented one of the deprecated interfaces or implemented an own interface and storer class to store simple values, you should replace it with the new ScalarValuesAware
class.
// before
class MyEvent extends Event implements \Shopware\Core\Content\Flow\Dispatching\Aware\UrlAware
{
private string $url;
public function __construct(string $url)
{
$this->url = $url;
}
public function getUrl(): string
{
return $this->url;
}
// ...
}
// after
class MyEvent extends Event implements \Shopware\Core\Content\Flow\Dispatching\Aware\ScalarValuesAware
{
private string $url;
public function __construct(string $url)
{
$this->url = $url;
}
public function getScalarValues(): array
{
return [
'url' => $this->url,
// ...
];
}
// ...
}
The deprecated flow storer interfaces are:
ConfirmUrlAware
ContactFormDataAware
ContentsAware
ContextTokenAware
DataAware
EmailAware
MediaUploadedAware
NameAware
RecipientsAware
ResetUrlAware
ReviewFormDataAware
ScalarStoreAware
ShopNameAware
SubjectAware
TemplateDataAware
UrlAware
If your plugin references media in a way that is not understood by the DAL, for example in JSON blobs, it is now possible for your plugin to inform the system that this media is used and should not be deleted when the \Shopware\Core\Content\Media\UnusedMediaPurger
service is executed.
To do this, you need to create a listener for the Shopware\Core\Content\Media\Event\UnusedMediaSearchEvent
event. This event can be called multiple times during the cleanup task with different sets of media ID's scheduled to be deleted. Your listener should check if any of the media ID's passed to the event are used by your plugin and mark them as used by calling the markMediaAsUsed
method on the event object with an array of the used media ID's.
You can get the media ID's scheduled for deletion from the event object by calling the getMediaIds
method.
See the following implementations for an example:
- \Shopware\Core\Content\Cms\Subscriber\UnusedMediaSubscriber
- \Shopware\Storefront\Theme\Subscriber\UnusedMediaSubscriber
The following method signatures were changed to comply with the parent class/interface signature: Visibility changes:
- Method
configure()
was changed from public to protected in:Shopware\Storefront\Theme\Command\ThemeCompileCommand
- Method
execute()
was changed from public to protected in:Shopware\Core\Framework\Adapter\Asset\AssetInstallCommand
Shopware\Core\DevOps\System\Command\SystemDumpDatabaseCommand
Shopware\Core\DevOps\System\Command\SystemRestoreDatabaseCommand
Shopware\Core\DevOps\Docs\App\DocsAppEventCommand
- Method
getExpectedClass()
was changed from public to protected in:Shopware\Storefront\Theme\ThemeSalesChannelCollection
Shopware\Core\Framework\Store\Struct\PluginRecommendationCollection
Shopware\Core\Framework\Store\Struct\PluginCategoryCollection
Shopware\Core\Framework\Store\Struct\LicenseDomainCollection
Shopware\Core\Framework\Store\Struct\PluginRegionCollection
Shopware\Core\Content\ImportExport\Processing\Mapping\UpdateByCollection
Shopware\Core\Content\ImportExport\Processing\Mapping\MappingCollection
Shopware\Core\Content\Product\Aggregate\ProductCrossSellingAssignedProducts\ProductCrossSellingAssignedProductsCollection
Shopware\Core\Content\Product\Aggregate\ProductCrossSelling\ProductCrossSellingCollection
Shopware\Core\Content\Product\SalesChannel\CrossSelling\CrossSellingElementCollection
Shopware\Core\Content\Product\SalesChannel\SalesChannelProductCollection
Shopware\Core\Checkout\Promotion\Aggregate\PromotionDiscountPrice\PromotionDiscountPriceCollection
- Method
getParentDefinitionClass()
was changed from public to protected in:Shopware\Core\System\SalesChannel\Aggregate\SalesChannelAnalytics\SalesChannelAnalyticsDefinition
Shopware\Core\Content\ImportExport\ImportExportProfileTranslationDefinition
Shopware\Core\Content\Product\Aggregate\ProductCrossSellingAssignedProducts\ProductCrossSellingAssignedProductsDefinition
Shopware\Core\Content\Product\Aggregate\ProductCrossSelling\ProductCrossSellingDefinition
Shopware\Core\Content\Product\Aggregate\ProductFeatureSetTranslation\ProductFeatureSetTranslationDefinition
Shopware\Core\Checkout\Promotion\Aggregate\PromotionTranslation\PromotionTranslationDefinition
- Method
getDecorated()
was changed from public to protected in:Shopware\Core\System\Country\SalesChannel\CachedCountryRoute
Shopware\Core\System\Country\SalesChannel\CachedCountryStateRoute
- Method
getSerializerClass()
was changed from public to protected in:Shopware\Core\Framework\DataAbstractionLayer\Field\StateMachineStateField
Parameter type changes:
- Changed parameter
$url
tostring
in:Shopware\Storefront\Framework\Cache\ReverseProxy\ReverseProxyCache#purge()
- Changed parameter
$data
and$format
tostring
in:Shopware\Core\Framework\Struct\Serializer\StructDecoder#decode()
Shopware\Core\Framework\Struct\Serializer\StructDecoder#supportsDecoding()
Shopware\Core\Framework\Api\Serializer\JsonApiDecoder#decode()
Shopware\Core\Framework\Api\Serializer\JsonApiDecoder#supportsDecoding()
- Changed parameter
$storageName
and$propertyName
tostring
in:Shopware\Core\Framework\DataAbstractionLayer\Field\CustomFields#__construct()
- Changed parameter
$event
toobject
in:Shopware\Core\Framework\Event\NestedEventDispatcher#dispatch()
- Changed parameter
$listener
tocallable
in:Shopware\Core\Framework\Event\NestedEventDispatcher#removeListener()
Shopware\Core\Framework\Event\NestedEventDispatcher#getListenerPriority()
Shopware\Core\Framework\Webhook\WebhookDispatcher#removeListener()
Shopware\Core\Framework\Webhook\WebhookDispatcher#getListenerPriority()
- Changed parameter
$constraints
toSymfony\Component\Validator\Constraint|array|null
in:Shopware\Core\Framework\Validation\HappyPathValidator#validate()
- Changed parameter
$object
toobject
,$propertyName
tostring
,$groups
tostring|Symfony\Component\Validator\Constraints\GroupSequence|array|null
and$objectOrClass
toobject|string
in:Shopware\Core\Framework\Validation\HappyPathValidator#validateProperty()
Shopware\Core\Framework\Validation\HappyPathValidator#validatePropertyValue()
- Changed parameter
$record
toiterable
in:Shopware\Core\Content\ImportExport\Processing\Pipe\EntityPipe#in()
- Changed parameter
$warmupDir
tostring
in:Shopware\Core\Kernel#reboot()
You can now use the twig.cache
configuration to configure the directory where twig caches are stored as described in the symfony docs. This is independent from the kernel.cache_dir
configuration, but by default it will still fallback to the %kernel.cache_dir%/twig
directory.
This is useful when the kernel.cache_dir
is configured to be a read-only directory.
- Deprecated
AbstractIncrementer::getDecorated
, increment are not designed for decoration pattern - Deprecated
MySQLIncrementer
, implementation will be private, use abstract class for type hints - Deprecated
RedisIncrementer
, implementation will be private, use abstract class for type hints - Deprecated
ImportExport\PriceFieldSerializer::isValidPrice
, function will be private in v6.6 - Deprecated
CsvReader::loadConfig
, function will be private in v6.6 - Deprecated
NewsletterSubscribeRoute.php
, function will be private in v6.6
App scripts now have access to the shopware version via the shopware.version
global variable.
{% if version_compare('6.4', shopware.version, '<=') %}
{# 6.4 or lower compatible code #}
{% else %}
{# 6.5 or higher compatible code #}
{% endif %}
Increased Node version to 18 and NPM to version 8 or 9.
- Replace any old icon your integration uses with its successor. A mapping can be found here.
- The object keys of the json file are the legacy icons. The values the replacement.
- In the next major, the icons will have no space around them by default. This could eventually lead to bigger looking icons in some places. If this is the case you need to adjust the styling with CSS so that it matches your wanted look.
Before:
<sw-icon name="default-object-image"/>
After:
<sw-icon name="regular-image"/>
Use value
property instead.
Before:
<sw-simple-search-field
…
:search-term="term"
…
/>
After:
<sw-simple-search-field
…
:value="term"
…
/>
To get the new state selection exchange your sw-order-state-select
component uses with sw-order-state-select-v2
.
No required props have been added or removed, only the styling and layout of the component changed.
- action
setAppModules
insrc/app/state/shopware-apps.store.ts
is removed - action
setAppModules
insrc/app/state/shopware-apps.store.ts
is removed
Shopware 6 now requires at least PHP 8.1.0. Please update your PHP version to at least 8.1.0. Refer to the upgrade guide to v8.0 and v8.1 for more information.
Shopware now uses symfony components in version 6.2, please make sure your plugins are compatible. Refer to the upgrade guides to v6.0, v6.1 and v6.2.
We changed the used Elasticsearch DSL library to shyim/opensearch-php-dsl
, instead of ongr/elasticsearch-dsl
.
It is a fork of the ONGR library and migrating should be straight forward. You need to change the namespace of the used classes from ONGR\ElasticsearchDSL
to OpenSearchDSL
.
Before:
use ONGR\ElasticsearchDSL\Aggregation\AbstractAggregation;
After:
use OpenSearchDSL\Aggregation\AbstractAggregation;
Also, we changed the Elasticsearch PHP SDK to OpenSearch
- Renamed following environment variables to use more generic environment variable name used by cloud providers:
SHOPWARE_ES_HOSTS
toOPENSEARCH_URL
MAILER_URL
toMAILER_DSN
You can change this variable back in your installation using a config/packages/elasticsearch.yaml
with
elasticsearch:
hosts: "%env(string:SHOPWARE_ES_HOSTS)%"
or prepare your env by replacing the var with the new one like
elasticsearch:
hosts: "%env(string:OPENSEARCH_URL)%"
We removed the single record behavior in the sync api. This means, that all operations and records are now handled as a collection and validated and written within one transaction.
The headers HTTP_Fail-On-Error
and single-operation
were removed. The single-operation
header is now always true.
We upgraded DBAL from 2.x to 3.x. Please take a look at the DBAL upgrade information itself to see if you need to adjust your code.
Before 6.5 our default message queue transport name were default
. We changed this to async
to ensure that application which are running with the 6.5 aren't handling the message of the 6.4.
You're now able to configure own transports and dispatch message over your own transports by adding new transports within the framework.messenger.transports
configuration. For more details, see official symfony documentation: https://symfony.com/doc/current/messenger.html
Before 6.5, we php-serialized all message queue messages and php-unserialize them. This causes different problems, and we decided to change this format to json. This format is also recommend from symfony and other open source projects. Due to this change, you may have to change your messages when you added some php objects to the message. If you have simple PHP objects within a message, the symfony serializer should be able to encode and decode your objects. For more information take a look to the offical symfony documentation: https://symfony.com/doc/current/messenger.html#serializing-messages
Since v6.6.0.0, ContextTokenResponse
class won't return the contextToken value in the response body anymore, please using the header sw-context-token
instead
The Route-level configurations for HttpCache
, Entity
and NoStore
where changed from custom annotations to @Route
defaults.
The reasons for those changes are outlined in this ADR and for a lot of former annotations this change was already done previously.
Now we also change the handling for the last three annotations to be consistent and to allow the removal of the abandoned sensio/framework-extra-bundle
.
This means the @HttpCache
, @Entity
, @NoStore
annotations are deprecated and have no effect anymore, the configuration no needs to be done as defaults
in the @Route
annotation.
Before:
/**
* @Route("/my-route", name="my.route", methods={"GET"})
* @NoStore
* @HttpCache(maxage="3600", states={"cart.filled"})
* @Entity("product")
*/
public function myRoute(): Response
{
// ...
}
After:
/**
* @Route("/my-route", name="my.route", methods={"GET"}, defaults={"_noStore"=true, "_httpCache"={"maxage"="3600", "states"={"cart.filled"}}, "_entity"="product"})
*/
public function myRoute(): Response
{
// ...
}
The \Shopware\Core\System\SalesChannel\Api\StructEncoder
now only encodes entity properties which are mapped in the entity definition. If you have custom code which relies on the encoder to encode properties which aren't mapped in the entity definition, you need to adjust your code to map these properties in the entity definition.
All type hints from EntityRepositoryInterface should be changed to EntityRepository, you can use rector for that.
We removed the EntityRepositoryInterface
& SalesChannelRepositoryInterface
classes and declared the EntityRepository
& SalesChannelRepository
as final.
Therefore, if you implemented an own repository class for your entities, you have to remove this now.
To modify the repository calls, you can use one of the following events:
BeforeDeleteEvent
: Allows an access point for before and after deleting the entityEntitySearchedEvent
: Allows access points to the criteria for search and search-idsPreWriteValidationEvent
/PostWriteValidationEvent
: Allows access points before and after the entity writtenSalesChannelProcessCriteriaEvent
: Allows access to the criteria before the entity is search within a sales channel scope
Additionally, you have to change your type hints from EntityRepositoryInterface
& SalesChannelRepositoryInterface
to EntityRepository
or SalesChannelRepository
:
Removed the following repository decorators:
MediaRepositoryDecorator
MediaThumbnailRepositoryDecorator
MediaFolderRepositoryDecorator
PaymentMethodRepositoryDecorator
If you used one of the repositories and type a hint against this specific classes,
you have to change your type hints to EntityRepository
:
Following, entity properties/methods have been removed:
product.blacklistIds
product.whitelistIds
seo_url.isValid
When you create a new shipping method, the default value for the active flag is false, i.e. the method is inactive after saving. Please provide the active value if you create shipping methods over the API.
- In the next major, the flow actions aren't executed over the symfony events anymore; we'll remove the dependence from
EventSubscriberInterface
inShopware\Core\Content\Flow\Dispatching\Action\FlowAction
. - In the next major, the flow actions aren't executed via symfony events anymore;
we'll remove the dependency from
EventSubscriberInterface
inShopware\Core\Content\Flow\Dispatching\Action\FlowAction
. That means, all the flow actions extendingFlowAction
get the "services" tag. - The flow builder will execute the actions when calling the
handleFlow
function directly, instead of dispatching an action event. - To get an action service in flow builder, we need to define the tag action service with an unique key, which should be an action name.
- The flow action data is stored in
StorableFlow $flow
, so you should use$flow->getStore('order_id')
or$flow->getData('order')
instead of$flowEvent->getOrder
.- Use
$flow->getStore($key)
if you want to get the data fromaware
interfaces. Example:order_id
inOrderAware
orcustomer_id
fromCustomerAware
. - Use
$flow->getData($key)
if you want to get the data from original events or additional data. Example:order
,customer
orcontactFormData
.
- Use
before
<service id="Shopware\Core\Content\Flow\Dispatching\Action\SendMailAction">
...
<tag name="flow.action"/>
</service>
class FlowExecutor
{
...
$this->dispatcher->dispatch($flowEvent, $actionname);
...
}
abstract class FlowAction implements EventSubscriberInterface
{
...
}
class SendMailAction extends FlowAction
{
...
public static function getSubscribedEvents()
{
return ['action.name' => 'handle'];
}
public function handle(FlowEvent $event)
{
...
$orderId = $event->getOrderId();
$contactFormData = $event->getConta();
...
}
}
after
<service id="Shopware\Core\Content\Flow\Dispatching\Action\SendMailAction">
...
<tag name="flow.action" key="action.mail.send" />
</service>
class FlowExecutor
{
...
$actionService = $actions[$actionName];
$actionService->handleFlow($storableFlow);
...
}
abstract class FlowAction
{
...
}
class SendMailAction extends FlowAction
{
...
// The `getSubscribedEvents` function has been removed.
public function handleFlow(StorableFlow $flow)
{
...
$orderId = $flow->getStore('order_id');
$contactFormData = $event->getData('contactFormData');
...
}
}
- Deprecated fixed address formatting, use
@Framework/snippets/render.html.twig
instead, applied on:src/Storefront/Resources/views/storefront/component/address/address.html.twig
src/Core/Framework/Resources/views/documents/delivery_note.html.twig
src/Core/Framework/Resources/views/documents/includes/letter_header.html.twig
The dependency on the "marc1706/fast-image-size" library was removed, requires the library yourself if you need it.
The CheapestPrice will only be resolved in SalesChannelContext, thus it moved from the basic ProductEntity
to the SalesChannelProductEntity
.
If you rely on the CheapestPrice props of the ProductEntity in your plugin, make sure that you're in a SalesChannelContext and use the sales_channel.product.repository
instead of the product.repository
private EntityRepositoryInterface $productRepository;
public function custom(SalesChannelContext $context): void
{
$products = $this->productRepository->search(new Criteria(), $context->getContext());
/** @var ProductEntity $product */
foreach ($products as $product) {
$cheapestPrice = $product->getCheapestPrice();
// do stuff with $cheapestPrice
}
}
private SalesChannelRepositoryInterface $salesChannelProductRepository;
public function custom(SalesChannelContext $context): void
{
$products = $this->salesChannelProductRepository->search(new Criteria(), $context);
/** @var SalesChannelProductEntity $product */
foreach ($products as $product) {
$cheapestPrice = $product->getCheapestPrice();
// do stuff with $cheapestPrice
}
}
You have to change the signature of your AbstractProductMaxPurchaseCalculator
implementation as follows:
// before
abstract public function calculate(SalesChannelProductEntity $product, SalesChannelContext $context): int;
// after
abstract public function calculate(Entity $product, SalesChannelContext $context): int;
You have to change the signature of your PropertyGroupSorter
implementation as follows:
// before
abstract public function sort(PropertyGroupOptionCollection $groupOptionCollection): PropertyGroupCollection;
// after
abstract public function sort(EntityCollection $options): PropertyGroupCollection;
Seo url generation will now only generate urls when the entity is also assigned to this sales channel.
To archive this \Shopware\Core\Content\Seo\SeoUrlRoute\SeoUrlRouteInterface::prepareCriteria
gets as second parameter the SalesChannelEntity which will be currently proceed, to filter the criteria for this scope.
To make your Plugin already compatible for next major version you can use ReflectionClass with an if condition to avoid interface issues
$criteria->addFilter(new EqualsFilter('visibilities.salesChannelId', $salesChannel->getId()));
use Shopware\Core\Content\Seo\SeoUrlRoute\SeoUrlRouteInterface;
if (($r = new ReflectionClass(SeoUrlRouteInterface::class)) && $r->hasMethod('prepareCriteria') && $r->getMethod('prepareCriteria')->getNumberOfRequiredParameters() === 2) {
class MyPluginRoute implements SeoUrlRouteInterface
{
public function getConfig(): SeoUrlRouteConfig
{
// your logic
}
public function prepareCriteria(Criteria $criteria, SalesChannelEntity $salesChannel): void
{
// your logic
}
public function getMapping(Entity $product, SalesChannelEntity $salesChannel): SeoUrlMapping
{
// your logic
}
}
} else {
class MyPluginRoute implements SeoUrlRouteInterface
{
public function getConfig(): SeoUrlRouteConfig
{
// your logic
}
public function prepareCriteria(Criteria $criteria): void
{
// your logic
}
public function getMapping(Entity $product, ?SalesChannelEntity $salesChannel): SeoUrlMapping
{
// your logic
}
}
}
The languageId of the SalesChannel inside the SalesChannelContext will not be overridden by the current Language of the context anymore.
So if you need the current language from the context use $salesChannelContext->getLanguageId()
instead of relying on the languageId of the SalesChannel.
$currentLanguageId = $salesChannelContext->getSalesChannel()->getLanguageId();
$currentLanguageId = $salesChannelContext->getLanguageId();
When calling the /store-api/context
route, you now get the core context information in the response.
Instead of using response.salesChannel.languageId
, please use response.context.languageIdChain[0]
now.
The protected method \Shopware\Core\Content\Seo\HreflangLoader::generateHreflangHome()
was removed, use \Shopware\Core\Content\Seo\HreflangLoader::load()
with route = 'frontend.home.page'
instead.
class CustomHrefLoader extends HreflangLoader
{
public function someFunction(SalesChannelContext $salesChannelContext)
{
return $this->generateHreflangHome($salesChannelContext);
}
}
class CustomHrefLoader extends HreflangLoader
{
public function someFunction(SalesChannelContext $salesChannelContext)
{
return $this->load(
new HreflangLoaderParameter('frontend.home.page', [], $salesChannelContext)
);
}
}
The platform doesn't rely on psalm
for static analysis anymore, but solely uses phpstan
for that purpose.
Therefore, the psalm
dev-dependency was removed.
If you used the dev-dependency from platform in your project, please install the psalm
package directly into your project.
If the double opt in feature for the customer registration is enabled the customer accounts will be set active by default starting from Shopware 6.6.0.0. The validation now only considers if the customer has the double opt in registration enabled, i.e. the database value customer.double_opt_in_registration
equals 1
and if there exists an double opt in date in customer.double_opt_in_confirm_date
.
Custom fields will now be removed from the cart for performance reasons. Add the to the allow list with CartBeforeSerializationEvent if you need them in cart.
By default, all messages which are dispatched via message queue, are handled synchronous. Before 6.5 we had a message queue decoration to change this default behavior to asynchronous. This decoration has now been removed. We provide a simple opportunity to restore the old behavior by implementing the AsyncMessageInterface
interface to dispatch message synchronous.
class EntityIndexingMessage implements AsyncMessageInterface
{
// ...
}
The addForwardTrigger()
, addBackwardTrigger()
and addTrigger()
methods of the MigrationStep
class were removed, use createTrigger()
instead.
Don't rely on the state of already executed migrations in your database triggers anymore!
Additionally, the @MIGRATION_{migration}_IS_ACTIVE
DB connection variables aren't set at kernel boot anymore.
We removed \Shopware\Core\Framework\Event\FlowEvent
, since Flow Actions aren't executed via symfony's event system anymore.
You should implement the handleFlow()
method in your FlowAction
and tag your actions as flow.action
.
All DB migration steps are now considered @internal
, as they never should be extended or adjusted afterward.
The /api/_action/database
endpoint was removed; this means the following routes aren't available anymore:
POST /api/_action/database/sync-migration
POST /api/_action/database/migrate
POST /api/_action/database/migrate-destructive
The migrations can't be executed over the API anymore. Database migrations should be only executed over the CLI.
- Move the schema described by your
@OpenApi
/@OA
annotations to json files. - New the openapi specification is now loaded from
$bundlePath/Resources/Schema/
. - For an examples look at
src/Core/Framework/Api/ApiDefinition/Generator/Schema
.
Removed class \Shopware\Core\Maintenance\System\Service\DatabaseInitializer
, use SetupDatabaseAdapter
instead.
Removed class \Shopware\Recovery\Common\Service\JwtCertificateService
, use JwtCertificateGenerator
instead.
Removal of \Shopware\Core\System\NumberRange\ValueGenerator\Pattern\ValueGeneratorPatternRegistry::getPatternResolver()
We removed the ValueGeneratorPatternRegistry::getPatternResolver()
method, please call the generatePattern()
method now directly.
Before:
$patternResolver = $this->valueGeneratorPatternRegistry->getPatternResolver($pattern);
if ($patternResolver) {
$generated .= $patternResolver->resolve($configuration, $patternArg, $preview);
} else {
$generated .= $patternPart;
}
After:
$generated .= $this->valueGeneratorPatternRegistry->generatePattern($pattern, $patternPart, $configuration, $patternArg, $preview);
We removed the Shopware\Core\System\NumberRange\ValueGenerator\Pattern\ValueGeneratorPatternInterface
.
If you've implemented a custom value pattern please use the abstract class Shopware\Core\System\NumberRange\ValueGenerator\Pattern\AbstractValueGenerator
.
class CustomPattern implements ValueGeneratorPatternInterface
{
public function resolve(NumberRangeEntity $configuration, ?array $args = null, ?bool $preview = false): string
{
return $this->createPattern($configuration->getId(), $configuration->getPattern());
}
public function getPatternId(): string
{
return 'custom';
}
}
After:
class CustomIncrementStorage extends AbstractValueGenerator
{
public function generate(array $config, ?array $args = null, ?bool $preview = false): string
{
return $this->createPattern($config['id'], $config['pattern']);
}
public function getPatternId(): string
{
return 'custom';
}
public function getDecorated(): self
{
return $this->decorated;
}
}
We removed \Shopware\Core\Framework\Adapter\Twig\EntityTemplateLoader::clearInternalCache()
, use reset()
instead.
We refactored the number range handling, to be faster and allow different storages to be used.
We removed the Shopware\Core\System\NumberRange\ValueGenerator\Pattern\IncrementStorage\IncrementStorageInterface
.
If you've implemented a custom increment storage please use the abstract class Shopware\Core\System\NumberRange\ValueGenerator\Pattern\IncrementStorage\AbstractIncrementStorage
.
Before:
class CustomIncrementStorage implements IncrementStorageInterface
{
public function pullState(\Shopware\Core\System\NumberRange\NumberRangeEntity $configuration): string
{
return $this->increment($configuration->getId(), $configuration->getPattern());
}
public function getNext(\Shopware\Core\System\NumberRange\NumberRangeEntity $configuration): string
{
return $this->get($configuration->getId(), $configuration->getPattern());
}
}
After:
class CustomIncrementStorage extends AbstractIncrementStorage
{
public function reserve(array $config): string
{
return $this->increment($config['id'], $config['pattern']);
}
public function preview(array $config): string
{
return $this->get($config['id'], $config['pattern']);
}
public function getDecorated(): self
{
return $this->decorated;
}
}
Due to a new and better profiling pattern we removed the following services:
\Shopware\Core\Profiling\Checkout\SalesChannelContextServiceProfiler
\Shopware\Core\Profiling\Entity\EntityAggregatorProfiler
\Shopware\Core\Profiling\Entity\EntitySearcherProfiler
\Shopware\Core\Profiling\Entity\EntityReaderProfiler
You can now use the Profiler::trace()
function to add custom traces directly from your services.
If the cart is empty the cart calculation will be skipped.
This means that all \Shopware\Core\Checkout\Cart\CartProcessorInterface
and \Shopware\Core\Checkout\Cart\CartDataCollectorInterface
will not be executed anymore if the cart is empty.
The ArrayEntity::getVars()
has been changed so that the data
property is no longer in the payload but applied to the root
level.
This change affects all entity definitions that don't have their own entity class defined.
The API routes shouldn't be affected, because they didn't work with an ArrayEntity before the change, so no before/after payload can be shown.
$entity = new ArrayEntity(['foo' => 'bar']);
assert($entity->getVars(), ['data' => ['foo' => 'bar'], 'foo' => 'bar']);
$entity = new ArrayEntity(['foo' => 'bar']);
assert($entity->getVars(), ['foo' => 'bar']);
The class StoreAppLifecycleService
has been marked as internal.
We also removed the StoreAppLifecycleService::getAppIdByName()
method.
We removed the ExtensionRequiresNewPrivilegesException
exception.
Will be replaced with the internal ExtensionUpdateRequiresConsentAffirmationException
exception to have a more generic one.
The media_thumbnail.repository
had an own implementation of the EntityRepository
(MediaThumbnailRepositoryDecorator
) which breaks the nested primary key pattern for the delete
call and allowed you providing a flat id array. If you used the repository in this way, you have to change the usage as follows:
$repository->delete([$id1, $id2], $context);
$repository->delete([
['id' => $id1],
['id' => $id2]
], $context);
The class StringTemplateRenderer
should not be extended and will become final
.
Bootstrap v5 introduces breaking changes in HTML, (S)CSS and JavaScript. Below you can find a migration overview of the effected areas in the Shopware platform. Please consider that we can't provide code migration examples for every possible scenario of a UI-Framework like Bootstrap. You can find a full migration guide on the official Bootstrap website: Migrating to v5
The Update to Bootstrap v5 often contains the renaming of attributes and classes. Those need to be replaced. However, all Twig blocks remain untouched so all template extensions will take effect.
- Replace
data-toggle
withdata-bs-toggle
- Replace
data-dismiss
withdata-bs-dismiss
- Replace
data-target
withdata-bs-target
- Replace
data-offset
withdata-bs-offset
- Replace
custom-select
withform-select
- Replace
custom-file
withform-file
- Replace
custom-range
withform-range
- Replace
no-gutters
withg-0
- Replace
custom-control custom-checkbox
withform-check
- Replace
custom-control custom-switch
withform-check form-switch
- Replace
custom-control custom-radio
withform-check
- Replace
custom-control-input
withform-check-input
- Replace
custom-control-label
withform-check-label
- Replace
form-row
withrow g-2
- Replace
modal-close
withbtn-close
- Replace
sr-only
withvisually-hidden
- Replace
badge-*
withbg-*
- Replace
badge-pill
withrounded-pill
- Replace
close
withbtn-close
- Replace
left-*
andright-*
withstart-*
andend-*
- Replace
float-left
andfloat-right
withfloat-start
andfloat-end
. - Replace
border-left
andborder-right
withborder-start
andborder-end
. - Replace
rounded-left
androunded-right
withrounded-start
androunded-end
. - Replace
ml-*
andmr-*
withms-*
andme-*
. - Replace
pl-*
andpr-*
withps-*
andpe-*
. - Replace
text-left
andtext-right
withtext-start
andtext-end
.
<a href="#" class="btn btn-block">Default button</a>
<div class="d-grid">
<a href="#" class="btn">Default button</a>
</div>
<div class="input-group">
<input type="text" class="form-control">
<div class="input-group-append">
<button type="submit" class="btn">Submit</button>
</div>
</div>
<div class="input-group">
<input type="text" class="form-control">
<button type="submit" class="btn">Submit</button>
</div>
Please consider that the classes documented in "HTML/Twig" must also be replaced inside SCSS.
- Replace all mixin usages of
media-breakpoint-down()
with the current breakpoint, instead of the next breakpoint:- Replace
media-breakpoint-down(xs)
withmedia-breakpoint-down(sm)
- Replace
media-breakpoint-down(sm)
withmedia-breakpoint-down(md)
- Replace
media-breakpoint-down(md)
withmedia-breakpoint-down(lg)
- Replace
media-breakpoint-down(lg)
withmedia-breakpoint-down(xl)
- Replace
media-breakpoint-down(xl)
withmedia-breakpoint-down(xxl)
- Replace
- Replace
$custom-select-*
variable with$form-select-*
With the update to Bootstrap v5, the jQuery dependency will be removed from the shopware platform. We strongly recommend migrating jQuery implementations to Vanilla JavaScript.
// Previously Bootstrap plugins were initialized on jQuery elements
const collapse = DomAccess.querySelector('.collapse');
$(collapse).collapse('toggle');
// With Bootstrap v5 the Collapse plugin is instantiated and takes a native HTML element as a parameter
const collapse = DomAccess.querySelector('.collapse');
new bootstrap.Collapse(collapse, {
toggle: true,
});
// Previously Bootstrap events were subscribed using the jQuery `on()` method
const collapse = DomAccess.querySelector('.collapse');
$(collapse).on('show.bs.collapse', this._myMethod.bind(this));
$(collapse).on('hide.bs.collapse', this._myMethod.bind(this));
// With Bootstrap v5 a native event listener is being used
const collapse = DomAccess.querySelector('.collapse');
collapse.addEventListener('show.bs.collapse', this._myMethod.bind(this));
collapse.addEventListener('hide.bs.collapse', this._myMethod.bind(this));
In case you still need jQuery, you can add it to your own app or theme. This is the recommended method for all apps/themes which don't have control over the Shopware environment in which they're running in.
- Extend the file
platform/src/Storefront/Resources/views/storefront/layout/meta.html.twig
. - Use the block
layout_head_javascript_jquery
to add a<script>
tag containing jQuery. Only use this block to add jQuery. - This block is not deprecated and can be used in the long term beyond the next major version of shopware.
- Don't** use the
{{ parent() }}
call. This prevents multiple usages of jQuery. Even if multiple other plugins/apps use this method, the jQuery script will only be added once. - Please use jQuery version
3.5.1
(slim minified) to avoid compatibility issues between different plugins/apps. - If you don't want to use a CDN for jQuery, download jQuery from the official website (jQuery Core 3.5.1 - slim minified) and add it to
MyExtension/src/Resources/public/assets/jquery-3.5.1.slim.min.js
- After executing
bin/console asset:install
, you can reference the file using theassset()
function. See also: https://developer.shopware.com/docs/guides/plugins/plugins/storefront/add-custom-assets
{% sw_extends '@Storefront/storefront/layout/meta.html.twig' %}
{% block layout_head_javascript_jquery %}
<script src="{{ asset('bundles/myextension/assets/jquery-3.5.1.slim.min.js', 'asset') }}"></script>
{% endblock %}
Attention: If you need to test jQuery prior to the next major version, you must use the block base_script_jquery
inside platform/src/Storefront/Resources/views/storefront/base.html.twig
, instead.
The block base_script_jquery
will be moved to layout/meta.html.twig
with the next major version. However, the purpose of the block remains the same:
{% sw_extends '@Storefront/storefront/base.html.twig' %}
{% block base_script_jquery %}
<script src="{{ asset('bundles/myextension/assets/jquery-3.5.1.slim.min.js', 'asset') }}"></script>
{% endblock %}
- The function
translatedTypes
insrc/app/component/rule/sw-condition-type-select/index.js
is removed. UsetranslatedLabel
property of conditions.
With the major version 6.5, we've updated to webpack v5 and Bootstrap to v5. Because of these changes to the JavaScript bundling and vendor libraries, previously bundled JavaScript which was created with Shopware 6.4.x is not compatible with Shopware 6.5.
Please re-build your bundled JavaScript inside <YourPlugin>/src/Resources/app/storefront/dist/storefront/js/<your-plugin>.js
using bin/build-storefront.sh
We removed the CSRF protection in favor of SameSite strategy which is already implemented in shopware6.
If you changed or added forms with csrf protection, you have to remove all calls to the twig function sw_csrf
and every input (hidden) field which holds the csrf token.
You can no longer use the JavaScript properties window.csrf
or window.storeApiProxyToken
.
The Route to frontend.csrf.generateToken
will no longer work.
You don't have to implement any additional post request protection, as the SameSite strategy is already in place.
Increased Node version to 18 and NPM to version 8 or 9.
The store-api
proxy route was removed. Please use the store-api directly.
If that is not possible use a custom controller, that calls the StoreApiRoute
internally.
The StoreApiClient
class from storefront/src/service/store-api-client.service.js
was also removed, as that class relied on the proxy route.
To access the cart via storefront javascript, you can use the /checkout/cart.json
route.
In previous Shopware versions the theme assets has been copied to both folders bundles/[theme-name]/file.png
and theme/[id]/file.png
.
This was needed to be able to link the asset in the Storefront as the theme asset doesn't include the theme path prefix.
To improve the performance of theme:compile
and to reduce the confusion of the usage of assets we copy the files only to theme/[id]
.
To use the updated asset package,
replace your current {{ asset('logo.png', '@ThemeName') }}
with {{ asset('logo.png', 'theme') }}
We moved the event ThemeCompilerEnrichScssVariablesEvent
from \Shopware\Storefront\Event\ThemeCompilerEnrichScssVariablesEvent
to \Shopware\Storefront\Theme\Event\ThemeCompilerEnrichScssVariablesEvent
.
Please use the new event now.
All base_body_script
child blocks and their <script>
tags are moved from Resources/views/storefront/base.html.twig
to Resources/views/storefront/layout/meta.html.twig
. The block base_body_script
itself remains in the base.html.twig
template to offer the option to inject scripts before the </body>
tag if desired.
The scripts got a defer
attribute to allow downloading the script file while the HTML document is still loading. The script execution happens after the document is parsed.
Example for a <script>
extension in the template:
{% sw_extends '@Storefront/storefront/base.html.twig' %}
{% block base_script_router %}
{{ parent() }}
<script type="text/javascript" src="extra-script.js"></script>
{% endblock %}
{% sw_extends '@Storefront/storefront/layout/meta.html.twig' %}
{% block layout_head_javascript_router %}
{{ parent() }}
<script type="text/javascript" src="extra-script.js"></script>
{% endblock %}
If you're extending line item templates inside the cart, OffCanvas or other areas, you need to use the line item base template Resources/views/storefront/component/line-item/line-item.html.twig
and extend from one of the template files inside the Resources/views/storefront/component/line-item/types/
directory.
For example, You extend the line item's information about product variants with additional content.
{# YourExtension/src/Resources/views/storefront/page/checkout/checkout-item.html.twig #}
{% sw_extends '@Storefront/storefront/page/checkout/checkout-item.html.twig' %}
{% block page_checkout_item_info_variant_characteristics %}
{{ parent() }}
<div>My extra content</div>
{% endblock %}
{# YourExtension/src/Resources/views/storefront/component/line-item/type/product.html.twig #}
{% sw_extends '@Storefront/storefront/component/line-item/type/product.html.twig' %}
{% block component_line_item_type_product_variant_characteristics %}
{{ parent() }}
<div>My extra content</div>
{% endblock %}
Since the new line-item.html.twig
is used throughout multiple areas, the template extension above will take effect for product line items
in all areas. Depending on your use case, you might want to restrict this to more specific areas. You have the possibility to check the
current displayMode
to determine if the line item is shown inside the OffCanvas for example. Previously, the OffCanvas line items had
an individual template. You can now use the same line-item.html.twig
template as for regular line items.
{# YourExtension/src/Resources/views/storefront/component/checkout/offcanvas-item.html.twig #}
{% sw_extends '@Storefront/storefront/component/checkout/offcanvas-item.html.twig' %}
{% block cart_item_variant_characteristics %}
{{ parent() }}
<div>My extra content</div>
{% endblock %}
{# YourExtension/src/Resources/views/storefront/component/line-item/type/product.html.twig #}
{% sw_extends '@Storefront/storefront/component/line-item/type/product.html.twig' %}
{% block component_line_item_type_product_variant_characteristics %}
{{ parent() }}
{# Only show content when line item is inside offcanvas #}
{% if displayMode === 'offcanvas' %}
<div>My extra content</div>
{% endif %}
{% endblock %}
You can narrow down this even more by checking for the controllerAction
and render your changes only in desired actions.
The dedicated confirm-item.html.twig
in the example below no longer exists. You can use line-item.html.twig
as well.
{# YourExtension/src/Resources/views/storefront/page/checkout/confirm/confirm-item.html.twig #}
{% sw_extends '@Storefront/storefront/page/checkout/confirm/confirm-item.html.twig' %}
{% block cart_item_variant_characteristics %}
{{ parent() }}
<div>My extra content</div>
{% endblock %}
{# YourExtension/src/Resources/views/storefront/component/line-item/type/product.html.twig #}
{% sw_extends '@Storefront/storefront/component/line-item/type/product.html.twig' %}
{% block component_line_item_type_product_variant_characteristics %}
{{ parent() }}
{# Only show content on the confirm page #}
{% if controllerAction === 'confirmPage' %}
<div>My extra content</div>
{% endif %}
{% endblock %}
To allow atomic theme compilations, a seeding mechanism for AbstractThemePathBuilder
was added.
Whenever a theme is compiled, a new seed is generated and passed to the generateNewPath()
method, to generate a new theme path with that seed.
After the theme was compiled successfully the saveSeed()
method is called to that seed, after that subsequent calls to the assemblePath()
method, must use the newly saved seed for the path generation.
Additionally, the default implementation for \Shopware\Storefront\Theme\AbstractThemePathBuilder
was changed from \Shopware\Storefront\Theme\MD5ThemePathBuilder
to \Shopware\Storefront\Theme\SeedingThemePathBuilder
.
Obsolete compiled theme files are now deleted with a delay, whenever a new theme compilation created new files.
The delay time can be configured in the shopware.yaml
file with the new storefront.theme.file_delete_delay
option, by default it is set to 900 seconds (15 min), if the old theme files should be deleted immediately you can set the value to 0.
For more details refer to the corresponding ADR.
The JavaScript plugin AjaxModal
is able to open a Bootstrap modal and fetching content via ajax.
This is currently done by using the known Bootstrap selector for opening modals [data-bs-toggle="modal"]
and an additional [data-url]
.
The corresponding modal HTML isn't existing upfront and will be created by AjaxModal
internally by using the .js-pseudo-modal-template
template.
However, Bootstrap v5 needs a data-bs-target="*"
upfront which points to the desired HTML of a modal. Otherwise, it throws a JavaScript error because the Modal's DOM can't be found.
The AjaxModal
itself works regardless of the error.
Because we don't want to enforce to have an additional data-bs-target="*"
selector everywhere and want to keep the general workflow using AjaxModal
, we change the
selector, which is initializing the AjaxModal
plugin, to [data-ajax-modal][data-url]
to not interfere with the Bootstrap default modal.
AjaxModal
will do all modal related tasks programmatically and doesn't rely on Bootstraps data-attribute API.
<a data-bs-toggle="modal" data-url="/my/route" href="/my/route">Open Ajax Modal</a>
<a data-ajax-modal="true" data-url="/my/route" href="/my/route">Open Ajax Modal</a>
The route /widgets/checkout/info
will now return an empty response with HTTP status code 204 - No Content
, as long as the cart is empty, instead of loading the page and responding with a rendered template.
If you call that route manually in your extensions, please ensure to handle the 204
status code correctly.
Additionally, as the whole info widget pagelet will not be loaded anymore for empty carts, your event subscriber or app scripts for that page also won't be executed anymore for empty carts.
The OffCanvas module of the Storefront (src/plugin/offcanvas/ajax-offcanvas.plugin
) was changed to use the Bootstrap v5 OffCanvas component in the background.
If you pass a string of HTML manually to method OffCanvas.open()
, you need to adjust your markup according to Bootstrap v5 in order to display the close button and content/body.
See: https://getbootstrap.com/docs/5.1/components/offcanvas/
const offCanvasContent = `
<button class="btn btn-light offcanvas-close js-offcanvas-close btn-block sticky-top">
Close
</button>
<div class="offcanvas-content-container">
Content
</div>
`;
OffCanvas.open(offCanvasContent);
// OffCanvas now needs additional `offcanvas-header`
// Content class `offcanvas-content-container` is now `offcanvas-body`
const offCanvasContent = `
<div class="offcanvas-header p-0">
<button class="btn btn-light offcanvas-close js-offcanvas-close btn-block sticky-top">
Close
</button>
</div>
<div class="offcanvas-body">
Content
</div>
`;
// No need for changes in general usage!
OffCanvas.open(offCanvasContent);
As for now, we've prefixed your app's module label with the app name to build navigation entries. From 6.5 on, this prefixing will be removed.
const entry = {
id: `app-${app.name}-${appModule.name}`,
label: {
translated: true,
- label: `${appLabel} - ${moduleLabel}`,
+ label: moduleLabel,
},
position: appModule.position,
parent: appModule.parent,
privilege: `app.${app.name}`,
};
Example: Your App - Module Label
will become Module Label
in Shopware's Administration menu.
Important: Please update your module label in your app's manifest.xml
so it's clearly identifiable by your users.
Keep in mind that using a generic label could lead to cases where multiple apps use the same or similar module labels.
If your plugin provides 3rd party dependencies, override the executeComposerCommands
method in your plugin base class
and return true.
Now on plugin installation and update of the plugin a composer require
of your plugin will also be executed,
which installs your dependencies to the root vendor directory of Shopware.
On plugin uninstallation a composer remove
of your plugin will be executed,
which will also remove all your dependencies.
If you ship dependencies with your plugins within the plugin ZIP file, you should now consider using this config instead.
With the upcoming major release, we're going to release a new XML-schema for Shopware Apps. In the new schema we remove two deprecations from the existing schema.
-
attribute
parent
for elementmodule
will be required.Please make sure that every of your admin modules has this attribute set like described in our documentation
-
attribute
openNewTab
for elementaction-button
will be removed.Make sure to remove the attribute
openNewTab
from youraction-button
elements in yourmanifest.xml
and use ActionButtonResponses as described in our documentation instead. -
Deprecation of
manifest-1.0.xsd
Update the
xsi:noNamespaceSchemaLocation
attribute of yourmanifest
root element tohttps://raw.githubusercontent.com/shopware/platform/trunk/src/Core/Framework/App/Manifest/Schema/manifest-2.0.xsd