Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions RELEASE_INFO-6.7.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@

Fixed an undefined array key warning within the webhook handling, which could lead to a server error, if strict error displaying is set up.

### Digital product legacy states repair after update

We fixed a bug in the indexer for the `product.states` field, which lead to issues where rules (and flows depending on those rules) with the `line item with product state` condition did not work as expected. This especially affected the flows to deliver digital download products after purchase.

This release repairs digital products with missing legacy `states` via a one-time `UpdatePostFinishEvent` subscriber.

The repair runs automatically once per installation and is marked as completed in `app_config`.

# 6.7.8.1

## Critical Fixes
Expand Down
10 changes: 10 additions & 0 deletions UPGRADE-6.7.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
# 6.7.8.2

## Digital product legacy states repair after update

We fixed a bug in the indexer for the `product.states` field, which lead to issues where rules (and flows depending on those rules) with the `line item with product state` condition did not work as expected. This especially affected the flows to deliver digital download products after purchase.

This release repairs digital products with missing legacy `states` via a one-time `UpdatePostFinishEvent` subscriber.

The repair runs automatically once per installation and is marked as completed in `app_config`.

# 6.7.6.0

## Deprecation of video blocks
Expand Down
9 changes: 9 additions & 0 deletions src/Core/Content/DependencyInjection/product.xml
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,15 @@
<tag name="kernel.event_subscriber"/>
</service>

<service id="Shopware\Core\Content\Product\Subscriber\RepairDigitalProductStatesSubscriber">
<argument type="service" id="Doctrine\DBAL\Connection"/>
<argument type="service" id="Shopware\Core\Content\Product\DataAbstractionLayer\StatesUpdater" on-invalid="null"/>
<argument type="service" id="Shopware\Core\Framework\Adapter\Storage\AbstractKeyValueStorage"/>
<argument type="service" id="logger"/>

<tag name="kernel.event_subscriber"/>
</service>

<service id="Shopware\Core\Content\Product\Subscriber\ProductSubscriber">
<argument type="service" id="Shopware\Core\Content\Product\ProductVariationBuilder"/>
<argument type="service" id="Shopware\Core\Content\Product\SalesChannel\Price\ProductPriceCalculator"/>
Expand Down
10 changes: 7 additions & 3 deletions src/Core/Content/Product/DataAbstractionLayer/StatesUpdater.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,18 +44,17 @@ public function update(array $ids, Context $context): void

$sql = 'SELECT LOWER(HEX(`product`.`id`)) as id,
IF(`product_download`.`id` IS NOT NULL, 1, 0) as hasDownloads,
`product`.`type`,
`product`.`states`
FROM `product`
LEFT JOIN `product_download` ON `product`.`id` = `product_download`.`product_id`
AND `product`.`version_id` = `product_download`.`product_version_id`
WHERE `product`.`id` IN (:ids)
AND `type` = :currentType
AND `product`.`version_id` = :versionId
GROUP BY `product`.`id`';

$params = [
'ids' => Uuid::fromHexToBytesList($ids),
'currentType' => ProductDefinition::TYPE_PHYSICAL,
'versionId' => Uuid::fromHexToBytes($context->getVersionId()),
];

Expand All @@ -67,6 +66,11 @@ public function update(array $ids, Context $context): void

