<?php
declare(strict_types=1);
namespace NetInventors\NetiNextProductDetailCms\Subscriber;
use NetInventors\NetiNextProductDetailCms\Core\Content\Product\Aggregate\ProductCms\ProductCmsEntity;
use NetInventors\NetiNextProductDetailCms\Service\PluginConfig;
use NetInventors\NetiNextProductDetailCms\Struct\Position;
use NetInventors\NetiNextProductDetailCms\Struct\ProductCmsStruct;
use Shopware\Core\Content\Cms\SalesChannel\SalesChannelCmsPageLoaderInterface;
use Shopware\Core\Content\ProductStream\Service\ProductStreamBuilderInterface;
use Shopware\Core\Framework\Context;
use Shopware\Core\Framework\DataAbstractionLayer\EntityRepositoryInterface;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
use Shopware\Core\Framework\DataAbstractionLayer\Search\EntitySearchResult;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\MultiFilter;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\NotFilter;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Sorting\FieldSorting;
use Shopware\Core\System\SalesChannel\SalesChannelCollection;
use Shopware\Core\System\SalesChannel\SalesChannelContext;
use Shopware\Storefront\Page\Page;
use Shopware\Storefront\Page\Product\ProductPageLoadedEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Request;
use Twig\Environment;
use Twig\Error\LoaderError;
use Twig\Error\RuntimeError;
use Twig\Error\SyntaxError;
class ProductPageLoaderSubscriber implements EventSubscriberInterface
{
/**
* @var EntityRepositoryInterface
*/
private $productCmsRepository;
/**
* @var SalesChannelCmsPageLoaderInterface
*/
private $cmsPageLoader;
/**
* @var Environment
*/
private $twig;
/**
* @var PluginConfig
*/
private $pluginConfig;
/**
* @var ProductStreamBuilderInterface
*/
private $productStreamBuilder;
/**
* @var EntityRepositoryInterface
*/
private $productRepository;
public function __construct(
PluginConfig $pluginConfig,
EntityRepositoryInterface $productCmsRepository,
SalesChannelCmsPageLoaderInterface $cmsPageLoader,
Environment $twig,
ProductStreamBuilderInterface $productStreamBuilder,
EntityRepositoryInterface $productRepository
) {
$this->pluginConfig = $pluginConfig;
$this->productCmsRepository = $productCmsRepository;
$this->cmsPageLoader = $cmsPageLoader;
$this->twig = $twig;
$this->productStreamBuilder = $productStreamBuilder;
$this->productRepository = $productRepository;
}
public static function getSubscribedEvents(): array
{
return [
ProductPageLoadedEvent::class => 'onProductPageLoaded',
];
}
/**
* Loads the cms pages for the given product (or its parent product)
*
* @param ProductPageLoadedEvent $event
*
* @throws LoaderError
* @throws RuntimeError
* @throws SyntaxError
*/
public function onProductPageLoaded(
ProductPageLoadedEvent $event
): void {
if (false === $this->pluginConfig->isActive()) {
return;
}
$product = $event->getPage()->getProduct();
$cmsPages = $this->getCmsPages($product->getId(), $product->getVersionId(), $event->getContext());
/** If the variant have no cms pages, we load it from it's parent (main variant) */
if (0 === $cmsPages->count() && is_string($product->getParentId())) {
$cmsPages = $this->getCmsPages($product->getParentId(), $product->getVersionId(), $event->getContext());
}
$productCms = new ProductCmsStruct();
/** @var ProductCmsEntity $cmsPage */
foreach ($cmsPages as $cmsPage) {
if (!is_string($cmsPage->getCmsId()) || '' === $cmsPage->getCmsId()) {
continue;
}
$salesChannels = $cmsPage->getSalesChannels();
if ($salesChannels instanceof SalesChannelCollection) {
$assignedSalesChannels = $salesChannels->getElements();
$salesChannelId = $event->getSalesChannelContext()->getSalesChannelId();
if (!isset($assignedSalesChannels[$salesChannelId]) && 0 < $salesChannels->count()) {
continue;
}
}
$html = $this->buildCmsPageContent(
$event->getRequest(),
$event->getSalesChannelContext(),
$cmsPage->getCmsId()
);
if ($cmsPage->isTwigCompiler()) {
$html = $this->twig->render(
$this->twig->createTemplate($html),
[
'page' => $event->getPage(),
]
);
}
if ($cmsPage->getTemplateMarker() && $cmsPage->getTemplateMarker()->isActive()) {
$productCms->addTemplateMarkerContent(
$cmsPage->getTemplateMarker()->getKey(),
$html
);
continue;
}
switch ($cmsPage->getPosition()) {
case Position::REPLACE_DESCRIPTION:
$productCms->addReplaceProductDetailDescriptionContent($html);
break;
case Position::BELOW_DESCRIPTION:
$productCms->addBelowProductDescriptionContent($html);
break;
case Position::ABOVE_DESCRIPTION:
$productCms->addAboveProductDescriptionContent($html);
break;
case Position::ABOVE_DETAIL_DESCRIPTION_TITLE:
$productCms->addAboveProductDetailDescriptionContent($html);
break;
case Position::BELOW_DETAIL_DESCRIPTION_CONTENT:
$productCms->addBelowProductDetailDescriptionContent($html);
break;
case Position::ABOVE_PRODUCT_PROPERTIES:
$productCms->addAboveProductPropertiesContent($html);
break;
case Position::BELOW_PRODUCT_PROPERTIES:
$productCms->addBelowProductPropertiesContent($html);
break;
case Position::REPLACE_PRODUCT_PROPERTIES:
$productCms->addReplaceProductPropertiesContent($html);
break;
case Position::LAST_PAGE_ELEMENT:
$productCms->addLastPageElement($html);
break;
}
}
$product->addExtension('netiProductCms', $productCms);
}
/**
* Builds the html content from the cmsPageId
*
* @param Request $request
* @param SalesChannelContext $context
* @param string $cmsPageId
*
* @return string
* @throws LoaderError
* @throws RuntimeError
* @throws SyntaxError
*/
protected function buildCmsPageContent(
Request $request,
SalesChannelContext $context,
string $cmsPageId
): string {
$pages = $this->cmsPageLoader->load(
$request,
new Criteria([ $cmsPageId ]),
$context
);
return $this->twig->render(
'@Storefront/storefront/page/content/detail.html.twig',
[
'cmsPage' => $pages->first(),
]
);
}
/**
* Loads the associated cms pages from the product
*
* @param string $productId
* @param string $versionId
* @param Context $context
*
* @return EntitySearchResult
*/
protected function getCmsPages(
string $productId,
string $versionId,
Context $context
): EntitySearchResult {
$criteria = new Criteria();
$criteria->addFilter(
new EqualsFilter('productId', $productId),
new MultiFilter(
MultiFilter::CONNECTION_OR,
[
new EqualsFilter('productVersionId', null),
new EqualsFilter('productVersionId', $versionId)
]
)
);
$criteria->addSorting(new FieldSorting('priority', FieldSorting::DESCENDING));
$criteria->addAssociation('salesChannels');
$criteria->addAssociation('templateMarker');
$cmsPages = $this->productCmsRepository->search($criteria, $context);
$this->addCmsPagesByMatchingProductStreams($cmsPages, $productId, $versionId, $context);
$cmsPages->sort(static function(ProductCmsEntity $a, ProductCmsEntity $b) {
return $b->getPriority() - $a->getPriority();
});
return $cmsPages;
}
/**
* Assigns cms pages whose product streams contains the given productId
*
* @param EntitySearchResult $result
* @param string $productId
* @param string $versionId
* @param Context $context
*/
protected function addCmsPagesByMatchingProductStreams(
EntitySearchResult $result,
string $productId,
string $versionId,
Context $context
): void {
$cmsPages = $this->getCmsPagesWithProductStream($context);
foreach ($cmsPages as $cmsPage) {
$productStreamId = $cmsPage->getProductStreamId();
$filters = $this->productStreamBuilder->buildFilters($productStreamId, $context);
$criteria = new Criteria();
$criteria->addFilter(
new EqualsFilter('id', $productId),
new EqualsFilter('versionId', $versionId)
);
$criteria->addFilter(...$filters);
$productIds = $this->productRepository->searchIds($criteria, $context);
if (null !== $productIds->firstId()) {
$result->add($cmsPage);
}
}
}
/**
* Finds all cms pages which have an associated product stream
*
* @param Context $context
*
* @return EntitySearchResult
*/
protected function getCmsPagesWithProductStream(Context $context): EntitySearchResult
{
$criteria = new Criteria();
$criteria->addFilter(
new NotFilter(
NotFilter::CONNECTION_OR,
[
new EqualsFilter('productStreamId', null),
]
)
);
$criteria->addSorting(new FieldSorting('priority', FieldSorting::DESCENDING));
$criteria->addAssociation('templateMarker');
return $this->productCmsRepository->search($criteria, $context);
}
}