Files
kupshop/bundles/KupShop/CatalogBundle/Util/SectionUtil.php
2025-08-02 16:30:27 +02:00

345 lines
12 KiB
PHP

<?php
namespace KupShop\CatalogBundle\Util;
use Doctrine\DBAL\Connection;
use KupShop\CatalogBundle\Section\SectionTree;
use KupShop\ContentBundle\Util\SliderUtil;
use KupShop\I18nBundle\Translations\SectionsTranslation;
use KupShop\KupShopBundle\Context\LanguageContext;
use KupShop\KupShopBundle\Exception\PermanentRedirectException;
use KupShop\KupShopBundle\Util\StringUtil;
use Query\Filter;
use Query\Operator;
use Query\QueryBuilder;
use Query\Translation;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Contracts\Service\Attribute\Required;
class SectionUtil
{
private $languageContext;
private $sectionsTranslation;
private $productsFilterSpecs;
private ?SliderUtil $sliderUtil;
private ?SectionTree $sectionTree;
public function __construct(LanguageContext $languageContext, ?SectionsTranslation $sectionsTranslation = null)
{
$this->languageContext = $languageContext;
$this->sectionsTranslation = $sectionsTranslation;
}
#[Required]
final public function setSliderUtil(SliderUtil $sliderUtil): void
{
$this->sliderUtil = $sliderUtil;
}
#[Required]
final public function setSectionTree(SectionTree $sectionTree): void
{
$this->sectionTree = $sectionTree;
}
/**
* @return $this
*
* @required
*/
public function setProductsFilterSpecs(ProductsFilterSpecs $productsFilterSpecs)
{
$this->productsFilterSpecs = $productsFilterSpecs;
return $this;
}
public function fetchSlidersIntoSectionsCache(?array $ids = null): void
{
$ids = $this->getDescendantCategories($ids);
$sliders = $this->sliderUtil->fetchSliders(
function (QueryBuilder $qb) use ($ids) {
$sectionPositions = sqlQueryBuilder()
->select('DISTINCT sis.id_slider', 'sis.position', 'sis.id_section')
->from('sliders_in_sections', 'sis')
->andWhere(Operator::inIntArray($ids, 'sis.id_section'));
$qb->addSelect('JSON_OBJECT("id_section", sis.id_section, "position", sis.position) as slider_key')
->joinSubQuery('sl', $sectionPositions, 'sis', 'sl.id = sis.id_slider');
},
'slider_key'
);
$data = [];
foreach ($sliders as $key => $slider) {
$info = json_decode($key, true);
$data[$info['id_section']][$info['position']] = $slider;
}
foreach ($data as $sectionId => $sliders) {
$this->sectionTree->getSectionById($sectionId)?->setSliders($sliders);
}
}
public function generateVirtualSections(?array $ids = null)
{
$virtualSections = sqlQueryBuilder()->select('*')
->from('sections')
->where(Operator::equals(['virtual' => 'Y']));
if ($ids) {
$virtualSections->andWhere(Operator::inIntArray($ids, 'id'));
}
try {
$nonVirtualSectionsIds = sqlQueryBuilder()->select('id')
->from('sections')
->where(Operator::equals(['virtual' => 'N']));
sqlQueryBuilder()->delete('products_in_sections')
->where(Operator::equals(['generated' => 1]))
->andWhere(Operator::inSubQuery('id_section', $nonVirtualSectionsIds))
->execute();
} catch (\Exception $e) {
getRaven()->captureException($e);
}
$dbcfg = \Settings::getDefault();
foreach ($virtualSections->execute() as $section) {
$data = json_decode($section['data'], true) ?? [];
$filter = $data['virtual_settings'] ?? [];
try {
sqlGetConnection()->transactional(function () use ($section, $filter, $dbcfg) {
$existingProducts = sqlQueryBuilder()->select('old_p.id_product id')->from('products_in_sections', 'old_p')
->where(Operator::equals(['old_p.id_section' => $section['id'], 'old_p.generated' => 1]));
$newProducts = sqlQueryBuilder()->select('p.id')
->from('products', 'p')
->leftJoin('p', 'products_in_sections', 'ps_filter', 'p.id = ps_filter.id_product AND ps_filter.id_section = :id_section AND ps_filter.generated = 0')
->setParameter('id_section', $section['id'])
->where('ps_filter.id_product IS NULL') // odfiltrovat produkty, ktere v te sekci uz zarazeny jsou
->andWhere($this->productsFilterSpecs->getSpecs($filter))
->groupBy('p.id');
$newProducts->andWhere(Filter::hasCategory($dbcfg->prod_show_not_in_category == 'N', false));
$deleteProductsIds = (clone $existingProducts)
->andWhere(Operator::not(Operator::inSubQuery('old_p.id_product', $newProducts)))
->execute()->fetchFirstColumn();
if (!empty($deleteProductsIds)) {
sqlQueryBuilder()->delete('products_in_sections')
->where(Operator::equals(['id_section' => $section['id'], 'generated' => 1]))
->andWhere(Operator::inIntArray($deleteProductsIds, 'id_product'))
->execute();
}
$addProductsIds = $newProducts
->select('p.id')
->andWhere(Operator::not(Operator::inSubQuery('p.id', $existingProducts)))
->execute()->fetchFirstColumn();
if (!empty($addProductsIds)) {
$insertQuery = sqlQueryBuilder()->insert('products_in_sections');
foreach ($addProductsIds as $id) {
$insertQuery->multiDirectValues([
'id_product' => $id,
'id_section' => $section['id'],
'generated' => 1,
]);
}
$insertQuery->execute();
}
});
} catch (\Exception $e) {
getRaven()->captureException($e);
}
}
}
public function matchSectionPath($sectionPath)
{
return sqlQueryBuilder()->select('s.id, s.url')
->from('sections', 's')
->andWhere(
Translation::joinTranslatedFields(
SectionsTranslation::class,
function (QueryBuilder $qb, $columnName, $translatedField) use ($sectionPath) {
if ($columnName == 'url') {
$qb->andWhere(Operator::equals([Operator::coalesce($translatedField, 's.url') => $sectionPath]));
return true;
}
},
['url']
)
)
->execute()->fetch();
}
/**
* @return array|bool
*
* @throws PermanentRedirectException
*/
public function resolve($uri)
{
$originalUri = strtok($uri, '?');
$partToTest = trim($originalUri, '/');
$match = $this->matchSectionPath($partToTest);
if ($match) {
if (StringUtil::startsWith(ltrim($originalUri, '/'), $match['url'])) {
$parameters = str_replace($match['url'].'/', '', trim($originalUri, '/').'/');
return [
'id' => $match['id'],
'parameters' => empty($parameters) ? null : $parameters,
];
}
}
return false;
}
public function getCustomUrls(array $languages, int $sectionID): array
{
if (!findModule(\Modules::PRODUCTS_SECTIONS, 'custom_url')) {
return [];
}
$translationAlias = $this->sectionsTranslation->getTableAlias();
$urls = sqlQueryBuilder()->select('s.id, s.url')->from('sections', 's')
->where(Operator::equals(['s.id' => $sectionID]))
->andWhere(
Translation::joinTranslations(
$languages,
$this->sectionsTranslation,
function (QueryBuilder $qb, $columnName, $translatedColumn, $langID) use ($translationAlias) {
if ($columnName == 'url') {
$qb->addSelect("COALESCE({$translatedColumn}, {$translationAlias}.{$columnName}) as url_{$langID}");
}
return false;
}
)
)
->execute()->fetch();
$return = [];
foreach ($languages as $language) {
if (isset($urls['url_'.$language])) {
$return[$language] = '/'.trim($urls['url_'.$language], '/').'/';
}
}
return $return;
}
public function removeFilterFromSectionUrl(Request $request, string $url): string
{
// odetekuju lomitko na konci URL, abych zpatky vracel stejnou URL
$isSlashAtTheEnd = StringUtil::endsWith($url, '/');
if ($parameters = $request->attributes->get('parameters')) {
$url = substr(rtrim($url, '/'), 0, -strlen(rtrim($parameters, '/')));
}
$parts = explode('/f/', $url);
return rtrim(reset($parts), '/').($isSlashAtTheEnd ? '/' : '');
}
public function getDescendantCategories($id_topsections, bool $onlyVisible = true): array
{
$visibilityCondition = static function (string $alias) use ($onlyVisible) {
return $onlyVisible ? 'AND '.$alias.'.figure != "N"' : '';
};
$sections = (array) $id_topsections;
$sections = array_merge($sections, sqlFetchAll(sqlQuery('WITH RECURSIVE cte (level, id, position) AS (
SELECT 0, SR1.id_section, SR1.position
FROM sections_relation SR1
INNER JOIN sections AS s ON s.id = SR1.id_section '.$visibilityCondition('s').'
'.($id_topsections !== null ? 'WHERE SR1.id_topsection IN (:id_topsections)' : '').'
UNION ALL
SELECT cte.level + 1, SR2.id_section, SR2.position
FROM sections_relation SR2
INNER JOIN sections AS s2 ON s2.id = SR2.id_section '.$visibilityCondition('s2').'
INNER JOIN cte ON (cte.id = SR2.id_topsection)
WHERE cte.level < 50
)
SELECT id
FROM cte ORDER BY level, position', ['id_topsections' => $sections], ['id_topsections' => Connection::PARAM_INT_ARRAY]), ['id' => 'id']));
return $sections;
}
public static function recurseDeleteSections(&$sections, $sectionIds)
{
$found = false;
foreach ($sections as $id => &$section) {
$foundLocal = isset($sectionIds[$section->getId()]);
if ($section['submenu']) {
$foundLocal |= self::recurseDeleteSections($section['submenu'], $sectionIds);
}
if (!$foundLocal && !($section->getRedirectUrl() || $section->isVirtual())) {
unset($sections[$id]);
continue;
}
$found |= $foundLocal;
}
return $found;
}
public function getBadges(?string $figure = 'Y', ?string $virtual = 'N', ?string $showInSearch = 'Y'): array
{
$result = [];
if ($figure === 'N') {
$result[] = [
'translate_key' => 'figureN',
'class' => 'badge-pastel-default',
'icon' => 'eye-slash-fill',
];
} elseif ($figure === 'O') {
$result[] = [
'translate_key' => 'figureO',
'class' => 'badge-pastel-default',
'icon' => 'clipboard-x',
];
}
if ($virtual === 'Y') {
$result[] = [
'translate_key' => 'virtualY',
'class' => 'badge-pastel-default',
'icon' => 'gear-wide-connected',
];
}
if ($showInSearch === 'N') {
$result[] = [
'translate_key' => 'showInSearchN',
'class' => 'badge-pastel-default',
'icon' => 'incognito',
];
}
return $result;
}
}