Zur Generierung von 1 Million Artikeln habe ich ein eigenes Plugin geschrieben.
Vielleicht mache ich dabei etwas falsch – eventuell werden unnötige oder inkonsistente Daten erzeugt?
/src/custom/plugins/NasadImportProducts/Command/ImportProductsCommand.php
<?php declare(strict_types=1);
namespace NasadImportProducts\Command;
use NasadImportProducts\Core\Content\Product\ProductImportService;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Console\Input\InputOption;
class ImportProductsCommand extends Command
{
protected static string $defaultName = 'nasad:import:products';
public function __construct(
private readonly ProductImportService $productImportService
) {
parent::__construct(self::$defaultName);
}
protected function configure(): void
{
$this
->setName(self::$defaultName)
->setDescription('Import products with dimensions')
->addOption(
'count',
'c',
InputOption::VALUE_OPTIONAL,
'Number of products to import',
100
)
->addOption(
'offset',
'o',
InputOption::VALUE_OPTIONAL,
'Starting offset for product numbers',
0
);
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$count = (int)$input->getOption('count');
$offset = (int)$input->getOption('offset');
try {
$io->section(sprintf('Starting products import (Count: %d, Offset: %d)', $count, $offset));
// Importiere Produkte
$io->text('Importing products...');
$this->productImportService->importProducts($count, $offset);
$io->success(sprintf('%d products imported successfully', $count));
return Command::SUCCESS;
} catch (\Exception $e) {
$io->error('Error during import: ' . $e->getMessage());
return Command::FAILURE;
}
}
}
/src/custom/plugins/NasadImportProducts/Command/ImportCategoriesCommand.php
<?php declare(strict_types=1);
namespace NasadImportProducts\Command;
use NasadImportProducts\Core\Content\Category\CategoryImportService;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
class ImportCategoriesCommand extends Command
{
protected static string $defaultName = 'nasad:import:categories';
public function __construct(
private readonly CategoryImportService $categoryImportService
) {
parent::__construct(self::$defaultName);
}
protected function configure(): void
{
$this
->setName(self::$defaultName)
->setDescription('Import categories')
->addOption(
'count',
'c',
InputOption::VALUE_OPTIONAL,
'Number of categories to import',
100
);
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$count = (int) $input->getOption('count');
try {
$io->section(sprintf('Starting categories import (Count: %d)', $count));
$categoryIds = $this->categoryImportService->importCategories($count);
$io->success(sprintf('%d categories imported successfully', $count));
return Command::SUCCESS;
} catch (\Exception $e) {
$io->error('Error during import: ' . $e->getMessage());
return Command::FAILURE;
}
}
}
/src/custom/plugins/NasadImportProducts/Command/ImportAttributesCommand.php
<?php declare(strict_types=1);
namespace NasadImportProducts\Command;
use NasadImportProducts\Core\Service\AttributeService;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
class ImportAttributesCommand extends Command
{
protected static string $defaultName = 'nasad:import:attributes';
public function __construct(
private readonly AttributeService $attributeService
) {
parent::__construct(self::$defaultName);
}
protected function configure(): void
{
$this
->setName(self::$defaultName)
->setDescription('Import attributes');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
try {
$io->section('Starting attributes import');
$this->attributeService->createAttributes();
$io->success('Attributes created successfully');
return Command::SUCCESS;
} catch (\Exception $e) {
$io->error('Error during import: ' . $e->getMessage());
return Command::FAILURE;
}
}
}
/src/custom/plugins/NasadImportProducts/Core/Content/Category/CategoryImportService.php
<?php declare(strict_types=1);
namespace NasadImportProducts\Core\Content\Category;
use NasadImportProducts\Core\Service\DummyDataGenerator;
use Shopware\Core\Framework\Context;
use Shopware\Core\Framework\DataAbstractionLayer\EntityRepository;
use Shopware\Core\Framework\Uuid\Uuid;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
class CategoryImportService
{
private DummyDataGenerator $dataGenerator;
public function __construct(
private readonly EntityRepository $categoryRepository,
private readonly EntityRepository $salesChannelRepository
) {
$this->dataGenerator = new DummyDataGenerator();
}
public function importCategories(int $count = 100): array
{
$context = Context::createDefaultContext();
$categoryIds = [];
// Finde den Standard-Storefront-Sales-Channel
$criteria = new Criteria();
$salesChannel = $this->salesChannelRepository->search($criteria, $context)->first();
if (!$salesChannel) {
throw new \RuntimeException('Default sales channel not found');
}
// Hole die Navigations-Kategorie
$navigationCategoryId = $salesChannel->getNavigationCategoryId();
$criteria = new Criteria([$navigationCategoryId]);
$navigationCategory = $this->categoryRepository->search($criteria, $context)->first();
if (!$navigationCategory) {
throw new \RuntimeException('Navigation category not found');
}
// Begrenze die Anzahl auf mindestens 1 und maximal 100
$count = max(1, min(100, $count));
for ($i = 1; $i <= $count; $i++) {
$categoryId = Uuid::randomHex();
$categoryIds[] = $categoryId;
$category = [
'id' => $categoryId,
'parentId' => $navigationCategory->getId(),
'translations' => $this->getCategoryTranslations($i),
'displayNestedProducts' => true,
'active' => true,
'visible' => true
];
$this->categoryRepository->create([$category], $context);
}
return $categoryIds;
}
private function getCategoryTranslations(int $number): array
{
$translations = [];
$supportedLocales = ['de-DE', 'en-GB', 'fr-FR', 'nl-NL', 'it-IT', 'es-ES'];
foreach ($supportedLocales as $locale) {
$translations[$locale] = [
'name' => $this->dataGenerator->generateCategoryName($number, $locale),
'description' => $this->dataGenerator->generateCategoryDescription($locale),
'metaTitle' => $this->dataGenerator->generateMetaTitle(
$this->dataGenerator->generateCategoryName($number, $locale),
$locale
),
'metaDescription' => $this->dataGenerator->generateMetaDescription($locale)
];
}
return $translations;
}
public function deleteAllCategories(): void
{
$context = Context::createDefaultContext();
$categories = $this->categoryRepository->search(new Criteria(), $context);
if ($categories->getTotal() === 0) {
return;
}
$ids = array_map(function ($category) {
return ['id' => $category->getId()];
}, $categories->getElements());
$this->categoryRepository->delete(array_values($ids), $context);
}
}
/src/custom/plugins/NasadImportProducts/Core/Content/Product/ProductImportService.php
<?php declare(strict_types=1);
namespace NasadsImportProducts\Core\Content\Product;
use NasadImportProducts\Core\Service\DummyDataGenerator;
use Shopware\Core\Framework\Context;
use Shopware\Core\Framework\DataAbstractionLayer\EntityRepository;
use Shopware\Core\Framework\Uuid\Uuid;
use NasadImportProducts\Core\Service\AttributeService;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
use Shopware\Core\Framework\DataAbstractionLayer\Indexing\EntityIndexerRegistry;
class ProductImportService
{
private DummyDataGenerator $dataGenerator;
private array $attributeIds;
private array $categoryIds = [];
private array $propertyOptionIds = [];
public function __construct(
private readonly EntityRepository $productRepository,
private readonly EntityRepository $categoryRepository,
private readonly AttributeService $attributeService,
private readonly EntityRepository $propertyGroupOptionRepository
) {
$this->dataGenerator = new DummyDataGenerator();
}
/**
* Import products with random categories and attributes
*
* @param int $count Number of products to import (default: 100, max: 1000000)
* @return void
* @throws \RuntimeException when no categories are found
*/
public function importProducts_old(int $count = 100): void
{
$context = Context::createDefaultContext();
$products = [];
$batchSize = 1000;
// Lade alle verfügbaren Kategorien und Attribute
$this->loadCategories($context);
$this->loadAttributes();
// Begrenze die Anzahl auf mindestens 1 und maximal 1.000.000
$count = max(1, min(10, $count));
for ($i = 1; $i <= $count; $i++) {
$productId = Uuid::randomHex();
// Zufällige Dimensionen zwischen 1 und 10
$width = mt_rand(1, 10);
$height = mt_rand(1, 10);
$length = mt_rand(1, 10);
$products[] = [
'id' => $productId,
'productNumber' => 'PROD-' . $width . '-' . $height . '-' . $length . '-' . $i,
'stock' => 100,
'translations' => $this->getProductTranslations($width, $height, $length),
'price' => [['currencyId' => 'b7d2554b0ce847cd82f3ac9bd1c0dfca',
'gross' => (string)$this->dataGenerator->getRandomPrice(), // Explizit als String
'net' => (string)$this->dataGenerator->getRandomPrice(), // Explizit als String
'linked' => true]],
'tax' => ['name' => '19%', 'taxRate' => 19],
'categories' => $this->getRandomCategoryIds(),
'properties' => [
['id' => Uuid::randomHex(), 'groupId' => $this->attributeIds['width'], 'name' => $width . ' cm'],
['id' => Uuid::randomHex(), 'groupId' => $this->attributeIds['height'], 'name' => $height . ' cm'],
['id' => Uuid::randomHex(), 'groupId' => $this->attributeIds['length'], 'name' => $length . ' cm']
],
'active' => true
];
if (count($products) >= $batchSize) {
$this->productRepository->create($products, $context);
$products = [];
}
}
if (!empty($products)) {
$this->productRepository->create($products, $context);
}
}
private function loadOrCreatePropertyOptions(Context $context): void
{
// Cache für Property-Optionen erstellen
if (empty($this->propertyOptionIds)) {
$this->propertyOptionIds = [
'width' => [],
'height' => [],
'length' => []
];
// Für jede Dimension (1-10 cm) Property-Optionen laden oder erstellen
foreach (['width' => $this->attributeIds['width'],
'height' => $this->attributeIds['height'],
'length' => $this->attributeIds['length']] as $type => $groupId) {
for ($value = 1; $value <= 10; $value++) {
$name = $value . ' cm';
// Prüfen, ob Option bereits existiert
$criteria = new Criteria();
$criteria->addFilter(new EqualsFilter('groupId', $groupId));
$criteria->addFilter(new EqualsFilter('name', $name));
$existing = $this->propertyGroupOptionRepository->search($criteria, $context)->first();
if ($existing) {
$this->propertyOptionIds[$type][$value] = $existing->getId();
} else {
// Neu erstellen wenn nicht vorhanden
$id = Uuid::randomHex();
$this->propertyGroupOptionRepository->create([
[
'id' => $id,
'groupId' => $groupId,
'name' => $name
]
], $context);
$this->propertyOptionIds[$type][$value] = $id;
}
}
}
}
}
public function importProducts(int $count = 100, int $offset = 0): void
{
// Disable Indexing während des Imports
$context = Context::createDefaultContext();
$context->addState(EntityIndexerRegistry::DISABLE_INDEXING);
$products = [];
$batchSize = 5000;
$this->loadCategories($context);
$this->loadAttributes();
// Lade oder erstelle Property-Optionen einmalig
$this->loadOrCreatePropertyOptions($context);
$count = max(1, min(40000, $count));
// Zufälliger Prefix für diese Import-Session
$importPrefix = substr(md5(uniqid()), 0, 6);
for ($i = 1; $i <= $count; $i++) {
$currentNumber = $i + $offset; // Verwendung des Offsets
$productId = Uuid::randomHex();
$width = mt_rand(1, 10);
$height = mt_rand(1, 10);
$length = mt_rand(1, 10);
// Neue eindeutige Produktnummer
$productNumber = sprintf('PROD-%s-%d-%d-%d-%06d',
$importPrefix,
$width,
$height,
$length,
$currentNumber
);
// Korrekte Preisberechnung mit 19% MwSt
$netPrice = $this->dataGenerator->getRandomPrice();
$grossPrice = $netPrice * 1.19; // Netto + 19% MwSt
$products[] = [
'id' => $productId,
'productNumber' => $productNumber, // Neue eindeutige Nummer,
'stock' => 100,
'translations' => $this->getProductTranslations($width, $height, $length),
'price' => [[
'currencyId' => strtolower('B7D2554B0CE847CD82F3AC9BD1C0DFCA'),
'net' => (string)$netPrice,
'gross' => (string)$grossPrice,
'linked' => true
]],
'tax' => ['name' => '19%', 'taxRate' => 19],
'categories' => $this->getRandomCategoryIds(),
'properties' => [
['id' => $this->propertyOptionIds['width'][$width]],
['id' => $this->propertyOptionIds['height'][$height]],
['id' => $this->propertyOptionIds['length'][$length]]
],
'active' => true,
'visibilities' => [
[
//'salesChannelId' => strtolower('98432DEF39FC4624B33213A56B8C944D'), // Default Storefront
'salesChannelId' => strtolower('0196cd8ed19871fa846d4bfba2298364'),
'visibility' => 30 // Fully visible
]
]
];
if (count($products) >= $batchSize) {
$this->productRepository->create($products, $context);
$products = [];
// Speicher freigeben
gc_collect_cycles();
// Fortschritt anzeigen
echo sprintf("\rImportiere Produkt %d von %d (%d%%) [Offset: %d]",
$i,
$count,
round(($i/$count) * 100),
$offset
);
}
}
if (!empty($products)) {
$this->productRepository->create($products, $context);
}
}
private function loadAttributes(): void
{
// Prüfe ob die Attribute bereits existieren
$this->attributeIds = $this->attributeService->getAttributeIds();
if (empty($this->attributeIds) ||
!isset($this->attributeIds['width']) ||
!isset($this->attributeIds['height']) ||
!isset($this->attributeIds['length'])) {
throw new \RuntimeException('Required attributes (width, height, length) not found. Please create attributes first using the import:attributes command.');
}
}
private function getRandomCategoryIds(): array
{
// Wähle 1-3 zufällige Kategorien aus
$numCategories = mt_rand(1, 1);
$selectedCategories = array_rand(array_flip($this->categoryIds), $numCategories);
if (!is_array($selectedCategories)) {
$selectedCategories = [$selectedCategories];
}
return array_map(function($categoryId) {
return ['id' => $categoryId];
}, $selectedCategories);
}
private function loadCategories(Context $context): void
{
$criteria = new Criteria();
$categories = $this->categoryRepository->search($criteria, $context);
if ($categories->getTotal() === 0) {
throw new \RuntimeException('No categories found. Please create categories first using the import:categories command.');
}
foreach ($categories->getElements() as $category) {
$this->categoryIds[] = $category->getId();
}
}
private function getProductTranslations(int $width, int $height, int $length): array
{
$translations = [];
$supportedLocales = ['de-DE', 'en-GB', 'fr-FR', 'nl-NL', 'it-IT', 'es-ES'];
foreach ($supportedLocales as $locale) {
$translations[$locale] = [
'name' => $this->dataGenerator->generateProductName($width, $height, $length, $locale),
'description' => $this->dataGenerator->generateProductDescription($locale),
'metaTitle' => $this->dataGenerator->generateMetaTitle(
$this->dataGenerator->generateProductName($width, $height, $length, $locale),
$locale
),
'metaDescription' => $this->dataGenerator->generateMetaDescription($locale)
];
}
return $translations;
}
}
/src/custom/plugins/NasadImportProducts/Core/Service/AttributeService.php
<?php declare(strict_types=1);
namespace NasadImportProducts\Core\Service;
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;
use Shopware\Core\Framework\Uuid\Uuid;
class AttributeService
{
public function __construct(
private readonly EntityRepository $propertyGroupRepository,
private readonly EntityRepository $propertyGroupOptionRepository
) {
}
public function getAttributeIds(): array
{
$context = Context::createDefaultContext();
$widthCriteria = new Criteria();
$widthCriteria->addFilter(new EqualsFilter('name', 'Breite'));
$widthGroup = $this->propertyGroupRepository->search($widthCriteria, $context)->first();
$heightCriteria = new Criteria();
$heightCriteria->addFilter(new EqualsFilter('name', 'Hoehe'));
$heightGroup = $this->propertyGroupRepository->search($heightCriteria, $context)->first();
$lengthCriteria = new Criteria();
$lengthCriteria->addFilter(new EqualsFilter('name', 'Laenge'));
$lengthGroup = $this->propertyGroupRepository->search($lengthCriteria, $context)->first();
if (!$widthGroup || !$heightGroup || !$lengthGroup) {
throw new \RuntimeException('Required property groups not found. Please run import:attributes command first.');
}
return [
'width' => $widthGroup->getId(),
'height' => $heightGroup->getId(),
'length' => $lengthGroup->getId(),
];
}
public function createAttributes(): void
{
$context = Context::createDefaultContext();
$widthId = Uuid::randomHex();
$heightId = Uuid::randomHex();
$lengthId = Uuid::randomHex();
// Erstelle die Basisgruppen
$groups = [
[
'id' => $widthId,
'name' => 'Breite',
'displayType' => 'text',
'sortingType' => 'numeric',
'filterable' => true,
],
[
'id' => $heightId,
'name' => 'Hoehe',
'displayType' => 'text',
'sortingType' => 'numeric',
'filterable' => true,
],
[
'id' => $lengthId,
'name' => 'Laenge',
'displayType' => 'text',
'sortingType' => 'numeric',
'filterable' => true,
],
];
// Erstelle die Gruppen
$this->propertyGroupRepository->create($groups, $context);
// Erstelle die Optionen für jede Gruppe
$options = [];
$groupIds = [$widthId, $heightId, $lengthId];
foreach ($groupIds as $groupId) {
for ($i = 1; $i <= 10; $i++) {
$options[] = [
'id' => Uuid::randomHex(),
'groupId' => $groupId,
'name' => $i . ' cm',
'position' => $i,
];
}
}
// Speichere die Optionen
if (!empty($options)) {
$this->propertyGroupOptionRepository->create($options, $context);
}
}
public function deleteAttributes(): void
{
$context = Context::createDefaultContext();
$attributeNames = ['Breite', 'Hoehe', 'Laenge'];
foreach ($attributeNames as $name) {
$criteria = new Criteria();
$criteria->addFilter(new EqualsFilter('name', $name));
$attribute = $this->propertyGroupRepository->search($criteria, $context)->first();
if ($attribute) {
$this->propertyGroupRepository->delete([['id' => $attribute->getId()]], $context);
}
}
}
}
/src/custom/plugins/NasadImportProducts/Core/Service/DummyDataGenerator.php
<?php declare(strict_types=1);
namespace NasadImportProducts\Core\Service;
class DummyDataGenerator
{
private const array LANGUAGES = ['de-DE', 'en-GB', 'fr-FR', 'nl-NL', 'it-IT', 'es-ES'];
public function generateCategoryName(int $number, string $locale): string
{
$prefixes = [
'de-DE' => 'Test',
'en-GB' => 'Test',
'fr-FR' => 'Test',
'nl-NL' => 'Test',
'it-IT' => 'Test',
'es-ES' => 'Test'
];
return $prefixes[$locale] . ' ' . $number;
}
public function generateCategoryDescription(string $locale): string
{
$descriptions = [
'de-DE' => 'Testbeschreibung',
'en-GB' => 'Description',
'fr-FR' => 'Description',
'nl-NL' => 'Beschrijving',
'it-IT' => 'Descrizione',
'es-ES' => 'Descripcion'
];
return $descriptions[$locale];
}
public function generateProductName(int $width, int $height, int $length, string $locale): string
{
$prefixes = [
'de-DE' => 'Produkt',
'en-GB' => 'Product',
'fr-FR' => 'Produit',
'nl-NL' => 'Product',
'it-IT' => 'Prodotto',
'es-ES' => 'Producto'
];
return sprintf('%s %dx%dx%d',
$prefixes[$locale],
$width, // Direkt die Werte verwenden
$height, // ohne Modifikation
$length
);
}
public function generateProductDescription(string $locale): string
{
$descriptions = [
'de-DE' => 'Test',
'en-GB' => 'Test',
'fr-FR' => 'Test',
'nl-NL' => 'Test',
'it-IT' => 'Test',
'es-ES' => 'Test'
];
return $descriptions[$locale];
}
public function generateMetaTitle(string $name, string $locale): string
{
return $name . ' | ' . $this->getStoreName($locale);
}
public function generateMetaDescription(string $locale): string
{
$descriptions = [
'de-DE' => 'Test',
'en-GB' => 'Test',
'fr-FR' => 'Test',
'nl-NL' => 'Test',
'it-IT' => 'Test',
'es-ES' => 'Test'
];
return $descriptions[$locale];
}
private function getStoreName(string $locale): string
{
$names = [
'de-DE' => 'Shop',
'en-GB' => 'Shop',
'fr-FR' => 'Shop',
'nl-NL' => 'Shop',
'it-IT' => 'Shop',
'es-ES' => 'Shop'
];
return $names[$locale];
}
public function getRandomPrice(): string
{
return number_format(mt_rand(1000, 99900) / 100, 2, '.', '');
}
}