$updates = [];
foreach ($products as $product) {
// Only update states for physical and digital products, as the states are only relevant for these types. For other types, the states will be left unchanged.
if (isset($product['type']) && $product['type'] !== ProductDefinition::TYPE_PHYSICAL && $product['type'] !== ProductDefinition::TYPE_DIGITAL) {
continue;
}

$newStates = $this->getNewStates($product);
$oldStates = $product['states'] ? json_decode((string) $product['states'], true, 512, \JSON_THROW_ON_ERROR) : [];

Expand Down Expand Up @@ -110,7 +114,7 @@ private function getNewStates(array $product): array
{
$states = [];

if ((int) $product['hasDownloads'] === 1) {
if ($product['type'] === ProductDefinition::TYPE_DIGITAL || (int) $product['hasDownloads'] === 1) {
$states[] = State::IS_DOWNLOAD;
} else {
$states[] = State::IS_PHYSICAL;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
<?php declare(strict_types=1);

namespace Shopware\Core\Content\Product\Subscriber;

use Doctrine\DBAL\Connection;
use Doctrine\DBAL\ParameterType;
use Psr\Log\LoggerInterface;
use Shopware\Core\Content\Product\DataAbstractionLayer\StatesUpdater;
use Shopware\Core\Content\Product\ProductDefinition;
use Shopware\Core\Defaults;
use Shopware\Core\Framework\Adapter\Storage\AbstractKeyValueStorage;
use Shopware\Core\Framework\Log\Package;
use Shopware\Core\Framework\Update\Event\UpdatePostFinishEvent;
use Shopware\Core\Framework\Uuid\Uuid;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

/**
* @internal
*
* @deprecated tag:v6.8.0 - reason:remove-subscriber - temporary one-time digital product states regression
*/
#[Package('inventory')]
final readonly class RepairDigitalProductStatesSubscriber implements EventSubscriberInterface
{
/**
* @deprecated tag:v6.8.0 - reason:remove-subscriber - temporary one-time repair marker
*/
public const REPAIRED_DIGITAL_PRODUCT_STATES = 'core.repaired_digital_product_states';

private const BATCH_SIZE = 500;

/**
* @internal
*/
public function __construct(
private Connection $connection,
private ?StatesUpdater $statesUpdater,
private AbstractKeyValueStorage $storage,
private LoggerInterface $logger,
) {
}

/**
* @return array<class-string, string>
*/
public static function getSubscribedEvents(): array
{
return [
UpdatePostFinishEvent::class => 'repair',
];
}

/**
* @deprecated tag:v6.8.0 - reason:remove-subscriber - temporary one-time repair for 6.7.8.0 digital product states regression
*/
public function repair(UpdatePostFinishEvent $event): void
{
if ($this->storage->has(self::REPAIRED_DIGITAL_PRODUCT_STATES)) {
return;
}

if ($this->statesUpdater === null) {
$this->storage->set(self::REPAIRED_DIGITAL_PRODUCT_STATES, '1');

return;
}

try {
$repaired = 0;
$previousChunk = null;

while (true) {
/** @var list<string> $ids */
$ids = $this->connection->fetchFirstColumn(
<<<'SQL'
SELECT LOWER(HEX(`id`)) AS id
FROM `product`
WHERE `version_id` = :versionId
AND `type` = :type
AND `states` IS NULL
LIMIT :limit
SQL,
[
'type' => ProductDefinition::TYPE_DIGITAL,
'limit' => self::BATCH_SIZE,
'versionId' => Uuid::fromHexToBytes(Defaults::LIVE_VERSION),
],
[
'limit' => ParameterType::INTEGER,
]
);

if ($ids === []) {
break;
}

if ($previousChunk === $ids) {
$this->logger->error('Aborting digital product states repair because the same product chunk was fetched repeatedly', [
'chunkSize' => \count($ids),
'productIds' => $ids,
]);

break;
}

$previousChunk = $ids;

$this->statesUpdater->update($ids, $event->getContext());
$repaired += \count($ids);

if (\count($ids) < self::BATCH_SIZE) {
break;
}
}

$this->storage->set(self::REPAIRED_DIGITAL_PRODUCT_STATES, '1');

if ($repaired > 0) {
$event->appendPostUpdateMessage(\sprintf(
'Repaired product states for %d digital product(s) with missing legacy states.',
$repaired
));
}
} catch (\Throwable $exception) {
// Must not break update flow.
$this->logger->error('Failed to repair digital product states', ['exception' => $exception]);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -403,7 +403,8 @@ private function addProducts(Cart $cart, array $productDownloads): Cart
->price(1.0)
->tax('t1')
->visibility()
->add('downloads', array_map(function (string $file): array {
->type($downloadFiles === [] ? ProductDefinition::TYPE_PHYSICAL : ProductDefinition::TYPE_DIGITAL)
->add('downloads', array_map(static function (string $file): array {
[$fileName, $fileExtension] = explode('.', $file);

return [
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
<?php declare(strict_types=1);

namespace Shopware\Tests\Unit\Core\Content\Product\Subscriber;

use Doctrine\DBAL\Connection;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
use Shopware\Core\Content\Product\DataAbstractionLayer\StatesUpdater;
use Shopware\Core\Content\Product\Subscriber\RepairDigitalProductStatesSubscriber;
use Shopware\Core\Framework\Api\Context\SystemSource;
use Shopware\Core\Framework\Context;
use Shopware\Core\Framework\Update\Event\UpdatePostFinishEvent;
use Shopware\Core\Test\Stub\Framework\Adapter\Storage\ArrayKeyValueStorage;

/**
* @internal
*/
#[CoversClass(RepairDigitalProductStatesSubscriber::class)]
class RepairDigitalProductStatesSubscriberTest extends TestCase
{
public function testRepairReturnsEarlyWhenAlreadyMarked(): void
{
$storage = new ArrayKeyValueStorage([
RepairDigitalProductStatesSubscriber::REPAIRED_DIGITAL_PRODUCT_STATES => '1',
]);

$connection = $this->createMock(Connection::class);
$connection->expects($this->never())->method('fetchFirstColumn');

$statesUpdater = $this->createMock(StatesUpdater::class);
$statesUpdater->expects($this->never())->method('update');

$subscriber = new RepairDigitalProductStatesSubscriber(
$connection,
$statesUpdater,
$storage,
$this->createMock(LoggerInterface::class),
);

$subscriber->repair($this->createEvent());

static::assertTrue($storage->has(RepairDigitalProductStatesSubscriber::REPAIRED_DIGITAL_PRODUCT_STATES));
static::assertSame('1', $storage->get(RepairDigitalProductStatesSubscriber::REPAIRED_DIGITAL_PRODUCT_STATES));
}

public function testRepairUpdatesProductsAndSetsMarker(): void
{
$ids = ['a', 'b'];
$event = $this->createEvent();

$storage = new ArrayKeyValueStorage();

$connection = $this->createMock(Connection::class);
$connection->expects($this->once())
->method('fetchFirstColumn')
->willReturn($ids);

$statesUpdater = $this->createMock(StatesUpdater::class);
$statesUpdater->expects($this->once())
->method('update')
->with($ids, $event->getContext());

$logger = $this->createMock(LoggerInterface::class);
$logger->expects($this->never())->method('error');

$subscriber = new RepairDigitalProductStatesSubscriber(
$connection,
$statesUpdater,
$storage,
$logger,
);

$subscriber->repair($event);

static::assertStringContainsString('Repaired product states for 2 digital product(s)', $event->getPostUpdateMessage());
static::assertTrue($storage->has(RepairDigitalProductStatesSubscriber::REPAIRED_DIGITAL_PRODUCT_STATES));
static::assertSame('1', $storage->get(RepairDigitalProductStatesSubscriber::REPAIRED_DIGITAL_PRODUCT_STATES));
}

public function testRepairAbortsWhenSameChunkIsFetchedTwice(): void
{
$ids = array_map(static fn (int $i): string => 'id-' . $i, range(1, 500));
$event = $this->createEvent();

$storage = new ArrayKeyValueStorage();

$connection = $this->createMock(Connection::class);
$connection->expects($this->exactly(2))
->method('fetchFirstColumn')
->willReturnOnConsecutiveCalls($ids, $ids);

$statesUpdater = $this->createMock(StatesUpdater::class);
$statesUpdater->expects($this->once())
->method('update')
->with($ids, $event->getContext());

$logger = $this->createMock(LoggerInterface::class);
$logger->expects($this->once())
->method('error')
->with(
static::stringContains('same product chunk was fetched repeatedly'),
static::arrayHasKey('productIds')
);

$subscriber = new RepairDigitalProductStatesSubscriber(
$connection,
$statesUpdater,
$storage,
$logger,
);

$subscriber->repair($event);

static::assertStringContainsString('Repaired product states for 500 digital product(s)', $event->getPostUpdateMessage());
static::assertTrue($storage->has(RepairDigitalProductStatesSubscriber::REPAIRED_DIGITAL_PRODUCT_STATES));
static::assertSame('1', $storage->get(RepairDigitalProductStatesSubscriber::REPAIRED_DIGITAL_PRODUCT_STATES));
}

private function createEvent(): UpdatePostFinishEvent
{
return new UpdatePostFinishEvent(new Context(new SystemSource()), '6.7.8.0', '6.7.8.1');
}
}
Loading