Neues Modul registrieren
Das twocream Shopware Connector Bundle bietet zwei Arten an, um ein Importmodul zu implementieren.
Das AbstractModule ist für einfache Entitäten geeignet und wird zum Beispiel für den Attributsimport eingesetzt. Es wird aber auch eingesetzt, wenn man flache Datenstrukturen aus dem MDM-Bereich in eigene Shopware-Entitäten einspielen möchte.
Das AbstractSyncModule ist für komplexere Entitäten, wie die Shopware Produkt-Entität, geeignet. Es bietet an, dass mehrere Entitäten während der Verarbeitung erstellt, aktualisiert oder gelöscht werden können. Zum Beispiel relationale Datensätze wie Shopware Maßeinheiten.
Aufbau eines einfachen Moduls
Einfache Module müssen von dieser abstrakten Klasse erben: Twocream\JsonImporter\Core\System\Import\Module\AbstractModule
<?php
namespace MyCustomExtension\Core\System\Import\Module;
use Shopware\Core\Framework\DataAbstractionLayer\Event\EntityWrittenContainerEvent;
use Twocream\JsonImporter\Core\System\Import\Module\AbstractModule;
use Twocream\JsonImporter\Core\System\Import\Validation\Decision;
use Twocream\JsonImporter\Core\System\Import\Validation\Result;
use MyCustomExtension\Core\Content\Pimcore\MasterDataManagement\Video\VideoDefinition;
class VideoModule extends AbstractModule
{
public function getHandledPackage(): string
{
return 'videos';
}
protected function getEntityName(): string
{
return VideoDefinition::ENTITY_NAME;
}
protected function validateEntry(array $entry): array
{
$problems = [];
if (empty($entry['uuid'])) {
$problems[] = 'uuid';
}
if (empty($entry['id'])) {
$problems[] = 'id';
}
return $problems;
}
protected function store(Result $result): ?EntityWrittenContainerEvent
{
$records = [];
$videoRepository = $this->registry->getRepository('app_pimcore_video');
foreach ($result as $entry) {
if ($entry->getDecision() === Decision::DECISION_SKIP_RECORD) {
continue;
}
$translations = $this->service->prepareTranslation($entry['texts']['translations']);
$videoTranslations = $this->service->prepareTranslation($entry['video']['translations'], [
'youtube-link' => 'youtubeLink'
]);
$thumbnailTranslations = $this->service->prepareTranslation($entry['thumbnail']['translations'], [
'image' => 'thumbnailImage',
'title' => 'thumbnailTitle',
'alt-text' => 'thumbnailAltText'
]);
foreach ($translations as $key => $values) {
if (isset($videoTranslations[$key])) {
$translations[$key] = array_merge($values, $videoTranslations[$key]);
} else {
$translations[$key] = array_merge($values, ['youtubeLink' => null]);
}
if (isset($thumbnailTranslations[$key])) {
$translations[$key] = array_merge($translations[$key], $thumbnailTranslations[$key]);
} else {
$translations[$key] = array_merge(
$translations[$key],
['thumbnailImage' => null, 'thumbnailTitle' => null, 'thumbnailAltText' => null]
);
}
}
$records[] = [
'id' => $entry['uuid'],
'pimcoreId' => $entry['id'],
'key' => $entry['key'],
'type' => 'youtube',
'defaultLink' => $entry['video']['default-value'],
'translations' => $translations
];
}
if (!empty($records)) {
return $videoRepository->upsert($records, $this->context);
}
return null;
}
}
Aufbau eines komplexereren Moduls
Komplexe Module müssen von dieser abstrakten Klasse erben: Twocream\JsonImporter\Core\System\Import\Module\AbstractSyncModule
<?php
namespace TwocreamExampleExtension\Core\System\Import\Module;
use Doctrine\DBAL\Connection;
use Shopware\Core\Content\Media\Aggregate\MediaFolder\MediaFolderDefinition;
use Shopware\Core\Content\Product\Aggregate\ProductConfiguratorSetting\ProductConfiguratorSettingDefinition;
use Shopware\Core\Content\Product\Aggregate\ProductMedia\ProductMediaDefinition;
use Shopware\Core\Content\Product\Aggregate\ProductProperty\ProductPropertyDefinition;
use Shopware\Core\Content\Product\Aggregate\ProductVisibility\ProductVisibilityDefinition;
use Shopware\Core\Content\Product\ProductDefinition;
use Shopware\Core\Content\Property\Aggregate\PropertyGroupOption\PropertyGroupOptionDefinition;
use Shopware\Core\Framework\Api\Sync\SyncOperation;
use Shopware\Core\Framework\Api\Sync\SyncService;
use Shopware\Core\Framework\DataAbstractionLayer\DefinitionInstanceRegistry;
use Shopware\Core\Framework\DataAbstractionLayer\Event\EntityWrittenContainerEvent;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
use Shopware\Core\Framework\DataAbstractionLayer\Search\EntitySearchResult;
use Shopware\Core\System\SalesChannel\SalesChannelDefinition;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Twocream\JsonImporter\Core\System\Import\Module\AbstractSyncModule;
use Twocream\JsonImporter\Core\System\Import\Module\Service\ModuleService;
use Twocream\JsonImporter\Core\System\Import\Validation\Decision;
use Twocream\JsonImporter\Core\System\Import\Validation\Result;
use TwocreamExampleExtension\Helper\UuidConstants;
use TwocreamExampleExtension\Helper\SalesChannelHelper;
use TwocreamExampleExtension\Service\ExampleProductService;
use TwocreamExampleExtension\Service\ExampleMediaService;
class ProductModule extends AbstractSyncModule
{
private array $variantPropertyGroupOptionPositions = [];
private array $existsOptionIdsInProductConfigurator = [];
private EntitySearchResult $salesChannels;
private string $mediaFolderId;
private ExampleProductService $productService;
private ExampleMediaService $mediaService;
public function __construct(
SyncService $syncService,
EventDispatcherInterface $dispatcher,
Connection $connection,
DefinitionInstanceRegistry $registry,
?ModuleService $moduleService = null,
ExampleProductService $productService,
ExampleMediaService $mediaService,
) {
parent::__construct($syncService, $dispatcher, $connection, $registry, $moduleService);
$this->productService = $productService;
$this->mediaService = $mediaService;
$this->mediaFolderId = md5('pimcore-media-folder');
$this->syncs[MediaFolderDefinition::ENTITY_NAME][SyncOperation::ACTION_UPSERT][] = [
'id' => $this->mediaFolderId,
'name' => 'Pimcore Media',
'useParentConfiguration' => false,
'configuration' => []
];
}
public function getHandledPackage(): string
{
return 'products';
}
protected function getEntityName(): string
{
return ProductDefinition::ENTITY_NAME;
}
protected function validateEntry(array $entry): array
{
$problems = [];
if (empty($entry['translations']['en_GB']['name'])) $problems[] = 'name';
return $problems;
}
protected function store(Result $result): ?EntityWrittenContainerEvent
{
$variantPropertyGroupOptions = $this->registry->getRepository(
PropertyGroupOptionDefinition::ENTITY_NAME
)->search(new Criteria(), $this->context);
$this->salesChannels = $this->registry->getRepository(SalesChannelDefinition::ENTITY_NAME)->search(
(new Criteria())
->addAssociation('countries')
->addAssociation('languages')
->addAssociation('languages.locale'),
$this->context
);
foreach ($variantPropertyGroupOptions as $variantPropertyGroupOption) {
$this->variantPropertyGroupOptionPositions[$variantPropertyGroupOption->getId()] = $variantPropertyGroupOption->getPosition();
}
foreach ($result as $entry) {
if ($entry->getDecision() === Decision::DECISION_SKIP_RECORD) continue;
if ($this->productService->isProductNumberAlreadyExisting($entry['uuid'], $entry['product-number'])) continue;
$variantsIds = [];
foreach ($entry['variants'] as $variant) {
$variantsIds[] = $variant['uuid'];
}
$this->storeProduct($entry->getValue(), null, $variantsIds);
foreach ($entry['variants'] as $variant) {
$this->storeProduct($variant, $entry['uuid']);
}
$this->existsOptionIdsInProductConfigurator = [];
if (empty($variantsIds)) continue;
$this->syncs[ProductDefinition::ENTITY_NAME][SyncOperation::ACTION_UPSERT][] = [
'id' => $entry['uuid'],
'mainVariantId' => $variantsIds[array_key_first($variantsIds)]
];
}
return $this->sync();
}
// Separate Funktion für Produkte
protected function storeProduct(array $entry, ?string $parentProductId, array $variantsIds = []): void
{
$price = [
'eur' => [
'currencyId' => UuidConstants::CURRENCY_EUR_ID,
'gross' => $entry['price']['gross'],
'net' => $entry['price']['net'],
'linked' => false
]
];
$visibilities = [];
$hasVisibility = false;
foreach ($this->salesChannels as $salesChannel) {
$salesChannelId = $salesChannel->getId();
foreach (SalesChannelHelper::SALES_CHANNEL_VISIBILITY_MAPPING[$salesChannelId] as $country) {
if (!isset($entry['visibilities'][$country])) continue 2;
}
$visibilities[$salesChannelId] = [
'id' => md5($entry['id'] . '-ProductVisibilityDefinition-' . $salesChannelId),
'productId' => $entry['uuid'],
'salesChannelId' => $salesChannelId,
'visibility' => ProductVisibilityDefinition::VISIBILITY_ALL,
];
$hasVisibility = true;
}
// In INT sollen auch die Produkte angezeigt werden,
// wenn diese mind. in einem anderen Sales-Channel hinterlegt sind.
if (!empty($visibilities) && !isset($visibilities[SalesChannelHelper::SALES_CHANNEL_INT_ID])) {
$visibilities[SalesChannelHelper::SALES_CHANNEL_INT_ID] = [
'id' => md5(
$entry['id']
. '-ProductVisibilityDefinition-'
. SalesChannelHelper::SALES_CHANNEL_INT_ID
),
'productId' => $entry['uuid'],
'salesChannelId' => SalesChannelHelper::SALES_CHANNEL_INT_ID,
'visibility' => ProductVisibilityDefinition::VISIBILITY_ALL,
];
}
$propertyGroupOptionIds = $this->prepareRelations(
$entry['id'],
$entry['properties'],
PropertyGroupOptionDefinition::ENTITY_NAME
);
$variantPropertyGroupOptionId = null;
// Der Attributswert / PropertyGroupOption für die Variantendarstellung (Variantenmerkmal)
// darf am Produkt nicht als 'property' sondern muss als 'option' importiert werden
foreach ($propertyGroupOptionIds as $propertyGroupOptionId => $relationData) {
if (array_key_exists($propertyGroupOptionId, $this->variantPropertyGroupOptionPositions)) {
$variantPropertyGroupOptionId = $propertyGroupOptionId;
unset($propertyGroupOptionIds[$propertyGroupOptionId]);
break;
}
}
$productMedia = $this->mediaService->getProductMedia(
$entry['custom-fields']['twocream-product-connector-media'],
$entry['product-number']
);
if ($this->isIdExist($entry['uuid'], ProductDefinition::ENTITY_NAME)) {
$product = $this->productService->fetchExistingProduct($entry['uuid']);
foreach ($product->getVisibilities() ?? [] as $visibility) {
if (!isset($visibilities[$visibility->getSalesChannelId()])) {
$this->syncs[ProductVisibilityDefinition::ENTITY_NAME][SyncOperation::ACTION_DELETE][] = [
'id' => $visibility->getId(),
'productId' => $product->getId(),
'salesChannelId' => $visibility->getId()
];
} else {
// Wenn der Sales-Channel bereits zugewiesen ist, muss er nicht erneut zugewiesen werden
unset($visibilities[$visibility->getSalesChannelId()]);
}
}
foreach ($product->getProperties() ?? [] as $property) {
if (!in_array($property->getId(), array_keys($propertyGroupOptionIds))) {
$this->syncs[ProductPropertyDefinition::ENTITY_NAME][SyncOperation::ACTION_DELETE][] = [
'productId' => $product->getId(),
'optionId' => $property->getId()
];
}
}
foreach ($product->getConfiguratorSettings() ?? [] as $configuratorSetting) {
$this->syncs[ProductConfiguratorSettingDefinition::ENTITY_NAME][SyncOperation::ACTION_DELETE][] = [
'id' => $configuratorSetting->getId()
];
}
foreach ($product->getChildren() as $variant) {
if (!in_array($variant->getId(), $variantsIds)) {
$this->syncs[ProductDefinition::ENTITY_NAME][SyncOperation::ACTION_DELETE][] = [
'id' => $variant->getId()
];
}
}
$currentMedia = [];
$newMedia = [];
foreach ($product->getMedia() ?? [] as $media) {
$currentMedia[] = $media->getMediaId();
}
foreach ($productMedia ?? [] as $media) {
$newMedia[] = $media['mediaId'];
}
if (!empty(array_diff($newMedia, $currentMedia))) {
foreach ($product->getMedia() ?? [] as $media) {
$this->syncs[ProductMediaDefinition::ENTITY_NAME][SyncOperation::ACTION_DELETE][] = ['id' => $media->getId()];
}
}
}
$translations = $this->service->prepareTranslation(
$entry['translations'],
[
'description' => 'description',
'name' => 'name',
'meta-title' => 'metaTitle',
'meta-description' => 'metaDescription'
]
);
$instructionsForUse = array_map(function ($translation) {
return $translation['twocream_pimcore_export_product_instructions_for_use'] ?? '';
}, $this->service->prepareTranslation(
$entry['custom-fields']['twocream-product-connector-instructions-for-use'],
['instructionsForUse' => 'twocream_pimcore_export_product_instructions_for_use']
));
$sets = array_map(function ($translation) {
return $translation['twocream_pimcore_export_product_sets'] ?? '';
}, $this->service->prepareTranslation(
$entry['custom-fields']['twocream-product-connector-sets'],
['setInformation' => 'twocream_pimcore_export_product_sets']
));
foreach ($translations as $languageId => $translation) {
$translations[$languageId]['customFields']['twocream_pimcore_export_product_instructions_for_use'] =
$instructionsForUse[$languageId];
$translations[$languageId]['customFields']['twocream_pimcore_export_product_sets'] =
json_encode($sets[$languageId]);
$translations[$languageId]['customFields']['twocream_pimcore_export_product_position'] =
$entry['custom-fields']['twocream-product-connector-position'];
$translations[$languageId]['customFields']['twocream_pimcore_export_product_downloads'] =
json_encode($entry['custom-fields']['twocream-product-connector-downloads']);
$translations[$languageId]['customFields']['twocream_pimcore_export_product_icons'] =
json_encode($entry['custom-fields']['twocream-product-connector-icons']);
$translations[$languageId]['customFields']['twocream_pimcore_export_product_videos'] =
json_encode($entry['custom-fields']['twocream-product-connector-videos']);
$translations[$languageId]['customFields']['twocream_pimcore_export_product_distribution_countries'] =
json_encode(array_keys($entry['visibilities']));
}
$productData = [
'id' => $entry['uuid'],
'parentID' => $parentProductId,
'active' => $hasVisibility,
'productNumber' => $entry['product-number'],
'manufacturerId' => $this->productService->fetchManufacturerId($entry['manufacturer']),
'markAsTopseller' => $entry['mark-as-topseller'],
'taxId' => $this->productService->fetchTaxId((int) $entry['tax']),
'price' => array_values($price),
'deliveryTimeId' => null,
'shippingFree' => $entry['shipping-free'],
'minPurchase' => $entry['min-purchase'] ?? 1,
'purchaseSteps' => 1,
'maxPurchase' => null,
'releaseDate' => $entry['release-date'],
'visibilities' => array_values($visibilities),
'ean' => $entry['ean'],
'media' => $productMedia,
'cover' => !empty($productMedia) ? array_shift($productMedia) : null,
'width' => $entry['width'],
'height' => $entry['height'],
'length' => $entry['length'],
'weight' => $entry['weight'],
'purchaseUnit' => empty($entry['purchase-unit']) ? 1 : $entry['purchase-unit'],
'unitId' => $this->productService->fetchUnitId($entry['unit']),
'referenceUnit' => empty($entry['purchase-unit']) ? 1 : $entry['purchase-unit'],
'properties' => array_values($propertyGroupOptionIds),
'translations' => $translations
];
if ($variantPropertyGroupOptionId) $productData['options'][] = ['id' => $variantPropertyGroupOptionId];
$this->syncs[ProductDefinition::ENTITY_NAME][SyncOperation::ACTION_UPSERT][] = $productData;
}
}
Erklärung
Das komplexe Modul baut auf dem einfachen Modul auf, daher ist folgende Definition auf beide Typen anwendbar:
| Name | Beschreibung |
|---|---|
getHandledPackage(): string | Eindeutiger Package-Name, z.B. "product" |
getEntityName(): string | Der Name der Entity, für die das Modul bestimmt ist, z.B. "product" |
store(Result $result): ?EntityWrittenContainerEvent | Hier erfolgt die inhaltsbezogene Verarbeitung der Daten zum importieren |
canBeDeleted(): bool | Hiermit kann optional die Löschung der Daten verhindert werden |
isIdExist(string $id, string $entityName): bool | Hilfsmethode um zu prüfen, ob der Datensatz zu der ID & Entität bereits vorhanden ist |
validateEntry(array $entry): array | Hier kann optional eine Validierungslogik für importierte Daten implementiert werden |
Das Modul registrieren
Neue Module sind in einer Service-Konfigurationsdatei zu registrieren. Hierbei ist der Service-Tag twocream_json_importer.module zwingend zu verwenden.
Zur strukturierten Konfiguration wird empfohlen, die Datei src/Resources/config/services/import.xml anzulegen.
Diese ist anschließend über einen Import in der zentralen services.xml einzubinden.
<service id="MyCustomExtension\Core\System\Import\Module\ProductModule">
<tag name="twocream_json_importer.module"/>
</service>