custom/plugins/NewsletterSendinblue/src/Subscriber/ProductSubscriber.php line 280

Open in your IDE?
  1. <?php declare(strict_types=1);
  2. namespace NewsletterSendinblue\Subscriber;
  3. use Monolog\Logger;
  4. use NewsletterSendinblue\Message\Product\ProductSyncDeleteMessage;
  5. use NewsletterSendinblue\Message\Product\ProductSyncMessage;
  6. use NewsletterSendinblue\NewsletterSendinblue;
  7. use NewsletterSendinblue\Service\BaseSyncService;
  8. use NewsletterSendinblue\Service\ConfigService;
  9. use NewsletterSendinblue\Service\Constant;
  10. use NewsletterSendinblue\Traits\HelperTrait;
  11. use Shopware\Core\Content\Product\ProductEntity;
  12. use Shopware\Core\Content\Product\ProductEvents;
  13. use Shopware\Core\Framework\DataAbstractionLayer\EntityRepositoryInterface;
  14. use Shopware\Core\Framework\DataAbstractionLayer\EntityWriteResult;
  15. use Shopware\Core\Framework\DataAbstractionLayer\Event\EntityDeletedEvent;
  16. use Shopware\Core\Framework\DataAbstractionLayer\Event\EntityWrittenEvent;
  17. use Shopware\Core\Framework\Struct\ArrayStruct;
  18. use Shopware\Storefront\Page\Product\ProductPageLoadedEvent;
  19. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  20. use Symfony\Component\HttpFoundation\RequestStack;
  21. use Symfony\Component\Messenger\MessageBusInterface;
  22. use Throwable;
  23. class ProductSubscriber implements EventSubscriberInterface
  24. {
  25.     use HelperTrait;
  26.     /** @var BaseSyncService */
  27.     private $productSyncService;
  28.     /** @var EntityRepositoryInterface */
  29.     private $systemConfigRepository;
  30.     /** @var RequestStack */
  31.     private $requestStack;
  32.     /** @var ConfigService */
  33.     private $configService;
  34.     /** @var Logger */
  35.     private $logger;
  36.     /** @var MessageBusInterface */
  37.     private $messageBus;
  38.     /** @var array */
  39.     private $stockUpdatedProducts = [];
  40.     /** @var array */
  41.     private $syncedProductIds = [];
  42.     /**
  43.      * @param BaseSyncService $productSyncService
  44.      * @param EntityRepositoryInterface $systemConfigRepository
  45.      * @param RequestStack $requestStack
  46.      * @param ConfigService $configService
  47.      * @param Logger $logger
  48.      * @param MessageBusInterface $messageBus
  49.      */
  50.     public function __construct(
  51.         BaseSyncService           $productSyncService,
  52.         EntityRepositoryInterface $systemConfigRepository,
  53.         RequestStack              $requestStack,
  54.         ConfigService             $configService,
  55.         Logger                    $logger,
  56.         MessageBusInterface       $messageBus
  57.     )
  58.     {
  59.         $this->productSyncService $productSyncService;
  60.         $this->systemConfigRepository $systemConfigRepository;
  61.         $this->requestStack $requestStack;
  62.         $this->configService $configService;
  63.         $this->logger $logger;
  64.         $this->messageBus $messageBus;
  65.     }
  66.     /**
  67.      * @return string[]
  68.      */
  69.     public static function getSubscribedEvents(): array
  70.     {
  71.         return [
  72.             ProductEvents::PRODUCT_TRANSLATION_WRITTEN_EVENT => 'onProductTranslationWrittenEvent',
  73.             ProductEvents::PRODUCT_WRITTEN_EVENT => 'onProductWrittenEvent',
  74.             ProductEvents::PRODUCT_DELETED_EVENT => 'onProductDeletedEvent',
  75.             ProductEvents::PRODUCT_CATEGORY_WRITTEN_EVENT => 'onProductCategoryChangedEvent',
  76.             ProductEvents::PRODUCT_CATEGORY_DELETED_EVENT => 'onProductCategoryChangedEvent',
  77.             ProductPageLoadedEvent::class => 'onProductPageLoadedEvent'
  78.         ];
  79.     }
  80.     /**
  81.      * @param ProductPageLoadedEvent $event
  82.      * @return void
  83.      */
  84.     public function onProductPageLoadedEvent(ProductPageLoadedEvent $event): void
  85.     {
  86.         $isBackInStockSyncEnabled false;
  87.         $this->configService->setSalesChannelId($event->getSalesChannelContext()->getSalesChannelId());
  88.         if ($this->configService->isBackInStockSyncEnabled()) {
  89.             $isBackInStockSyncEnabled true;
  90.         }
  91.         $event->getPage()->addExtension('brevoProductPage', new ArrayStruct([
  92.             'isBackInStockSyncEnabled' => $isBackInStockSyncEnabled
  93.         ]));
  94.     }
  95.     /**
  96.      * @param EntityDeletedEvent $event
  97.      * @return void
  98.      */
  99.     public function onProductDeletedEvent(EntityDeletedEvent $event): void
  100.     {
  101.         $connectionId $this->getAutoSyncConnectionId(
  102.             ConfigService::CONFIG_IS_PRODUCTS_AUTO_SYNC_ENABLED,
  103.             $event->getContext()
  104.         );
  105.         if (empty($connectionId)) {
  106.             return;
  107.         }
  108.         foreach ($event->getWriteResults() as $writeResult) {
  109.             $productId $writeResult->getPrimaryKey();
  110.             if (empty($productId)) {
  111.                 continue;
  112.             }
  113.             $deleteProducts $event->getContext()->hasExtension('brevoPreWriteEvent')
  114.                 ? $event->getContext()->getExtension('brevoPreWriteEvent')->get('deleteProducts')
  115.                 : null;
  116.             if ($writeResult->getOperation() === EntityWriteResult::OPERATION_DELETE
  117.                 && !empty($deleteProducts)
  118.                 && isset($deleteProducts[$productId])
  119.                 && $deleteProducts[$productId] instanceof ProductEntity
  120.             ) {
  121.                 $this->productSyncService->syncDelete($deleteProducts[$productId], $connectionId$event->getContext());
  122.             }
  123.         }
  124.         if (!empty($deleteProducts)) {
  125.             $event->getContext()->getExtension('brevoPreWriteEvent')->set('deleteProducts', []);
  126.             $this->productSyncService->setDeleteEntities([]);
  127.         }
  128.     }
  129.     /**
  130.      * @param EntityWrittenEvent $event
  131.      * @return void
  132.      */
  133.     public function onProductWrittenEvent(EntityWrittenEvent $event): void
  134.     {
  135.         if ($event->getContext()->hasExtension(NewsletterSendinblue::IGNORE_EVENT)) {
  136.             return;
  137.         }
  138.         $connectionId $this->getAutoSyncConnectionId(
  139.             ConfigService::CONFIG_IS_PRODUCTS_AUTO_SYNC_ENABLED,
  140.             $event->getContext()
  141.         );
  142.         if (empty($connectionId)) {
  143.             return;
  144.         }
  145.         $syncProductIds = [];
  146.         $stockUpdatedProductIds = [];
  147.         $onlyVisibilityRemoved $event->getContext()->hasExtension('brevoPreWriteEvent')
  148.             ? $event->getContext()->getExtension('brevoPreWriteEvent')->get('onlyVisibilityRemoved')
  149.             : null;
  150.         $visibilityAlsoRemoved $event->getContext()->hasExtension('brevoPreWriteEvent')
  151.             ? $event->getContext()->getExtension('brevoPreWriteEvent')->get('visibilityAlsoRemoved')
  152.             : null;
  153.         // this variable is not for "product deleted",
  154.         // it is in case when all sales channels are removed from the product
  155.         $deletableIds = [];
  156.         try {
  157.             foreach ($event->getWriteResults() as $writeResult) {
  158.                 $productId $writeResult->getPrimaryKey();
  159.                 if (empty($productId)) {
  160.                     continue;
  161.                 }
  162.                 if ($writeResult->getOperation() === EntityWriteResult::OPERATION_INSERT) {
  163.                     $syncProductIds[] = $productId;
  164.                     // Track specific product IDs that have been synced
  165.                     // It is to avoid calling "update" when the product is created.
  166.                     // It will be checked onProductTranslationWrittenEvent
  167.                     $this->syncedProductIds[] = $productId;
  168.                 }
  169.                 if ($writeResult->getOperation() === EntityWriteResult::OPERATION_UPDATE
  170.                 ) {
  171.                     $changeSet $writeResult->getChangeSet();
  172.                     if ($changeSet
  173.                         && $changeSet->hasChanged('stock')
  174.                     ) {
  175.                         $stockUpdatedProductIds[] = $productId;
  176.                     }
  177.                     if ($changeSet
  178.                         && ((int)$changeSet->getBefore('active') === 1
  179.                             || $changeSet->getBefore('active') === null// for variant
  180.                         && $writeResult->getChangeSet()->hasChanged('active')
  181.                         && (int)$changeSet->getAfter('active') === 0
  182.                     ) {
  183.                         $deletableIds[] = $productId;
  184.                         $childrenIds $this->productSyncService->getChildrenIds($productId$event->getContext());
  185.                         $deletableIds array_merge($deletableIds$childrenIds);
  186.                     } elseif ($onlyVisibilityRemoved || $visibilityAlsoRemoved) {
  187.                         /** @var ProductEntity $product */
  188.                         $product $this->productSyncService->getEntity($productId$event->getContext());
  189.                         if ($product->getVisibilities()->count() > && $onlyVisibilityRemoved) {
  190.                             $syncProductIds[] = $productId;
  191.                             $this->syncedProductIds[] = $productId;
  192.                         }
  193.                         if ($product->getVisibilities()->count() === 0) {
  194.                             // if this is variant - need to get the parent product and check visibility for parent
  195.                             $parentProductForVisibility null;
  196.                             if ($product->getParentId()) {
  197.                                 $parentProductForVisibility $this->productSyncService->getEntity($product->getParentId(), $event->getContext());
  198.                             }
  199.                             if (!$product->getParentId()
  200.                                 || ($parentProductForVisibility && $parentProductForVisibility->getVisibilities()->count() === 0)
  201.                             ) {
  202.                                 $deletableIds[] = $productId;
  203.                                 $childrenIds $this->productSyncService->getChildrenIds($productId$event->getContext());
  204.                                 $deletableIds array_merge($deletableIds$childrenIds);
  205.                             }
  206.                         }
  207.                     }
  208.                 }
  209.             }
  210.             if (!empty($stockUpdatedProductIds)) {
  211.                 /*
  212.                  * it will look like this
  213.                  *  [
  214.                  *     'availableStock' => [
  215.                  *         [
  216.                  *             'product_id' => '<productId>',
  217.                  *             'open_quantity' => '<quantityFromNonCompletedOrders>',
  218.                  *             'sales_quantity' => '<quantityFromCompletedOrders>'
  219.                  *         ]
  220.                  *     ]
  221.                  *  ]
  222.                  */
  223.                 $calculatedAvailableStocks $this->productSyncService->getAdditionalData([
  224.                     'availableStock' => $stockUpdatedProductIds
  225.                 ], $event->getContext());
  226.                 $this->stockUpdatedProducts = !empty($calculatedAvailableStocks['availableStock'])
  227.                     ? array_combine(array_column($calculatedAvailableStocks['availableStock'], 'product_id'), $calculatedAvailableStocks['availableStock'])
  228.                     : ['availableStock' => []];
  229.             }
  230.             // Handle sync operations
  231.             $this->handleSyncProducts($syncProductIds$connectionId$event->getContext());
  232.             // Handle delete operations
  233.             $deletableIds array_unique($deletableIds);
  234.             $this->handleSyncDeleteProductIds($deletableIds$connectionId$event->getContext());
  235.         } catch (Throwable $e) {
  236.         }
  237.     }
  238.     /**
  239.      * @param EntityWrittenEvent $event
  240.      * @return void
  241.      */
  242.     public function onProductCategoryChangedEvent(EntityWrittenEvent $event): void
  243.     {
  244.         $connectionId $this->getAutoSyncConnectionId(
  245.             ConfigService::CONFIG_IS_PRODUCTS_AUTO_SYNC_ENABLED,
  246.             $event->getContext()
  247.         );
  248.         if (empty($connectionId)) {
  249.             return;
  250.         }
  251.         $syncProductIds = [];
  252.         try {
  253.             foreach ($event->getWriteResults() as $writeResult) {
  254.                 $productId $writeResult->getPrimaryKey()['productId'];
  255.                 if (empty($productId)) {
  256.                     continue;
  257.                 }
  258.                 $product $this->productSyncService->getEntity($productId$event->getContext());
  259.                 if ($product instanceof ProductEntity
  260.                     && !$this->productSyncService->isCustomFieldEmpty($product)
  261.                     && !in_array($productId$this->syncedProductIdstrue)
  262.                 ) {
  263.                     $syncProductIds[] = $productId;
  264.                     $this->syncedProductIds[] = $productId;
  265.                 }
  266.             }
  267.             $this->handleSyncProducts($syncProductIds$connectionId$event->getContext());
  268.         } catch (Throwable $e) {
  269.         }
  270.     }
  271.     /**
  272.      * @param EntityWrittenEvent $event
  273.      * @return void
  274.      */
  275.     public function onProductTranslationWrittenEvent(EntityWrittenEvent $event): void
  276.     {
  277.         if ($event->getContext()->hasExtension(NewsletterSendinblue::IGNORE_EVENT)) {
  278.             return;
  279.         }
  280.         $connectionId $this->getAutoSyncConnectionId(
  281.             ConfigService::CONFIG_IS_PRODUCTS_AUTO_SYNC_ENABLED,
  282.             $event->getContext()
  283.         );
  284.         if (empty($connectionId)) {
  285.             return;
  286.         }
  287.         $syncProductIds = [];
  288.         foreach ($event->getWriteResults() as $writeResult) {
  289.             $productId $writeResult->getPrimaryKey()['productId'];
  290.             if (empty($productId)) {
  291.                 continue;
  292.             }
  293.             // it is for avoiding call "sync" action when custom field is saved
  294.             if (!$this->checkChangeSet($writeResult)) {
  295.                 continue;
  296.             }
  297.             if (($writeResult->getOperation() === EntityWriteResult::OPERATION_UPDATE
  298.                     || $writeResult->getOperation() === EntityWriteResult::OPERATION_INSERT)
  299.                 && !in_array($productId$this->syncedProductIdstrue)
  300.             ) {
  301.                 $syncProductIds[] = $productId;
  302.                 // Also sync children
  303.                 $childrenIds $this->productSyncService->getChildrenIds($productId$event->getContext());
  304.                 $syncProductIds array_merge($syncProductIds$childrenIds);
  305.                 $this->syncedProductIds[] = $productId;
  306.                 // Also mark children as synced to avoid duplicate processing
  307.                 $this->syncedProductIds array_merge($this->syncedProductIds$childrenIds);
  308.             }
  309.         }
  310.         // Handle sync with custom context (including stock data)
  311.         $customizedContext $event->getContext();
  312.         if (!empty($this->stockUpdatedProducts)) {
  313.             $customizedContext->addExtension('availableStock', new ArrayStruct([
  314.                 'value' => $this->stockUpdatedProducts
  315.             ]));
  316.         }
  317.         $this->handleSyncProducts($syncProductIds$connectionId$customizedContext);
  318.         $this->syncedProductIds = [];
  319.     }
  320.     /**
  321.      * @param array $productIds
  322.      * @param string $connectionId
  323.      * @param $context
  324.      * @return void
  325.      */
  326.     private function handleSyncProducts(array $productIdsstring $connectionId$context): void
  327.     {
  328.         if (empty($productIds)) {
  329.             return;
  330.         }
  331.         if (count($productIds) >= Constant::ASYNC_THRESHOLD) {
  332.             // Use message queue for batch operations
  333.             foreach ($productIds as $productId) {
  334.                 $this->messageBus->dispatch(new ProductSyncMessage(
  335.                     $productId,
  336.                     $connectionId,
  337.                     $context
  338.                 ));
  339.             }
  340.         } else {
  341.             // Process synchronously
  342.             foreach ($productIds as $productId) {
  343.                 $this->productSyncService->sync($productId$connectionId$context);
  344.             }
  345.         }
  346.     }
  347.     /**
  348.      * @param array $productEntities
  349.      * @param string $connectionId
  350.      * @param $context
  351.      * @return void
  352.      */
  353.     private function handleSyncDeleteProducts(array $productEntitiesstring $connectionId$context): void
  354.     {
  355.         if (empty($productEntities)) {
  356.             return;
  357.         }
  358.         if (count($productEntities) >= Constant::ASYNC_THRESHOLD) {
  359.             // Use message queue for batch operations
  360.             foreach ($productEntities as $product) {
  361.                 $this->messageBus->dispatch(new ProductSyncDeleteMessage(
  362.                     $product->getId(),
  363.                     $connectionId,
  364.                     $context,
  365.                     false // isDeactivation flag
  366.                 ));
  367.             }
  368.         } else {
  369.             // Process synchronously
  370.             foreach ($productEntities as $product) {
  371.                 $this->productSyncService->syncDelete($product$connectionId$context);
  372.             }
  373.         }
  374.     }
  375.     /**
  376.      * @param array $productIds
  377.      * @param string $connectionId
  378.      * @param $context
  379.      * @return void
  380.      */
  381.     private function handleSyncDeleteProductIds(array $productIdsstring $connectionId$context): void
  382.     {
  383.         if (empty($productIds)) {
  384.             return;
  385.         }
  386.         if (count($productIds) >= Constant::ASYNC_THRESHOLD) {
  387.             // Use message queue for batch operations
  388.             foreach ($productIds as $productId) {
  389.                 $this->messageBus->dispatch(new ProductSyncDeleteMessage(
  390.                     $productId,
  391.                     $connectionId,
  392.                     $context,
  393.                     true // isDeactivation flag
  394.                 ));
  395.             }
  396.         } else {
  397.             // Process synchronously
  398.             foreach ($productIds as $productId) {
  399.                 $item $this->productSyncService->getEntity($productId$context);
  400.                 if ($item instanceof ProductEntity
  401.                     && ($item->getVisibilities()->count() === || !$item->getActive())
  402.                 ) {
  403.                     $this->productSyncService->syncDelete($item$connectionId$contexttrue);
  404.                 }
  405.             }
  406.         }
  407.     }
  408. }