<?php declare(strict_types=1);
namespace NewsletterSendinblue\Subscriber;
use Monolog\Logger;
use NewsletterSendinblue\Service\BaseSyncService;
use NewsletterSendinblue\Service\ConfigService;
use NewsletterSendinblue\Traits\HelperTrait;
use Shopware\Core\Content\Product\Aggregate\ProductTranslation\ProductTranslationDefinition;
use Shopware\Core\Content\Product\Aggregate\ProductVisibility\ProductVisibilityDefinition;
use Shopware\Core\Content\Product\ProductDefinition;
use Shopware\Core\Content\Product\ProductEntity;
use Shopware\Core\Content\Product\ProductEvents;
use Shopware\Core\Defaults;
use Shopware\Core\Framework\DataAbstractionLayer\EntityRepositoryInterface;
use Shopware\Core\Framework\DataAbstractionLayer\EntityWriteResult;
use Shopware\Core\Framework\DataAbstractionLayer\Event\EntityDeletedEvent;
use Shopware\Core\Framework\DataAbstractionLayer\Event\EntityWrittenEvent;
use Shopware\Core\Framework\DataAbstractionLayer\Write\Command\ChangeSetAware;
use Shopware\Core\Framework\DataAbstractionLayer\Write\Command\DeleteCommand;
use Shopware\Core\Framework\DataAbstractionLayer\Write\Validation\PreWriteValidationEvent;
use Shopware\Core\Framework\Uuid\Uuid;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use Throwable;
class ProductSubscriber implements EventSubscriberInterface
{
use HelperTrait;
/** @var BaseSyncService */
private $productSyncService;
/** @var EntityRepositoryInterface */
private $systemConfigRepository;
/** @var RequestStack */
private $requestStack;
/** @var ConfigService */
private $configService;
/** @var Logger */
private $logger;
/** @var array */
private $deleteProducts = [];
/** @var bool */
private $onlyVisibilityRemoved = false;
/** @var bool */
private $visibilityAlsoRemoved = false;
/** @var bool */
private $isProductSynced = false;
/**
* @param BaseSyncService $productSyncService
* @param EntityRepositoryInterface $systemConfigRepository
* @param RequestStack $requestStack
* @param ConfigService $configService
* @param Logger $logger
*/
public function __construct(
BaseSyncService $productSyncService,
EntityRepositoryInterface $systemConfigRepository,
RequestStack $requestStack,
ConfigService $configService,
Logger $logger
)
{
$this->productSyncService = $productSyncService;
$this->systemConfigRepository = $systemConfigRepository;
$this->requestStack = $requestStack;
$this->configService = $configService;
$this->logger = $logger;
}
/**
* @return string[]
*/
public static function getSubscribedEvents(): array
{
return [
ProductEvents::PRODUCT_TRANSLATION_WRITTEN_EVENT => 'onProductTranslationWrittenEvent',
ProductEvents::PRODUCT_WRITTEN_EVENT => 'onProductWrittenEvent',
ProductEvents::PRODUCT_DELETED_EVENT => 'onProductDeletedEvent',
ProductEvents::PRODUCT_CATEGORY_WRITTEN_EVENT => 'onProductCategoryChangedEvent',
ProductEvents::PRODUCT_CATEGORY_DELETED_EVENT => 'onProductCategoryChangedEvent',
PreWriteValidationEvent::class => 'onPreWriteValidationEvent',
];
}
/**
* @param PreWriteValidationEvent $event
* @return void
*/
public function onPreWriteValidationEvent(PreWriteValidationEvent $event): void
{
if ($event->getContext()->getVersionId() !== Defaults::LIVE_VERSION) {
return;
}
// Need to check if there are updating fields concerning to productEntity (e.g. stock, price etc).
// For example there can be only product sales channel (visibility) change etc...
$isProductFieldUpdated = $this->isProductFieldUpdated($event->getCommands());
$deletableIds = [];
foreach ($event->getCommands() as $command) {
if (!$command instanceof ChangeSetAware) {
continue;
}
// if one of the sales channels is removed from the product
if ($command->getDefinition()->getEntityName() === ProductVisibilityDefinition::ENTITY_NAME
&& get_class($command) === DeleteCommand::class
) {
if ($isProductFieldUpdated) {
$this->visibilityAlsoRemoved = true;
} else {
$this->onlyVisibilityRemoved = true;
}
}
// if product is removed
if ($command->getDefinition()->getEntityName() === ProductDefinition::ENTITY_NAME
&& get_class($command) === DeleteCommand::class
&& !empty($command->getPrimaryKey()['id'])
) {
$productId = Uuid::fromBytesToHex($command->getPrimaryKey()['id']);
$deletableIds[] = $productId;
$childrenIds = $this->productSyncService->getChildrenIds($productId, $event->getContext());
$deletableIds = array_merge($deletableIds, $childrenIds);
}
}
foreach ($deletableIds as $id) {
$this->deleteProducts[$id] = $this->productSyncService->getEntity($id, $event->getContext());
}
if (!empty($this->deleteProducts)) {
$this->productSyncService->setDeleteEntities($this->deleteProducts);
}
}
/**
* @param EntityDeletedEvent $event
* @return void
*/
public function onProductDeletedEvent(EntityDeletedEvent $event): void
{
$connectionId = $this->getAutoSyncConnectionId(
ConfigService::CONFIG_IS_PRODUCTS_AUTO_SYNC_ENABLED,
$event->getContext()
);
if (empty($connectionId)) {
return;
}
foreach ($event->getWriteResults() as $writeResult) {
$productId = $writeResult->getPrimaryKey();
if (empty($productId)) {
continue;
}
if ($writeResult->getOperation() === EntityWriteResult::OPERATION_DELETE
&& isset($this->deleteProducts[$productId])
&& $this->deleteProducts[$productId] instanceof ProductEntity
) {
$this->productSyncService->syncDelete($this->deleteProducts[$productId], $connectionId, $event->getContext());
}
}
}
/**
* @param EntityWrittenEvent $event
* @return void
*/
public function onProductWrittenEvent(EntityWrittenEvent $event): void
{
$connectionId = $this->getAutoSyncConnectionId(
ConfigService::CONFIG_IS_PRODUCTS_AUTO_SYNC_ENABLED,
$event->getContext()
);
if (empty($connectionId)) {
return;
}
// this variable is not for "product deleted",
// it is in case when all sales channels are removed from the product
$deletableIds = [];
try {
foreach ($event->getWriteResults() as $writeResult) {
$productId = $writeResult->getPrimaryKey();
if (empty($productId)) {
continue;
}
if ($this->isRequestHasSession()
&& $writeResult->getOperation() === EntityWriteResult::OPERATION_INSERT
) {
$this->productSyncService->sync($productId, $connectionId, $event->getContext());
// It is to avoid calling "update" when product is created.
// It will be checked onProductTranslationWrittenEvent
$this->requestStack->getSession()->set('sbProductSynced', true);
}
if ($writeResult->getOperation() === EntityWriteResult::OPERATION_UPDATE
) {
$changeSet = $writeResult->getChangeSet();
if ($changeSet
&& (int)$changeSet->getBefore('active') === 1
&& $writeResult->getChangeSet()->hasChanged('active')
&& (int)$changeSet->getAfter('active') === 0
) {
$deletableIds[] = $productId;
$childrenIds = $this->productSyncService->getChildrenIds($productId, $event->getContext());
$deletableIds = array_merge($deletableIds, $childrenIds);
} elseif ($this->onlyVisibilityRemoved || $this->visibilityAlsoRemoved) {
/** @var ProductEntity $product */
$product = $this->productSyncService->getEntity($productId, $event->getContext());
if ($product->getVisibilities()->count() > 0 && $this->onlyVisibilityRemoved) {
$this->productSyncService->sync($productId, $connectionId, $event->getContext());
}
if ($product->getVisibilities()->count() === 0) {
$deletableIds[] = $productId;
$childrenIds = $this->productSyncService->getChildrenIds($productId, $event->getContext());
$deletableIds = array_merge($deletableIds, $childrenIds);
}
}
}
}
$deletableIds = array_unique($deletableIds);
foreach ($deletableIds as $id) {
$item = $this->productSyncService->getEntity($id, $event->getContext());
if ($item instanceof ProductEntity
&& ($item->getVisibilities()->count() === 0 || !$item->getActive())
) {
$this->productSyncService->syncDelete($item, $connectionId, $event->getContext(), true);
}
}
} catch (Throwable $e) {
}
}
/**
* @param EntityWrittenEvent $event
* @return void
*/
public function onProductCategoryChangedEvent(EntityWrittenEvent $event): void
{
$connectionId = $this->getAutoSyncConnectionId(
ConfigService::CONFIG_IS_PRODUCTS_AUTO_SYNC_ENABLED,
$event->getContext()
);
if (empty($connectionId)) {
return;
}
try {
foreach ($event->getWriteResults() as $writeResult) {
$productId = $writeResult->getPrimaryKey()['productId'];
if (empty($productId)) {
continue;
}
$product = $this->productSyncService->getEntity($productId, $event->getContext());
if ($this->isRequestHasSession()
&& $product instanceof ProductEntity
&& !$this->productSyncService->isCustomFieldEmpty($product)
&& !$this->isProductSynced
) {
$this->productSyncService->sync($productId, $connectionId, $event->getContext());
$this->isProductSynced = true;
break;
}
}
} catch (Throwable $e) {
}
}
/**
* @param EntityWrittenEvent $event
* @return void
*/
public function onProductTranslationWrittenEvent(EntityWrittenEvent $event): void
{
$connectionId = $this->getAutoSyncConnectionId(
ConfigService::CONFIG_IS_PRODUCTS_AUTO_SYNC_ENABLED,
$event->getContext()
);
if (empty($connectionId)) {
return;
}
foreach ($event->getWriteResults() as $writeResult) {
$productId = $writeResult->getPrimaryKey()['productId'];
if (empty($productId)) {
continue;
}
// it is for avoiding call "sync" action when custom field is saved
if (!$this->checkChangeSet($writeResult)) {
continue;
}
if ($this->isRequestHasSession()) {
if ($writeResult->getOperation() === EntityWriteResult::OPERATION_UPDATE
&& !$this->requestStack->getSession()->has('sbProductSynced')
) {
$this->productSyncService->sync($productId, $connectionId, $event->getContext());
$childrenIds = $this->productSyncService->getChildrenIds($productId, $event->getContext());
foreach ($childrenIds as $id) {
$this->productSyncService->sync($id, $connectionId, $event->getContext());
}
$this->isProductSynced = true;
}
$this->requestStack->getSession()->remove('sbProductSynced');
}
}
}
/**
* @return bool
*/
private function isRequestHasSession(): bool
{
if ($this->requestStack
&& $this->requestStack->getCurrentRequest()
&& $this->requestStack->getCurrentRequest()->hasSession()
) {
return true;
}
return false;
}
/**
* @param array $commands
* @return bool
*/
private function isProductFieldUpdated(array $commands): bool
{
foreach ($commands as $command) {
if ($command->getDefinition()->getEntityName() === ProductDefinition::ENTITY_NAME
|| $command->getDefinition()->getEntityName() === ProductTranslationDefinition::ENTITY_NAME
) {
return true;
}
}
return false;
}
}