vendor/valantic/sales-channel-product-data/src/Subscriber/ProductSubscriber.php line 159

Open in your IDE?
  1. <?php declare(strict_types=1);
  2. namespace Valantic\SalesChannelProductData\Subscriber;
  3. use Shopware\Core\Content\Product\ProductEvents;
  4. use Shopware\Core\Content\Product\SalesChannel\SalesChannelProductEntity;
  5. use Shopware\Core\Defaults;
  6. use Shopware\Core\Framework\DataAbstractionLayer\Entity;
  7. use Shopware\Core\Framework\DataAbstractionLayer\EntityRepositoryInterface;
  8. use Shopware\Core\Framework\DataAbstractionLayer\Event\EntityWrittenContainerEvent;
  9. use Shopware\Core\Framework\DataAbstractionLayer\Event\EntityWrittenEvent;
  10. use Shopware\Core\Framework\DataAbstractionLayer\Indexing\InheritanceUpdater;
  11. use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
  12. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsAnyFilter;
  13. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
  14. use Shopware\Core\Framework\Uuid\Uuid;
  15. use Shopware\Core\System\SalesChannel\Entity\SalesChannelEntityLoadedEvent;
  16. use Shopware\Storefront\Page\Product\ProductPageCriteriaEvent;
  17. use Shopware\Storefront\Page\Product\ProductPageLoadedEvent;
  18. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  19. use Symfony\Component\HttpFoundation\RequestStack;
  20. use Valantic\SalesChannelProductData\Core\Content\Product\DataAbstractionLayer\StockUpdater;
  21. use Valantic\SalesChannelProductData\Core\Content\ProductData\ProductDataDefinition;
  22. use Valantic\SalesChannelProductData\Service\ProductDataServiceInterface;
  23. class ProductSubscriber implements EventSubscriberInterface
  24. {
  25.     public const CHECK_CONFIGURATION_FIELDS = [
  26.         'isStockEnabled' => self::STOCK_ENABLED_FIELDS,
  27.         'isDeliveryTimeEnabled' => self::DELIVERY_TIME_ENABLED_FIELDS,
  28.         'isReleaseDateEnabled' => self::RELEASE_DATE_FIELDS,
  29.     ];
  30.     public const STOCK_ENABLED_FIELDS = [
  31.         'active''sales''stock''availableStock''isCloseout''available',
  32.     ];
  33.     public const DELIVERY_TIME_ENABLED_FIELDS = [
  34.         'deliveryTimeId''deliveryTime',
  35.     ];
  36.     public const RELEASE_DATE_FIELDS = ['releaseDate'];
  37.     private array $processedProducts = [];
  38.     // cache product visibility data
  39.     /*
  40.      * The product Ids used to query the associated product visibility data
  41.      */
  42.     private array $visibilityRequestIds = [];
  43.     /**
  44.      * The product visibility data result, grouped by 'parentId' value
  45.      */
  46.     private array $visibilityRequestData = [];
  47.     // end cache product visibility data
  48.     private EntityRepositoryInterface $productVisibilityRepository;
  49.     private ProductDataServiceInterface $productDataService;
  50.     private InheritanceUpdater $inheritanceUpdater;
  51.     private StockUpdater $stockUpdater;
  52.     private EntityRepositoryInterface $productDataRepository;
  53.     private RequestStack $requestStack;
  54.     public function __construct(
  55.         EntityRepositoryInterface $productVisibilityRepository,
  56.         ProductDataServiceInterface $productDataService,
  57.         InheritanceUpdater $inheritanceUpdater,
  58.         StockUpdater $stockUpdater,
  59.         EntityRepositoryInterface $productDataRepository,
  60.         RequestStack $requestStack
  61.     ) {
  62.         $this->productVisibilityRepository $productVisibilityRepository;
  63.         $this->productDataService $productDataService;
  64.         $this->inheritanceUpdater $inheritanceUpdater;
  65.         $this->stockUpdater $stockUpdater;
  66.         $this->productDataRepository $productDataRepository;
  67.         $this->requestStack $requestStack;
  68.     }
  69.     public static function getSubscribedEvents(): array
  70.     {
  71.         return [
  72.             EntityWrittenContainerEvent::class => ['entityWrittenContainer'],
  73.             'product_visibility.written' => ['productVisibilityWritten'],
  74.             'sales_channel.' ProductEvents::PRODUCT_LOADED_EVENT => ['salesChannelLoaded'1000],
  75.             ProductPageCriteriaEvent::class => 'addVisibilities',
  76.             ProductPageLoadedEvent::class => 'onProductPageLoaded',
  77.         ];
  78.     }
  79.     public function entityWrittenContainer(EntityWrittenContainerEvent $event): void
  80.     {
  81.         $updates $event->getPrimaryKeys(ProductDataDefinition::ENTITY_NAME);
  82.         if (empty($updates)) {
  83.             return;
  84.         }
  85.         // update ManyToOneAssociationField(s) associtation(s)
  86.         // 'delivery_time_id' => 'deliveryTime'
  87.         $this->inheritanceUpdater->update(ProductDataDefinition::ENTITY_NAME$updates$event->getContext());
  88.         // update 'stock' related fields
  89.         $this->stockUpdater->update($updates$event->getContext(), ProductDataDefinition::ENTITY_NAME);
  90.     }
  91.     public function productVisibilityWritten(EntityWrittenEvent $event): void
  92.     {
  93.         // Abbrechen, wenn der Connector bereits Daten verwaltet hat
  94.         $request $this->requestStack->getMainRequest();
  95.         if ($request && str_contains($request->getUri(), 'adjust-val-product-data')) {
  96.             return;
  97.         }
  98.     
  99.         $entityWriteResults $event->getWriteResults();
  100.         $productData = [];
  101.     
  102.         foreach ($entityWriteResults as $entityWriteResult) {
  103.             $productVisibilityId $entityWriteResult->getPrimaryKey();
  104.     
  105.             // Prüfen, ob bereits ein Eintrag in `val_product_data` für die product_visibility_id existiert
  106.             $criteria = new Criteria();
  107.             $criteria->addFilter(new EqualsFilter('productVisibilityId'$productVisibilityId));
  108.     
  109.             // Suche nach vorhandenen Datensätzen in der `val_product_data`
  110.             $existingProductData $this->productDataRepository->search($criteria$event->getContext());
  111.     
  112.             // Wenn bereits ein Eintrag existiert, keinen neuen Datensatz anlegen
  113.             if ($existingProductData->getTotal() > 0) {
  114.                 continue;
  115.             }
  116.     
  117.             // Falls kein Eintrag existiert, einen neuen Datensatz anlegen
  118.             if ($entityWriteResult->getOperation() === 'insert') {
  119.                 $productData[] = [
  120.                     'id' => Uuid::randomHex(),
  121.                     'productVisibilityId' => $productVisibilityId,
  122.                     'stock' => 0,
  123.                     'isCloseout' => false,
  124.                     'createdAt' => (new \DateTime())->format(Defaults::STORAGE_DATE_TIME_FORMAT),
  125.                 ];
  126.             }
  127.         }
  128.     
  129.         // Nur wenn es neue productData gibt, diese in die Datenbank schreiben
  130.         if (!empty($productData)) {
  131.             $this->productDataRepository->create(
  132.                 $productData,
  133.                 $event->getContext()
  134.             );
  135.         }
  136.     }
  137.     public function salesChannelLoaded(SalesChannelEntityLoadedEvent $event): void
  138.     {
  139.         $productVisibilityData $this->getProductVisibilityData($event);
  140.         /** @var SalesChannelProductEntity $product */
  141.         foreach ($event->getEntities() as $product) {
  142.             $productId $product->getId();
  143.             $parentId $product->getParentId() ?? '';
  144.             // continue if the product was already processed
  145.             if (!empty($this->processedProducts)
  146.                 && \array_key_exists($productId$this->processedProducts)) {
  147.                 // update the current product with the cached data
  148.                 $this->processProduct(
  149.                     $product,
  150.                     $this->processedProducts[$productId],
  151.                     '',
  152.                     false
  153.                 );
  154.                 continue;
  155.             }
  156.             if (\array_key_exists($productId$productVisibilityData)) {
  157.                 $productVisibility $productVisibilityData[$productId];
  158.             } elseif (\array_key_exists($parentId$productVisibilityData)) {
  159.                 $productVisibility $productVisibilityData[$parentId];
  160.             } else {
  161.                 continue;
  162.             }
  163.             // check if there is a 'productData' extension
  164.             if ($productVisibility->hasExtension('productData')) {
  165.                 // process the product with new data if the case
  166.                 $this->processProduct(
  167.                     $product,
  168.                     $productVisibility->getExtension('productData'),
  169.                     $event->getSalesChannelContext()->getSalesChannelId(),
  170.                     true
  171.                 );
  172.                 // cache processed product
  173.                 $this->processedProducts[$productId] = $product;
  174.             }
  175.         }
  176.     }
  177.     public function addVisibilities(ProductPageCriteriaEvent $event): void
  178.     {
  179.         $criteria $event->getCriteria();
  180.         $criteria->addAssociation('visibilities');
  181.     }
  182.     public function onProductPageLoaded(ProductPageLoadedEvent $event): void
  183.     {
  184.         $salesChannelId $event->getSalesChannelContext()->getSalesChannelId();
  185.         if ($this->productDataService->isConfigFieldActive('isReleaseDateEnabled'$salesChannelId)) {
  186.             $product $event->getPage()->getProduct();
  187.             if (
  188.                 ($visibilities $product->getVisibilities())
  189.                 && ($elements $visibilities->getElements())
  190.                 && isset($elements[$salesChannelId])
  191.                 && $record $elements[$salesChannelId]
  192.             ) {
  193.                 $extensions $record->getExtension('productData');
  194.                 $product->setReleaseDate($extensions->getReleaseDate());
  195.             }
  196.         }
  197.     }
  198.     private function getProductVisibilityData(SalesChannelEntityLoadedEvent $event): array
  199.     {
  200.         $salesChannelId $event->getSalesChannelContext()->getSalesChannelId();
  201.         $products $event->getEntities();
  202.         $productIds array_unique(
  203.             array_filter(
  204.                 array_merge(
  205.                     array_column($products'id'),
  206.                     array_column($products'parentId'// we take the 'parentId' in case it's an inherited variant
  207.                 )
  208.             )
  209.         );
  210.         // return the cached value if the $productIds were already used to query visibility data
  211.         if (
  212.             !empty($this->visibilityRequestIds)
  213.             && !empty($this->visibilityRequestData)
  214.             && empty(array_diff($this->visibilityRequestIds$productIds))
  215.         ) {
  216.             return $this->visibilityRequestData;
  217.         }
  218.         $criteria = new Criteria();
  219.         $criteria->addFilter(new EqualsFilter('salesChannelId'$salesChannelId));
  220.         $criteria->addFilter(new EqualsAnyFilter('productId'$productIds));
  221.         $criteria->addAssociation('productData');
  222.         $productDataCriteria $criteria->getAssociation('productData');
  223.         $productDataCriteria->addAssociation('deliveryTime');
  224.         $productVisibilityData $this->productVisibilityRepository->search($criteria$event->getContext())->getElements();
  225.         // cache $productIds used for the query
  226.         $this->visibilityRequestIds $productIds;
  227.         // group by 'productId' field value
  228.         // cache product visibility data
  229.         $this->visibilityRequestData array_column($productVisibilityDatanull'productId');
  230.         return $this->visibilityRequestData;
  231.     }
  232.     private function processProduct(Entity $productEntity $productDatastring $salesChannelIdbool $isNew): void
  233.     {
  234.         foreach (self::CHECK_CONFIGURATION_FIELDS as $configField => $fieldsToUpdate) {
  235.             if ($isNew) {
  236.                 $isEnabledField $this->productDataService->isConfigFieldActive($configField$salesChannelId);
  237.                 if (!$isEnabledField) {
  238.                     continue;
  239.                 }
  240.             }
  241.             foreach ($fieldsToUpdate as $field) {
  242.                 $getMethod $this->generateGetSetMethod($field);
  243.                 $setMethod $this->generateGetSetMethod($field'set');
  244.                 if (method_exists($productData$getMethod)) {
  245.                     $setValue $productData->{$getMethod}();
  246.                     if (method_exists($product$setMethod)) {
  247.                         $product->{$setMethod}($setValue);
  248.                     }
  249.                 }
  250.             }
  251.         }
  252.     }
  253.     private function generateGetSetMethod(string $namestring $type 'get'): string
  254.     {
  255.         if (!\in_array($type, ['get''set'], true)) {
  256.             $type 'get';
  257.         }
  258.         return $type ucwords($name);
  259.     }
  260. }