title | issue |
---|---|
Refactor stock management |
NEXT-12479 |
- Changed
Shopware\Core\Checkout\Cart\Order\RecalculationService
to roll back theorderStateId
after order conversion. - Deprecated
Shopware\Core\Content\Product\DataAbstractionLayer\StockUpdater
. It will be removed in 6.6. Furthermore, if you opt into the new stock handling method with theSTOCK_HANDLING
feature flag, the subscriber will no longer execute. - Added
\Shopware\Core\Content\Product\Stock\AbstractStockStorage
to handle stock alterations and stock loading. - Added
\Shopware\Core\Content\Product\Stock\StockStorage
as the default implementation of\Shopware\Core\Content\Product\Stock\AbstractStockStorage
. It persists stock alterations to the product table, using theproduct.stock
field. - Added
\Shopware\Core\Content\Product\SalesChannel\Detail\AbstractAvailableCombinationLoader::loadCombinations()
with a default implementation which proxies toload
. It must be implemented in 6.6, as theload
method will be removed. The signature is updated tostring $productId, SalesChannelContext $salesChannelContext
instead ofstring $productId, Context $context, string $salesChannelId
. - Deprecated
\Shopware\Core\Content\Product\SalesChannel\Detail\AbstractAvailableCombinationLoader::load()
. It will be removed in 6.6. UseloadCombinations
instead. - Deprecated
\Shopware\Core\Content\Product\SalesChannel\Detail\AvailableCombinationLoader::load()
. It will be removed in 6.6. UseloadCombinations
instead. - Added
\Shopware\Core\Content\Product\SalesChannel\Detail\AvailableCombinationLoader::loadCombinations()
. It is the same asload
with the update method signature. - Added
\Shopware\Core\Content\Product\Stock\LoadProductStockSubscriber
to allow augmenting stock data when loading products via\Shopware\Core\Content\Product\Stock\AbstractStockStorage::load
. By decorating\Shopware\Core\Content\Product\Stock\AbstractStockStorage::load
you can easily load your stock data from a different source. - Added the feature flag
STOCK_HANDLING
to opt into the new stock handling implementation. - Deprecated writing to the
product.availableStock
field. It will become write protected in 6.6. There are no plans to remove it. Opt in early with theSTOCK_HANDLING
feature flag. - Changed the
product.stock
field to represent the realtime stock of a product. When a product is ordered, the stock value is instantly decremented. When an order is cancelled, the stock value is instantly incremented and so on. This will become the default behaviour in 6.6 but for now can be activated via theSTOCK_HANDLING
feature flag. - Added new configuration setting
stock.enable_stock_management
. The default value is true. - Added
\Shopware\Core\Content\Product\Stock\OrderStockSubscriber
. This subscriber is responsible for communicating with\Shopware\Core\Content\Product\Stock\AbstractStockStorage
and issuing stock alteration commands whenever an order is created or transitioned through the various states. The subscriber is only enabled if the configuration settingstock.enable_stock_management
is enabled AND theSTOCK_HANDLING
feature flag is enabled. This will be the default behaviour in 6.6. - Added
\Shopware\Core\Content\Product\Stock\AvailableStockMirrorSubscriber
. This subscriber is responsible for updating theproduct.availableStock
field whenever theproduct.stock
field is written. This mirror is useful in case you have some integration which relies on this field which cannot be updated. The subscriber is only enabled if theSTOCK_HANDLING
feature flag is enabled. This will be the default behaviour in 6.6. - Changed
\Shopware\Core\Content\Product\DataAbstractionLayer\ProductIndexer
. It now depends on\Shopware\Core\Content\Product\Stock\AbstractStockStorage
. If theSTOCK_HANDLING
feature flag is not enabled,Shopware\Core\Content\Product\DataAbstractionLayer\StockUpdater
is used to recalculate stock availability for the updated and indexed products. If it is not enabled,\Shopware\Core\Content\Product\Stock\AbstractStockStorage
is used to index the updated products, which means the stock availability is recalculated. - Added
\Shopware\Core\Framework\DataAbstractionLayer\Event\EntityWriteEvent
. This event allows you to hook in to the process of writing an entity. This includes, creating, updating and deleting entities. You have the possibility to execute code before and after the entity is written via the success and error callbacks. You can call theaddSuccess
oraddError
methods with any PHP callable. - Added
\Shopware\Core\Framework\DataAbstractionLayer\Event\EntityDeleteEvent
. This event allows you to hook in to the process of removing an entity. You have the possibility to execute code before and after the entity is removed via the success and error callbacks. You can call theaddSuccess
oraddError
methods with a Closure. - Deprecated
\Shopware\Core\Framework\DataAbstractionLayer\Event\BeforeDeleteEvent
. It will be removed in 6.6. Use\Shopware\Core\Framework\DataAbstractionLayer\Event\EntityDeleteEvent
instead, it has the same API. - Changed
\Shopware\Core\Framework\DataAbstractionLayer\Dbal\EntityWriteGateway
to dispatch the\Shopware\Core\Framework\DataAbstractionLayer\Event\EntityWriteEvent
event, whenever entities are written (created, updated, deleted) and execute the added success and error callbacks when the operation succeeds or fails, respectively.
The product.stock
field is now the primary source for real time product stock values. However, product.availableStock
is a direct mirror of product.stock
and is updated whenever product.stock
is updated via the DAL.
A database migration \Shopware\Core\Migration\V6_6\Migration1691662140MigrateAvailableStock
takes care of copying the available_stock
field to the stock
field.
stock.enable_stock_management
- Defaulttrue
. This can be used to completely disable Shopware's stock handling. If disabled, product stock will be not be updated as orders are created and transitioned through the various states.
The listener was replaced with a new listener \Shopware\Core\Content\Product\Stock\OrderStockSubscriber
which handles subscribing to the various order events and interfaces with the stock storage \Shopware\Core\Content\Product\Stock\AbstractStockStorage
to write the stock alterations.
Removed \Shopware\Core\Content\Product\SalesChannel\Detail\AbstractAvailableCombinationLoader::load()
&& \Shopware\Core\Content\Product\SalesChannel\Detail\AvailableCombinationLoader::load()
These methods are removed and superseded by loadCombinations
which has a different method signature.
From:
public function load(string $productId, Context $context, string $salesChannelId)
To:
public function loadCombinations(string $productId, SalesChannelContext $salesChannelContext): AvailableCombinationResult
The loadCombinations
method has been made abstract so it must be implemented. The SalesChannelContext
instance, contains the data which was previously in the defined on the load
method.
$salesChannelId
can be replaced with $salesChannelContext->getSalesChannel()->getId()
.
$context
can be replaced with $salesChannelContext->getContext()
.
The field is write protected. Use the product.stock
to write new stock levels.
The product.stock
should be used to read the current stock level. When building new extensions which need to query the stock of a product, use this field. Not the product.availableStock
field.
It is replaced by \Shopware\Core\Framework\DataAbstractionLayer\Event\EntityWriteEvent
with the same API.
You should use \Shopware\Core\Framework\DataAbstractionLayer\Event\EntityWriteEvent
instead, only the class name changed.
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',
];
}