<?php declare(strict_types=1);
namespace Cbax\ModulCrossSelling\Subscriber;
use Doctrine\DBAL\Connection;
use Shopware\Core\Framework\Uuid\Uuid;
use Shopware\Core\Defaults;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Shopware\Storefront\Page\Product\ProductPageLoadedEvent;
use Shopware\Storefront\Page\Checkout\Cart\CheckoutCartPageLoadedEvent;
use Shopware\Storefront\Page\Checkout\Finish\CheckoutFinishPageLoadedEvent;
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\DataAbstractionLayer\Search\Sorting\FieldSorting;
use Shopware\Core\System\SystemConfig\SystemConfigService;
use Shopware\Core\Framework\DataAbstractionLayer\EntityRepositoryInterface;
use Shopware\Core\System\SalesChannel\Entity\SalesChannelRepositoryInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
use Shopware\Core\Content\Product\Aggregate\ProductCrossSelling\ProductCrossSellingEntity;
use Shopware\Core\Content\Product\SalesChannel\CrossSelling\CrossSellingElementCollection;
use Shopware\Core\Content\Product\SalesChannel\CrossSelling\CrossSellingElement;
use Shopware\Core\Content\Product\SalesChannel\CrossSelling\AbstractProductCrossSellingRoute;
use Shopware\Core\Content\Product\ProductCollection;
//use Shopware\Core\Checkout\Order\Aggregate\OrderLineItem\OrderLineItemCollection;
use Shopware\Storefront\Page\Navigation\NavigationPageLoadedEvent;
use Shopware\Storefront\Page\Search\SearchPageLoadedEvent;
use Shopware\Storefront\Page\LandingPage\LandingPageLoadedEvent;
use Cbax\ModulCrossSelling\CbaxModulCrossSelling;
class FrontendSubscriber implements EventSubscriberInterface
{
/**
* @var TranslatorInterface
*/
private $translator;
/**
* @var SystemConfigService
*/
private $systemConfigService;
/** SalesChannelProductRepository */
/**
* @var SalesChannelRepositoryInterface
*/
private $salesChannelProductRepository;
/**
* @var EntityRepositoryInterface
*/
private $productRepository;
/**
* @var EntityRepositoryInterface
*/
private $cbaxCrossSellingRepository;
/**
* @var EntityRepositoryInterface
*/
private $alsoViewedRepository;
/**
* @var EntityRepositoryInterface
*/
private $alsoBoughtRepository;
/**
* @var AbstractProductCrossSellingRoute
*/
private $crossSellingLoader;
/**
* @var CrossSellingElementCollection
*/
private $filteredAccessories;
/**
* @var CrossSellingElementCollection
*/
private $filteredSimilarArticle;
/**
* @var Connection
*/
private $connection;
/**
* @var string
*/
private $shopwareVersion;
public function __construct(
SystemConfigService $systemConfigService,
EntityRepositoryInterface $productRepository,
SalesChannelRepositoryInterface $salesChannelProductRepository,
EntityRepositoryInterface $cbaxCrossSellingRepository,
EntityRepositoryInterface $alsoViewedRepository,
EntityRepositoryInterface $alsoBoughtRepository,
AbstractProductCrossSellingRoute $crossSellingLoader,
Connection $connection,
TranslatorInterface $translator,
$shopwareVersion
)
{
$this->systemConfigService = $systemConfigService;
$this->productRepository = $productRepository;
$this->salesChannelProductRepository = $salesChannelProductRepository;
$this->cbaxCrossSellingRepository = $cbaxCrossSellingRepository;
$this->crossSellingLoader = $crossSellingLoader;
$this->shopwareVersion = $shopwareVersion;
$this->alsoViewedRepository = $alsoViewedRepository;
$this->alsoBoughtRepository = $alsoBoughtRepository;
$this->connection = $connection;
$this->translator = $translator;
}
public static function getSubscribedEvents(): array
{
return[
ProductPageLoadedEvent::class => 'onProductPageLoadedEvent',
CheckoutCartPageLoadedEvent::class => 'onCheckoutCartPageLoadedEvent',
CheckoutFinishPageLoadedEvent::class => ['onOrderFinished', -1],
NavigationPageLoadedEvent::class => 'onNavigationPageLoaded',
SearchPageLoadedEvent::class => 'onSearchPageLoaded',
LandingPageLoadedEvent::class => 'onLandingPageLoaded'
];
}
public function onLandingPageLoaded(LandingPageLoadedEvent $event)
{
$context = $event->getSalesChannelContext();
$salesChannelId = $context->getSalesChannelId();
$config = $this->systemConfigService->get(CbaxModulCrossSelling::MODUL_NAME, $salesChannelId)['config'];
if (!empty($config['showLastSeen']) && !empty($config['lastSeenPageTypes']) && in_array('landing', $config['lastSeenPageTypes'])) {
$lastSeenProducts = $this->getlastSeenProducts($event, $context);
if (count($lastSeenProducts) > 0) {
$page = $event->getPage();
$page->assign(['lastSeenArticle' => $lastSeenProducts]);
}
}
}
public function onSearchPageLoaded(SearchPageLoadedEvent $event)
{
$context = $event->getSalesChannelContext();
$salesChannelId = $context->getSalesChannelId();
$config = $this->systemConfigService->get(CbaxModulCrossSelling::MODUL_NAME, $salesChannelId)['config'];
if (!empty($config['showLastSeen']) && !empty($config['lastSeenPageTypes']) && in_array('search', $config['lastSeenPageTypes'])) {
$lastSeenProducts = $this->getlastSeenProducts($event, $context);
if (count($lastSeenProducts) > 0) {
$page = $event->getPage();
$page->assign(['lastSeenArticle' => $lastSeenProducts]);
}
}
}
private function getlastSeenProducts($event, $context)
{
$session = $event->getRequest()->getSession();
$lastSeen = [];
if ($session->has('cbaxLastSeen')) {
$lastSeen = $session->get('cbaxLastSeen');
$lastSeen = is_array($lastSeen) ? $lastSeen : [];
}
if (count($lastSeen) > 0) {
$lastSeenCriteria = new Criteria();
$lastSeenCriteria->addFilter(new EqualsAnyFilter('id', $lastSeen));
return $this->salesChannelProductRepository->search($lastSeenCriteria, $context)->getElements();
} else {
return [];
}
}
public function onNavigationPageLoaded(NavigationPageLoadedEvent $event)
{
$context = $event->getSalesChannelContext();
$salesChannelId = $context->getSalesChannelId();
$config = $this->systemConfigService->get(CbaxModulCrossSelling::MODUL_NAME, $salesChannelId)['config'];
$page = $event->getPage();
if (!empty($config['showLastSeen']) && !empty($config['lastSeenPageTypes']) && in_array('navigation', $config['lastSeenPageTypes'])) {
$lastSeenProducts = $this->getlastSeenProducts($event, $context);
if (count($lastSeenProducts) > 0) {
$page->assign(['lastSeenArticle' => $lastSeenProducts]);
}
}
}
public function onOrderFinished(CheckoutFinishPageLoadedEvent $event)
{
$context = $event->getSalesChannelContext();
$salesChannelId = $context->getSalesChannelId();
$config = $this->systemConfigService->get(CbaxModulCrossSelling::MODUL_NAME, $salesChannelId)['config'];
if (empty($config['showAlsoBought'])) return;
$order = $event->getPage()->getOrder();
if (!empty($order) and !empty($order->getLineItems()) and $order->getLineItems()->count() > 1)
{
$lineItems = $order->getLineItems();
$combinations = [];
$insertValues = '';
$createdAt = (new \DateTimeImmutable())->format(Defaults::STORAGE_DATE_TIME_FORMAT);
foreach ($lineItems as $key1 => $item1)
{
foreach ($lineItems as $key2 => $item2)
{
if ($key1 === $key2) continue;
if ($item1->getType() !== 'product' || $item2->getType() !== 'product') continue;
$combinations[] = [$item1->getProductId(), $item2->getProductId()];
}
}
foreach ($combinations as $idPair)
{
$insertValues .= '(UNHEX("' . Uuid::randomHex() . '"),UNHEX("' . $idPair[0] . '"),UNHEX("' . $idPair[1] . '"),1,:created_at),';
}
$insertValues = rtrim($insertValues, ', ');
if (!empty($insertValues))
{
try {
$this->connection->executeUpdate('
INSERT INTO `cbax_cross_selling_also_bought`
(`id`, `product_id`, `related_product_id`, `sales`, `created_at`)
VALUES ' . $insertValues . ' ON DUPLICATE KEY UPDATE sales=sales+1;',
[
'created_at' => $createdAt
]
);
} catch (\Exception $e) {
}
}
}
}
public function onProductPageLoadedEvent(ProductPageLoadedEvent $productPageLoadedEvent)
{
$context = $productPageLoadedEvent->getSalesChannelContext();
$salesChannelId = $context->getSalesChannelId();
$config = $this->systemConfigService->get(CbaxModulCrossSelling::MODUL_NAME, $salesChannelId)['config'];
$page = $productPageLoadedEvent->getPage();
if (!empty($config['showLastSeen']) || !empty($config['showAlsoViewed']))
{
$productId = $productPageLoadedEvent->getPage()->getProduct()->getId();
$session = $productPageLoadedEvent->getRequest()->getSession();
$lastSeenLimit = !empty($config['lastSeenLimit']) ? $config['lastSeenLimit'] : 5;
if ($session->has('cbaxLastSeen'))
{
$lastSeen = $session->get('cbaxLastSeen');
if (is_array($lastSeen))
{
$lastSeen = array_filter($lastSeen, function($e) use ($productId) {
return ($e !== $productId);
});
$lastSeen[] = $productId;
} else {
$lastSeen = [$productId];
}
} else {
$lastSeen = [$productId];
}
if (count($lastSeen) > $lastSeenLimit + 1)
{
array_shift($lastSeen);
}
$session->set('cbaxLastSeen', $lastSeen);
array_pop($lastSeen);
if (!empty($config['showLastSeen']) && !empty($config['lastSeenPageTypes']) && in_array('product', $config['lastSeenPageTypes']) && count($lastSeen) > 0)
{
$lastSeenCriteria = new Criteria();
$lastSeenCriteria->addFilter(new EqualsAnyFilter('id', $lastSeen));
$lastSeenProducts = $this->salesChannelProductRepository->search($lastSeenCriteria, $context)->getElements();
if (count($lastSeenProducts) > 0)
{
$page->assign(['lastSeenArticle' => $lastSeenProducts]);
}
}
if (!empty($config['showAlsoViewed']))
{
$alsoViewedLimit = !empty($config['alsoViewedLimit']) ? $config['alsoViewedLimit'] : 5;
$alsoViewedCriteria1 = new Criteria();
$alsoViewedCriteria1->addFilter(new EqualsFilter('productId', $productId));
$alsoViewedCriteria1->addSorting(new FieldSorting('viewed', FieldSorting::DESCENDING));
$alsoViewedCriteria1->setLimit($alsoViewedLimit);
$alsoViewedResult = $this->alsoViewedRepository->search($alsoViewedCriteria1, $context->getContext())->getElements();
if (count($alsoViewedResult) > 0)
{
$alsoViewedIds = array_map(static function($item) {
return $item->getRelatedProductId();
}, $alsoViewedResult);
$alsoViewedCriteria2 = new Criteria($alsoViewedIds);
$alsoViewedCriteria2->addFilter(new EqualsFilter('active', true));
$alsoViewedProducts = $this->salesChannelProductRepository->search($alsoViewedCriteria2, $context);
if ($alsoViewedProducts->getTotal() > 0)
{
if ($config['alsoProductsLocation'] == 'crossSelling')
{
$locale = $this->translator->getLocale() ?? 'en-GB';
$alsoViewedTitle = $this->translator->trans('cbaxCrossSelling.alsoViewed.title', [], null, $locale);
$alsoViewedPCSEntity = new ProductCrossSellingEntity();
$alsoViewedPCSEntity->setActive(true);
$alsoViewedPCSEntity->setId(Uuid::randomHex());
$alsoViewedPCSEntity->setTranslated(['name' => $alsoViewedTitle]);
$alsoViewedPCSEntity->setName($alsoViewedTitle);
$alsoViewedCSElement = new CrossSellingElement();
$alsoViewedCSElement->setCrossSelling($alsoViewedPCSEntity);
$alsoViewedCSElement->setProducts($alsoViewedProducts->getEntities());
$alsoViewedCSElement->setTotal($alsoViewedProducts->getTotal());
} else {
$page->assign(['alsoViewedArticle' => $alsoViewedProducts->getElements()]);
}
}
}
if (count($lastSeen) > 0)
{
$insertValues = '';
$createdAt = (new \DateTimeImmutable())->format(Defaults::STORAGE_DATE_TIME_FORMAT);
$productIdBytes = Uuid::fromHexToBytes($productId);
foreach ($lastSeen as $seenId)
{
$insertValues .= '(UNHEX("' . Uuid::randomHex() . '"),:product_id,UNHEX("' . $seenId . '"),1,:created_at),';
$insertValues .= '(UNHEX("' . Uuid::randomHex() . '"),UNHEX("' . $seenId . '"),:product_id,1,:created_at),';
}
$insertValues = rtrim($insertValues, ', ');
try {
$this->connection->executeUpdate('
INSERT INTO `cbax_cross_selling_also_viewed`
(`id`, `product_id`, `related_product_id`, `viewed`, `created_at`)
VALUES ' . $insertValues . ' ON DUPLICATE KEY UPDATE viewed=viewed+1;',
[
'created_at' => $createdAt,
'product_id' => $productIdBytes
]
);
} catch (\Exception $e) {
}
}
}
}
if (!empty($config['showAlsoBought']) && !empty($productId)) {
$alsoBoughtLimit = !empty($config['alsoBoughtLimit']) ? $config['alsoBoughtLimit'] : 5;
$alsoBoughtCriteria1 = new Criteria();
$alsoBoughtCriteria1->addFilter(new EqualsFilter('productId', $productId));
$alsoBoughtCriteria1->addSorting(new FieldSorting('sales', FieldSorting::DESCENDING));
$alsoBoughtCriteria1->setLimit($alsoBoughtLimit);
$alsoBoughtResult = $this->alsoBoughtRepository->search($alsoBoughtCriteria1, $context->getContext())->getElements();
if (count($alsoBoughtResult) > 0) {
$alsoBoughtIds = array_map(static function ($item) {
return $item->getRelatedProductId();
}, $alsoBoughtResult);
$alsoBoughtCriteria2 = new Criteria($alsoBoughtIds);
$alsoBoughtCriteria2->addFilter(new EqualsFilter('active', true));
$alsoBoughtProducts = $this->salesChannelProductRepository->search($alsoBoughtCriteria2, $context);
if ($alsoBoughtProducts->getTotal() > 0)
{
if ($config['alsoProductsLocation'] == 'crossSelling')
{
if (empty($locale))
{
$locale = $this->translator->getLocale() ?? 'en-GB';
}
$alsoBoughtTitle = $this->translator->trans('cbaxCrossSelling.alsoBought.title', [], null, $locale);
$alsoBoughtPCSEntity = new ProductCrossSellingEntity();
$alsoBoughtPCSEntity->setActive(true);
$alsoBoughtPCSEntity->setId(Uuid::randomHex());
$alsoBoughtPCSEntity->setTranslated(['name' => $alsoBoughtTitle]);
$alsoBoughtPCSEntity->setName($alsoBoughtTitle);
$alsoBoughtCSElement = new CrossSellingElement();
$alsoBoughtCSElement->setCrossSelling($alsoBoughtPCSEntity);
$alsoBoughtCSElement->setProducts($alsoBoughtProducts->getEntities());
$alsoBoughtCSElement->setTotal($alsoBoughtProducts->getTotal());
} else {
$page->assign(['alsoBoughtProducts' => $alsoBoughtProducts->getElements()]);
}
}
}
}
if (empty($config['activeSimilarContainer']) && empty($config['activeAccessoriesTab'])) return;
$cmsPage = $page->getCmsPage();
if (empty($cmsPage))
{
$result = $page->getCrossSellings();
} else
{
$crossSellingElement = $cmsPage->getFirstElementOfType('cross-selling');
if (!empty($crossSellingElement) && !empty($crossSellingElement->getData()))
{
$result = $crossSellingElement->getData()->getCrossSellings();
}
}
// Filtern nur nötig bei echten CS Elementen, wenn nur also.. Pseudo CS Elemnte, nicht nötig
if (!empty($result) && $result->count() > 0)
{
$doFilterung = true;
} else {
$doFilterung = false;
}
if (!empty($alsoViewedCSElement)) {
if (empty($result))
{
$result = new CrossSellingElementCollection();
}
$result->add($alsoViewedCSElement);
}
if (!empty($alsoBoughtCSElement)) {
if (empty($result))
{
$result = new CrossSellingElementCollection();
}
$result->add($alsoBoughtCSElement);
}
if (empty($result)) return;
if ($result->count() === 0) return;
$crossSellingIds = array_map(static function ($item) {
return $item->getCrossSelling()->get('id');
}, $result->getElements());
if (count($crossSellingIds) > 0)
{
// Filtern nur nötig bei echten CS Elementen, wenn nur also... Pseudo CS Elemente, nicht nötig
if ($doFilterung)
{
$this->filteredAccessories = new CrossSellingElementCollection();
$this->filteredSimilarArticle = new CrossSellingElementCollection();
$criteria = new Criteria();
$criteria->addFilter(new EqualsAnyFilter('crossSellingId', $crossSellingIds));
$criteria->addAssociation('crossSelling');
$crossSellings = $this->cbaxCrossSellingRepository->search($criteria, $context->getContext())->getElements();
$filtered = $result->filter(function ($element) use ($crossSellings, $config) {return $this->filterData($element, $crossSellings, $config);});
$result = $filtered;
$page->assign(['cbaxCrossSelling' => $crossSellings]);
$page->assign(['filteredAccessories' => $this->filteredAccessories]);
$page->assign(['filteredSimilarArticle' => $this->filteredSimilarArticle]);
}
if (empty($cmsPage))
{
$page->setCrossSellings($result);
} else {
$crossSellingElement->getData()->setCrossSellings($result);
}
if (version_compare($this->shopwareVersion, '6.4.11', '>='))
{
$page->assign(['cbaxSW6411' => true]);
} else {
$page->assign(['cbaxSW6411' => false]);
}
}
}
public function filterData(CrossSellingElement $element, array $crossSellings, $config)
{
$accessoriesIds = array();
$similarArticleIds = array();
foreach ($crossSellings as $item) {
if ($item->getCrossSellingGroup() === 'accessories') {
$accessoriesIds[] = $item->crossSellingId;
}
if ($item->getCrossSellingGroup() === 'similar_article') {
$similarArticleIds[] = $item->crossSellingId;
}
}
if (in_array($element->getCrossSelling()->get('id'), $accessoriesIds)) {
$this->filteredAccessories->add($element);
if (!empty($config['filterAccessories'])) {
return false;
}
}
if (in_array($element->getCrossSelling()->get('id'), $similarArticleIds)) {
$this->filteredSimilarArticle->add($element);
if (!empty($config['filterSimilarArticle'])) {
return false;
}
}
return true;
}
public function onCheckoutCartPageLoadedEvent(CheckoutCartPageLoadedEvent $checkoutCartPageLoadedEvent)
{
$salesChannelId = $checkoutCartPageLoadedEvent->getSalesChannelContext()->getSalesChannelId();
$config = $this->systemConfigService->get(CbaxModulCrossSelling::MODUL_NAME, $salesChannelId)['config'];
if (empty($config['activeAccessoriesInCart'])) return;
$page = $checkoutCartPageLoadedEvent->getPage();
$lineItems = $page->getCart()->getLineItems();
$filteredLineItems = $lineItems->filterType('product');
if ($filteredLineItems->count() > 0)
{
$context = $checkoutCartPageLoadedEvent->getSalesChannelContext();
$emptyCriteria = new Criteria();
$criteria = new Criteria();
$criteria->addFilter(new EqualsAnyFilter('id', $filteredLineItems->getKeys()));
$criteria->addAssociation('crossSellings');
$products = $this->productRepository->search($criteria, $context->getContext());
$crossSellingIds = array();
foreach($products->getElements() as $product)
{
$crossSellingIds = array_merge($crossSellingIds, $product->getCrossSellings()->getIds());
}
if (count($crossSellingIds) > 0)
{
$request = $checkoutCartPageLoadedEvent->getRequest();
$this->filteredAccessories = new CrossSellingElementCollection();
$this->filteredSimilarArticle = new CrossSellingElementCollection();
$criteria = new Criteria();
$criteria->addFilter(new EqualsAnyFilter('crossSellingId', $crossSellingIds));
$criteria->addAssociation('crossSelling');
$cbaxCrossSellings = $this->cbaxCrossSellingRepository->search($criteria, $context->getContext())->getElements();
foreach($filteredLineItems->getKeys() as $id)
{
$elements = $this->crossSellingLoader->load($id, $request, $context, $emptyCriteria)->getResult();
foreach($elements->getElements() as $element)
{
$this->sortData($element, $cbaxCrossSellings);
}
}
$this->filterSameAccessories($filteredLineItems->getKeys(), $config);
if (!empty($config['accessoriesCartOneSlider']) && $this->filteredAccessories->count() > 1)
{
$counter = 0;
$productCollection = new ProductCollection();
foreach ($this->filteredAccessories->getElements() as $index => $crossSellingElement)
{
if ($counter === 0)
{
$remainingElement = $crossSellingElement;
}
foreach ($crossSellingElement->getProducts()->getElements() as $prod)
{
if ($counter < $config['accessoriesCartOneSliderNumber'])
{
$productCollection->add($prod);
$counter++;
} else {
break;
}
}
$this->filteredAccessories->remove($index);
}
$remainingElement->setProducts($productCollection);
$this->filteredAccessories->add($remainingElement);
}
if (version_compare($this->shopwareVersion, '6.4.11', '>='))
{
$page->assign(['cbaxSW6411' => true]);
} else {
$page->assign(['cbaxSW6411' => false]);
}
$page->assign(['cbaxCrossSelling' => $cbaxCrossSellings]);
$page->assign(['filteredAccessories' => $this->filteredAccessories]);
//$page->assign(['filteredSimilarArticle' => $this->filteredSimilarArticle]);
}
}
}
public function sortData($element, $crossSellings)
{
$accessoriesIds = array();
$similarArticleIds = array();
foreach ($crossSellings as $item) {
if ($item->getCrossSellingGroup() === 'accessories') {
$accessoriesIds[] = $item->crossSellingId;
}
if ($item->getCrossSellingGroup() === 'similar_article') {
$similarArticleIds[] = $item->crossSellingId;
}
}
if (in_array($element->getCrossSelling()->get('id'), $accessoriesIds)) {
$this->filteredAccessories->add($element);
}
if (in_array($element->getCrossSelling()->get('id'), $similarArticleIds)) {
$this->filteredSimilarArticle->add($element);
}
}
public function filterSameAccessories($filteredLineItemsIds, $config)
{
$streamIds = array();
$names = array();
foreach ($this->filteredAccessories->getElements() as $index => $crossSellingElement) {
$crossSelling = $crossSellingElement->getCrossSelling();
if ($crossSelling->getType() === 'productStream' && $crossSelling->getProductStreamId() !== null) {
if (!in_array($crossSelling->getProductStreamId(), $streamIds)) {
$streamIds[] = $crossSelling->getProductStreamId();
} else {
$this->filteredAccessories->remove($index);
}
} else if ($crossSelling->getType() === 'productList') {
// Abgleich per Name
if (!in_array($crossSelling->getTranslated()['name'], $names)) {
$names[] = $crossSelling->getTranslated()['name'];
} else {
$this->filteredAccessories->remove($index);
}
}
}
//Produkte schon in cart ausfiltern
if (!empty($config['accessoriesCartFilter']))
{
foreach ($this->filteredAccessories->getElements() as $index => $crossSellingElement)
{
$assignedProducts = $crossSellingElement->getProducts();
$assignedProductIds = $assignedProducts->getIds();
foreach ($assignedProductIds as $productId)
{
if (in_array($productId, $filteredLineItemsIds))
{
$assignedProducts->remove($productId);
}
}
if ($assignedProducts->count() > 0)
{
$crossSellingElement->setProducts($assignedProducts);
} else {
$this->filteredAccessories->remove($index);
}
}
}
}
}