<?php declare(strict_types=1);
namespace Valantic\SalesChannelProductData\Subscriber;
use Shopware\Core\Content\Product\ProductEvents;
use Shopware\Core\Content\Product\SalesChannel\SalesChannelProductEntity;
use Shopware\Core\Defaults;
use Shopware\Core\Framework\DataAbstractionLayer\Entity;
use Shopware\Core\Framework\DataAbstractionLayer\EntityRepositoryInterface;
use Shopware\Core\Framework\DataAbstractionLayer\Event\EntityWrittenContainerEvent;
use Shopware\Core\Framework\DataAbstractionLayer\Event\EntityWrittenEvent;
use Shopware\Core\Framework\DataAbstractionLayer\Indexing\InheritanceUpdater;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsAnyFilter;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
use Shopware\Core\Framework\Uuid\Uuid;
use Shopware\Core\System\SalesChannel\Entity\SalesChannelEntityLoadedEvent;
use Shopware\Storefront\Page\Product\ProductPageCriteriaEvent;
use Shopware\Storefront\Page\Product\ProductPageLoadedEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use Valantic\SalesChannelProductData\Core\Content\Product\DataAbstractionLayer\StockUpdater;
use Valantic\SalesChannelProductData\Core\Content\ProductData\ProductDataDefinition;
use Valantic\SalesChannelProductData\Service\ProductDataServiceInterface;
class ProductSubscriber implements EventSubscriberInterface
{
public const CHECK_CONFIGURATION_FIELDS = [
'isStockEnabled' => self::STOCK_ENABLED_FIELDS,
'isDeliveryTimeEnabled' => self::DELIVERY_TIME_ENABLED_FIELDS,
'isReleaseDateEnabled' => self::RELEASE_DATE_FIELDS,
];
public const STOCK_ENABLED_FIELDS = [
'active', 'sales', 'stock', 'availableStock', 'isCloseout', 'available',
];
public const DELIVERY_TIME_ENABLED_FIELDS = [
'deliveryTimeId', 'deliveryTime',
];
public const RELEASE_DATE_FIELDS = ['releaseDate'];
private array $processedProducts = [];
// cache product visibility data
/*
* The product Ids used to query the associated product visibility data
*/
private array $visibilityRequestIds = [];
/**
* The product visibility data result, grouped by 'parentId' value
*/
private array $visibilityRequestData = [];
// end cache product visibility data
private EntityRepositoryInterface $productVisibilityRepository;
private ProductDataServiceInterface $productDataService;
private InheritanceUpdater $inheritanceUpdater;
private StockUpdater $stockUpdater;
private EntityRepositoryInterface $productDataRepository;
private RequestStack $requestStack;
public function __construct(
EntityRepositoryInterface $productVisibilityRepository,
ProductDataServiceInterface $productDataService,
InheritanceUpdater $inheritanceUpdater,
StockUpdater $stockUpdater,
EntityRepositoryInterface $productDataRepository,
RequestStack $requestStack
) {
$this->productVisibilityRepository = $productVisibilityRepository;
$this->productDataService = $productDataService;
$this->inheritanceUpdater = $inheritanceUpdater;
$this->stockUpdater = $stockUpdater;
$this->productDataRepository = $productDataRepository;
$this->requestStack = $requestStack;
}
public static function getSubscribedEvents(): array
{
return [
EntityWrittenContainerEvent::class => ['entityWrittenContainer'],
'product_visibility.written' => ['productVisibilityWritten'],
'sales_channel.' . ProductEvents::PRODUCT_LOADED_EVENT => ['salesChannelLoaded', 1000],
ProductPageCriteriaEvent::class => 'addVisibilities',
ProductPageLoadedEvent::class => 'onProductPageLoaded',
];
}
public function entityWrittenContainer(EntityWrittenContainerEvent $event): void
{
$updates = $event->getPrimaryKeys(ProductDataDefinition::ENTITY_NAME);
if (empty($updates)) {
return;
}
// update ManyToOneAssociationField(s) associtation(s)
// 'delivery_time_id' => 'deliveryTime'
$this->inheritanceUpdater->update(ProductDataDefinition::ENTITY_NAME, $updates, $event->getContext());
// update 'stock' related fields
$this->stockUpdater->update($updates, $event->getContext(), ProductDataDefinition::ENTITY_NAME);
}
public function productVisibilityWritten(EntityWrittenEvent $event): void
{
// Abbrechen, wenn der Connector bereits Daten verwaltet hat
$request = $this->requestStack->getMainRequest();
if ($request && str_contains($request->getUri(), 'adjust-val-product-data')) {
return;
}
$entityWriteResults = $event->getWriteResults();
$productData = [];
foreach ($entityWriteResults as $entityWriteResult) {
$productVisibilityId = $entityWriteResult->getPrimaryKey();
// Prüfen, ob bereits ein Eintrag in `val_product_data` für die product_visibility_id existiert
$criteria = new Criteria();
$criteria->addFilter(new EqualsFilter('productVisibilityId', $productVisibilityId));
// Suche nach vorhandenen Datensätzen in der `val_product_data`
$existingProductData = $this->productDataRepository->search($criteria, $event->getContext());
// Wenn bereits ein Eintrag existiert, keinen neuen Datensatz anlegen
if ($existingProductData->getTotal() > 0) {
continue;
}
// Falls kein Eintrag existiert, einen neuen Datensatz anlegen
if ($entityWriteResult->getOperation() === 'insert') {
$productData[] = [
'id' => Uuid::randomHex(),
'productVisibilityId' => $productVisibilityId,
'stock' => 0,
'isCloseout' => false,
'createdAt' => (new \DateTime())->format(Defaults::STORAGE_DATE_TIME_FORMAT),
];
}
}
// Nur wenn es neue productData gibt, diese in die Datenbank schreiben
if (!empty($productData)) {
$this->productDataRepository->create(
$productData,
$event->getContext()
);
}
}
public function salesChannelLoaded(SalesChannelEntityLoadedEvent $event): void
{
$productVisibilityData = $this->getProductVisibilityData($event);
/** @var SalesChannelProductEntity $product */
foreach ($event->getEntities() as $product) {
$productId = $product->getId();
$parentId = $product->getParentId() ?? '';
// continue if the product was already processed
if (!empty($this->processedProducts)
&& \array_key_exists($productId, $this->processedProducts)) {
// update the current product with the cached data
$this->processProduct(
$product,
$this->processedProducts[$productId],
'',
false
);
continue;
}
if (\array_key_exists($productId, $productVisibilityData)) {
$productVisibility = $productVisibilityData[$productId];
} elseif (\array_key_exists($parentId, $productVisibilityData)) {
$productVisibility = $productVisibilityData[$parentId];
} else {
continue;
}
// check if there is a 'productData' extension
if ($productVisibility->hasExtension('productData')) {
// process the product with new data if the case
$this->processProduct(
$product,
$productVisibility->getExtension('productData'),
$event->getSalesChannelContext()->getSalesChannelId(),
true
);
// cache processed product
$this->processedProducts[$productId] = $product;
}
}
}
public function addVisibilities(ProductPageCriteriaEvent $event): void
{
$criteria = $event->getCriteria();
$criteria->addAssociation('visibilities');
}
public function onProductPageLoaded(ProductPageLoadedEvent $event): void
{
$salesChannelId = $event->getSalesChannelContext()->getSalesChannelId();
if ($this->productDataService->isConfigFieldActive('isReleaseDateEnabled', $salesChannelId)) {
$product = $event->getPage()->getProduct();
if (
($visibilities = $product->getVisibilities())
&& ($elements = $visibilities->getElements())
&& isset($elements[$salesChannelId])
&& $record = $elements[$salesChannelId]
) {
$extensions = $record->getExtension('productData');
$product->setReleaseDate($extensions->getReleaseDate());
}
}
}
private function getProductVisibilityData(SalesChannelEntityLoadedEvent $event): array
{
$salesChannelId = $event->getSalesChannelContext()->getSalesChannelId();
$products = $event->getEntities();
$productIds = array_unique(
array_filter(
array_merge(
array_column($products, 'id'),
array_column($products, 'parentId') // we take the 'parentId' in case it's an inherited variant
)
)
);
// return the cached value if the $productIds were already used to query visibility data
if (
!empty($this->visibilityRequestIds)
&& !empty($this->visibilityRequestData)
&& empty(array_diff($this->visibilityRequestIds, $productIds))
) {
return $this->visibilityRequestData;
}
$criteria = new Criteria();
$criteria->addFilter(new EqualsFilter('salesChannelId', $salesChannelId));
$criteria->addFilter(new EqualsAnyFilter('productId', $productIds));
$criteria->addAssociation('productData');
$productDataCriteria = $criteria->getAssociation('productData');
$productDataCriteria->addAssociation('deliveryTime');
$productVisibilityData = $this->productVisibilityRepository->search($criteria, $event->getContext())->getElements();
// cache $productIds used for the query
$this->visibilityRequestIds = $productIds;
// group by 'productId' field value
// cache product visibility data
$this->visibilityRequestData = array_column($productVisibilityData, null, 'productId');
return $this->visibilityRequestData;
}
private function processProduct(Entity $product, Entity $productData, string $salesChannelId, bool $isNew): void
{
foreach (self::CHECK_CONFIGURATION_FIELDS as $configField => $fieldsToUpdate) {
if ($isNew) {
$isEnabledField = $this->productDataService->isConfigFieldActive($configField, $salesChannelId);
if (!$isEnabledField) {
continue;
}
}
foreach ($fieldsToUpdate as $field) {
$getMethod = $this->generateGetSetMethod($field);
$setMethod = $this->generateGetSetMethod($field, 'set');
if (method_exists($productData, $getMethod)) {
$setValue = $productData->{$getMethod}();
if (method_exists($product, $setMethod)) {
$product->{$setMethod}($setValue);
}
}
}
}
}
private function generateGetSetMethod(string $name, string $type = 'get'): string
{
if (!\in_array($type, ['get', 'set'], true)) {
$type = 'get';
}
return $type . ucwords($name);
}
}