Skip to content

Commit 9e3d792

Browse files
authored
INT-297: Introduce worker for product export
Add worker command for product export - reduce memory usage and increase performance for big catalogs
1 parent e2c721c commit 9e3d792

6 files changed

Lines changed: 481 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
## Unreleased
33
### Add
44
- Add Recently viewed component
5+
- Add worker command for product export - reduce memory usage and increase performance for big catalogs
56

67
### Change
78
- Upgrade Web Components version to v5.2.1

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,15 @@ This is especially useful if you have several SalesChannels with different domai
364364

365365
APP_URL=http://saleschannel-domain.com php [SHOPWARE_ROOT]/bin/console factfinder:data:export
366366

367+
##### Running export with a worker
368+
369+
Since version 7.3.0, we've introduced a new export flow based on the worker.
370+
The biggest advantage of this export method is the reduced memory usage, which is especially helpful when you have a large or complex products catalog.
371+
Currently, this command is only available from the CLI and can be executed by:
372+
373+
php [SHOPWARE_ROOT]/bin/console factfinder:data:worker-export
374+
375+
The options are the same as described above for `factfinder:data:export`
367376

368377
#### Selecting Categories for CMS Export
369378
With CMS Export we introduced custom field for CategoryEntity by which we filter the Categories going to be exported.

src/Command/ExportBatchCommand.php

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Omikron\FactFinder\Shopware6\Command;
6+
7+
use Omikron\FactFinder\Shopware6\Export\CurrencyFieldsProvider;
8+
use Omikron\FactFinder\Shopware6\Export\Data\Entity\ProductEntity;
9+
use Omikron\FactFinder\Shopware6\Export\FeedFactory;
10+
use Omikron\FactFinder\Shopware6\Export\Field\FieldInterface;
11+
use Omikron\FactFinder\Shopware6\Export\FieldsProvider;
12+
use Omikron\FactFinder\Shopware6\Export\SalesChannelService;
13+
use Omikron\FactFinder\Shopware6\Export\Stream\CsvFile;
14+
use Shopware\Core\Framework\Api\Context\SystemSource;
15+
use Shopware\Core\Framework\Context;
16+
use Shopware\Core\Framework\DataAbstractionLayer\EntityRepository;
17+
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
18+
use Symfony\Component\Console\Attribute\AsCommand;
19+
use Symfony\Component\Console\Command\Command;
20+
use Symfony\Component\Console\Input\InputArgument;
21+
use Symfony\Component\Console\Input\InputInterface;
22+
use Symfony\Component\Console\Output\OutputInterface;
23+
24+
/**
25+
* @SuppressWarnings(PHPMD.UnusedPrivateMethod)
26+
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
27+
*/
28+
#[AsCommand(name: 'factfinder:export:batch', description: 'Internal worker command for exporting a batch of products', hidden: true)]
29+
class ExportBatchCommand extends Command
30+
{
31+
private const PRODUCTS_EXPORT_TYPE = 'products';
32+
33+
public function __construct(
34+
private readonly SalesChannelService $channelService,
35+
private readonly FeedFactory $feedFactory,
36+
private readonly FieldsProvider $fieldProviders,
37+
private readonly array $productsColumnsBase,
38+
private readonly CurrencyFieldsProvider $currencyFieldsProvider,
39+
private readonly EntityRepository $salesChannelRepository,
40+
) {
41+
parent::__construct();
42+
}
43+
44+
protected function configure(): void
45+
{
46+
$this->addArgument('sales_channel', InputArgument::REQUIRED, 'ID of the sales channel');
47+
$this->addArgument('language', InputArgument::REQUIRED, 'ID of the language');
48+
$this->addArgument('offset', InputArgument::REQUIRED, 'Offset');
49+
$this->addArgument('limit', InputArgument::REQUIRED, 'Limit');
50+
$this->addArgument('file_path', InputArgument::REQUIRED, 'Path to output CSV');
51+
}
52+
53+
protected function execute(InputInterface $input, OutputInterface $output): int
54+
{
55+
$offset = (int) $input->getArgument('offset');
56+
$limit = (int) $input->getArgument('limit');
57+
$filePath = $input->getArgument('file_path');
58+
$salesChannelId = $input->getArgument('sales_channel');
59+
$salesChannel = null;
60+
61+
if (!empty($salesChannelId)) {
62+
$salesChannel = $this->salesChannelRepository->search(
63+
new Criteria([$salesChannelId]),
64+
new Context(new SystemSource())
65+
)->first();
66+
}
67+
68+
$context = $this->channelService->getSalesChannelContext(
69+
$salesChannel,
70+
$input->getArgument('language')
71+
);
72+
73+
$entityClass = ProductEntity::class;
74+
$feedService = $this->feedFactory->create($context, $entityClass);
75+
$fileResource = fopen($filePath, 'a');
76+
$out = new CsvFile($fileResource);
77+
$feedColumns = $this->getFeedColumns('products', ProductEntity::class);
78+
$processedCount = $feedService->generateBatch($out, $feedColumns, $offset, $limit, $offset === 0);
79+
80+
fclose($fileResource);
81+
82+
$memoryUsageMB = memory_get_usage(true) / 1024 / 1024;
83+
$peakMemoryMB = memory_get_peak_usage(true) / 1024 / 1024;
84+
85+
$result = [
86+
'count' => $processedCount,
87+
'memory' => round($memoryUsageMB, 2),
88+
'peak' => round($peakMemoryMB, 2),
89+
];
90+
91+
$output->write(json_encode($result));
92+
93+
return Command::SUCCESS;
94+
}
95+
96+
private function getFeedColumns(string $exportType, string $entityClass): array
97+
{
98+
$fields = $this->fieldProviders->getFields($entityClass);
99+
return array_values(
100+
array_unique(
101+
array_merge(
102+
$this->productsColumnsBase,
103+
array_map([$this, 'getFieldName'], $fields),
104+
$exportType === self::PRODUCTS_EXPORT_TYPE ? $this->currencyFieldsProvider->getCurrencyFields() : []
105+
)
106+
)
107+
);
108+
}
109+
110+
private function getFieldName(FieldInterface $field): string
111+
{
112+
return $field->getName();
113+
}
114+
}

0 commit comments

Comments
 (0)