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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
## Unreleased
### Add
- Add Recently viewed component
- Add worker command for product export - reduce memory usage and increase performance for big catalogs

### Change
- Upgrade Web Components version to v5.2.1
Expand Down
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,15 @@ This is especially useful if you have several SalesChannels with different domai

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

##### Running export with a worker

Since version 7.3.0, we've introduced a new export flow based on the worker.
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.
Currently, this command is only available from the CLI and can be executed by:

php [SHOPWARE_ROOT]/bin/console factfinder:data:worker-export

The options are the same as described above for `factfinder:data:export`

#### Selecting Categories for CMS Export
With CMS Export we introduced custom field for CategoryEntity by which we filter the Categories going to be exported.
Expand Down
114 changes: 114 additions & 0 deletions src/Command/ExportBatchCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
<?php

declare(strict_types=1);

namespace Omikron\FactFinder\Shopware6\Command;

use Omikron\FactFinder\Shopware6\Export\CurrencyFieldsProvider;
use Omikron\FactFinder\Shopware6\Export\Data\Entity\ProductEntity;
use Omikron\FactFinder\Shopware6\Export\FeedFactory;
use Omikron\FactFinder\Shopware6\Export\Field\FieldInterface;
use Omikron\FactFinder\Shopware6\Export\FieldsProvider;
use Omikron\FactFinder\Shopware6\Export\SalesChannelService;
use Omikron\FactFinder\Shopware6\Export\Stream\CsvFile;
use Shopware\Core\Framework\Api\Context\SystemSource;
use Shopware\Core\Framework\Context;
use Shopware\Core\Framework\DataAbstractionLayer\EntityRepository;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

/**
* @SuppressWarnings(PHPMD.UnusedPrivateMethod)
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
*/
#[AsCommand(name: 'factfinder:export:batch', description: 'Internal worker command for exporting a batch of products', hidden: true)]
class ExportBatchCommand extends Command
{
private const PRODUCTS_EXPORT_TYPE = 'products';

public function __construct(
private readonly SalesChannelService $channelService,
private readonly FeedFactory $feedFactory,
private readonly FieldsProvider $fieldProviders,
private readonly array $productsColumnsBase,
private readonly CurrencyFieldsProvider $currencyFieldsProvider,
private readonly EntityRepository $salesChannelRepository,
) {
parent::__construct();
}

protected function configure(): void
{
$this->addArgument('sales_channel', InputArgument::REQUIRED, 'ID of the sales channel');
$this->addArgument('language', InputArgument::REQUIRED, 'ID of the language');
$this->addArgument('offset', InputArgument::REQUIRED, 'Offset');
$this->addArgument('limit', InputArgument::REQUIRED, 'Limit');
$this->addArgument('file_path', InputArgument::REQUIRED, 'Path to output CSV');
}

protected function execute(InputInterface $input, OutputInterface $output): int
{
$offset = (int) $input->getArgument('offset');
$limit = (int) $input->getArgument('limit');
$filePath = $input->getArgument('file_path');
$salesChannelId = $input->getArgument('sales_channel');
$salesChannel = null;

if (!empty($salesChannelId)) {
$salesChannel = $this->salesChannelRepository->search(
new Criteria([$salesChannelId]),
new Context(new SystemSource())
)->first();
}

$context = $this->channelService->getSalesChannelContext(
$salesChannel,
$input->getArgument('language')
);

$entityClass = ProductEntity::class;
$feedService = $this->feedFactory->create($context, $entityClass);
$fileResource = fopen($filePath, 'a');
$out = new CsvFile($fileResource);
$feedColumns = $this->getFeedColumns('products', ProductEntity::class);
$processedCount = $feedService->generateBatch($out, $feedColumns, $offset, $limit, $offset === 0);

fclose($fileResource);

$memoryUsageMB = memory_get_usage(true) / 1024 / 1024;
$peakMemoryMB = memory_get_peak_usage(true) / 1024 / 1024;

$result = [
'count' => $processedCount,
'memory' => round($memoryUsageMB, 2),
'peak' => round($peakMemoryMB, 2),
];

$output->write(json_encode($result));

return Command::SUCCESS;
}

private function getFeedColumns(string $exportType, string $entityClass): array
{
$fields = $this->fieldProviders->getFields($entityClass);
return array_values(
array_unique(
array_merge(
$this->productsColumnsBase,
array_map([$this, 'getFieldName'], $fields),
$exportType === self::PRODUCTS_EXPORT_TYPE ? $this->currencyFieldsProvider->getCurrencyFields() : []
)
)
);
}

private function getFieldName(FieldInterface $field): string
{
return $field->getName();
}
}
Loading
Loading