<?php declare(strict_types=1);
namespace H1web\Blog\Blog\Listing;
use Doctrine\DBAL\Connection;
use H1web\Blog\Blog\Events\BlogListingCriteriaEvent;
use H1web\Blog\Blog\Events\BlogListingResultEvent;
use Shopware\Core\Content\Property\Aggregate\PropertyGroupOption\PropertyGroupOptionCollection;
use Shopware\Core\Framework\DataAbstractionLayer\Doctrine\FetchModeHelper;
use Shopware\Core\Framework\DataAbstractionLayer\EntityRepositoryInterface;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Aggregation\Bucket\TermsAggregation;
use Shopware\Core\Framework\DataAbstractionLayer\Search\AggregationResult\Bucket\TermsResult;
use Shopware\Core\Framework\DataAbstractionLayer\Search\AggregationResult\Metric\EntityResult;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsAnyFilter;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\MultiFilter;
use Shopware\Core\Framework\Uuid\Uuid;
use Shopware\Core\System\SalesChannel\SalesChannelContext;
use Shopware\Core\System\SystemConfig\SystemConfigService;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Request;
class BlogListingFeaturesSubscriber implements EventSubscriberInterface
{
public const DEFAULT_SORT = 'publishedAt--desc';
public const DEFAULT_SEARCH_SORT = 'score';
public const DEFAULT_POSTS_PER_PAGE = 24;
/**
* @var EntityRepositoryInterface
*/
private $optionRepository;
/**
* @var BlogListingSortingRegistry
*/
private $sortingRegistry;
/**
* @var Connection
*/
private $connection;
/**
* @var SystemConfigService
*/
private $configService;
/**
* @var string
*/
private $defaultSort;
/**
* @var int
*/
private $defaultPagesize;
/**
* BlogListingFeaturesSubscriber constructor.
* @param Connection $connection
* @param EntityRepositoryInterface $optionRepository
* @param BlogListingSortingRegistry $sortingRegistry
*/
public function __construct(
Connection $connection,
EntityRepositoryInterface $optionRepository,
BlogListingSortingRegistry $sortingRegistry,
SystemConfigService $configService
)
{
$this->optionRepository = $optionRepository;
$this->connection = $connection;
$this->sortingRegistry = $sortingRegistry;
$this->configService = $configService;
}
/**
* @return array
*/
public static function getSubscribedEvents()
{
return [
BlogListingCriteriaEvent::class => [
['handleListingRequest', 100],
['switchFilter', -100],
],
BlogListingResultEvent::class => 'handleResult',
];
}
/**
* @param BlogListingCriteriaEvent $event
*/
public function switchFilter(BlogListingCriteriaEvent $event): void
{
$request = $event->getRequest();
$criteria = $event->getCriteria();
if ($request->get('no-aggregations') === true) {
$criteria->resetAggregations();
}
// switch all post filters to normal filters to reduce remaining aggregations
if ($request->get('reduce-aggregations')) {
foreach ($criteria->getPostFilters() as $filter) {
$criteria->addFilter($filter);
}
$criteria->resetPostFilters();
}
if ($request->get('only-aggregations') === true) {
// set limit to zero to fetch no blogs.
$criteria->setLimit(0);
// no total count required
$criteria->setTotalCountMode(Criteria::TOTAL_COUNT_MODE_NONE);
// sorting and association are only required for the blog data
$criteria->resetSorting();
$criteria->resetAssociations();
}
}
/**
* @param BlogListingCriteriaEvent $event
*/
public function handleListingRequest(BlogListingCriteriaEvent $event): void
{
$request = $event->getRequest();
$criteria = $event->getCriteria();
$this->handlePagination($request, $criteria);
$this->handleFilters($request, $criteria);
$this->handleSorting($request, $criteria, $event->getSalesChannelContext());
}
/**
* @param BlogListingResultEvent $event
*/
public function handleResult(BlogListingResultEvent $event): void
{
$this->groupOptionAggregations($event);
$this->addCurrentFilters($event);
$currentSorting = $this->getCurrentSorting($event->getRequest());
$event->getResult()->setSorting($currentSorting);
$sortings = $this->sortingRegistry->getSortings();
/** @var BlogListingSorting $sorting */
foreach ($sortings as $sorting) {
$sorting->setActive($sorting->getKey() === $currentSorting);
}
$event->getResult()->setSortings($sortings);
$event->getResult()->setPage($this->getPage($event->getRequest()));
$event->getResult()->setLimit($this->getLimit($event->getRequest()));
}
/**
* @param Request $request
* @param Criteria $criteria
*/
private function handleFilters(Request $request, Criteria $criteria): void
{
$this->handlePropertyFilter($request, $criteria);
}
/**
* @param Request $request
* @param Criteria $criteria
*/
private function handlePagination(Request $request, Criteria $criteria): void
{
$limit = $this->getLimit($request);
$page = $this->getPage($request);
$criteria->setOffset(($page - 1) * $limit);
$criteria->setLimit($limit);
$criteria->setTotalCountMode(Criteria::TOTAL_COUNT_MODE_EXACT);
}
/**
* @param Request $request
* @param Criteria $criteria
*/
private function handlePropertyFilter(Request $request, Criteria $criteria): void
{
$criteria->addAggregation(
new TermsAggregation('properties', 'h1webblog_blog.properties.id')
);
$criteria->addAggregation(
new TermsAggregation('options', 'h1webblog_blog.options.id')
);
$ids = $this->getPropertyIds($request);
if (empty($ids)) {
return;
}
$grouped = $this->connection->fetchAll(
'SELECT LOWER(HEX(property_group_id)) as property_group_id, LOWER(HEX(id)) as id FROM property_group_option WHERE id IN (:ids)',
['ids' => Uuid::fromHexToBytesList($ids)],
['ids' => Connection::PARAM_STR_ARRAY]
);
$grouped = FetchModeHelper::group($grouped);
foreach ($grouped as $options) {
$options = array_column($options, 'id');
$criteria->addPostFilter(
new MultiFilter(
MultiFilter::CONNECTION_OR,
[
new EqualsAnyFilter('h1webblog_blog.optionIds', $options),
new EqualsAnyFilter('h1webblog_blog.propertyIds', $options),
]
)
);
}
}
/**
* @param Request $request
* @param Criteria $criteria
* @param SalesChannelContext|null $context
*/
private function handleSorting(Request $request, Criteria $criteria, SalesChannelContext $context = null): void
{
$currentSorting = $this->getCurrentSorting($request, $context);
if (!$currentSorting) {
return;
}
$sorting = $this->sortingRegistry->get(
$currentSorting
);
if (!$sorting) {
return;
}
foreach ($sorting->createDalSortings() as $fieldSorting) {
$criteria->addSorting($fieldSorting);
}
}
/**
* @param BlogListingResultEvent $event
* @return array
*/
private function collectOptionIds(BlogListingResultEvent $event): array
{
$aggregations = $event->getResult()->getAggregations();
/** @var TermsResult|null $properties */
$properties = $aggregations->get('properties');
/** @var TermsResult|null $options */
$options = $aggregations->get('options');
$options = $options ? $options->getKeys() : [];
$properties = $properties ? $properties->getKeys() : [];
return array_unique(array_filter(array_merge($options, $properties)));
}
/**
* @param BlogListingResultEvent $event
*/
private function groupOptionAggregations(BlogListingResultEvent $event): void
{
$ids = $this->collectOptionIds($event);
if (empty($ids)) {
return;
}
$criteria = new Criteria($ids);
$criteria->addAssociation('group');
$criteria->addAssociation('media');
$result = $this->optionRepository->search($criteria, $event->getContext());
/** @var PropertyGroupOptionCollection $options */
$options = $result->getEntities();
// group options by their property-group
$grouped = $options->groupByPropertyGroups();
$grouped->sortByConfig();
// remove id results to prevent wrong usages
$event->getResult()->getAggregations()->remove('properties');
$event->getResult()->getAggregations()->remove('configurators');
$event->getResult()->getAggregations()->remove('options');
$event->getResult()->getAggregations()->add(new EntityResult('properties', $grouped));
}
/**
* @param BlogListingResultEvent $event
*/
private function addCurrentFilters(BlogListingResultEvent $event): void
{
$event->getResult()->addCurrentFilter('properties', $this->getPropertyIds($event->getRequest()));
}
/**
* @param Request $request
* @return string|null
*/
private function getCurrentSorting(Request $request, SalesChannelContext $context = null): ?string
{
// @deprecated tag:v6.3.0 - use `order` instead
$sort = $request->get('sort', null);
if (is_string($sort) && !empty($sort)) {
$request->query->set('order', $sort);
$request->request->set('order', $sort);
$request->query->set('sort', null);
$request->request->set('sort', null);
}
$key = $request->get('order', $this->getDefaultSort($context));
if (!$key) {
return null;
}
if ($this->sortingRegistry->has($key)) {
return $key;
}
return $this->getDefaultSort($context);
}
/**
* @param Request $request
* @return array
*/
private function getPropertyIds(Request $request): array
{
$ids = $request->query->get('properties', '');
if ($request->isMethod(Request::METHOD_POST)) {
$ids = $request->request->get('properties', '');
}
if (is_string($ids)) {
$ids = explode('|', $ids);
}
return array_filter($ids);
}
/**
* @param Request $request
* @return int
*/
private function getLimit(Request $request): int
{
$default = $this->getDefaultPageSize();
$limit = $request->query->getInt('limit', $default);
if ($request->isMethod(Request::METHOD_POST)) {
$limit = $request->request->getInt('limit', $limit);
}
return $limit <= 0 ? $default : $limit;
}
/**
* @param Request $request
* @return int
*/
private function getPage(Request $request): int
{
$page = $request->query->getInt('p', 1);
if ($request->isMethod(Request::METHOD_POST)) {
$page = $request->request->getInt('p', $page);
}
return $page <= 0 ? 1 : $page;
}
/**
* @param SalesChannelContext|null $context
* @return string|null
*/
public function getDefaultSort(SalesChannelContext $context = null): ?string
{
if ($this->defaultSort === null) {
$salesChannelId = $context ? $context->getSalesChannel()->getId() : null;
$this->defaultSort = $this->configService->get('H1webBlog.config.blogDefaultSortOrder', $salesChannelId) ?: static::DEFAULT_SORT;
}
return $this->defaultSort;
}
/**
* @param SalesChannelContext|null $context
* @return int|null
*/
public function getDefaultPageSize(SalesChannelContext $context = null): ?int
{
if ($this->defaultPagesize === null) {
$salesChannelId = $context ? $context->getSalesChannel()->getId() : null;
$this->defaultPagesize = $this->configService->getInt('H1webBlog.config.blogDefaultPostsPerPage', $salesChannelId) ?: static::DEFAULT_POSTS_PER_PAGE;
}
return $this->defaultPagesize;
}
}