first commit

This commit is contained in:
2025-08-02 16:30:27 +02:00
commit 23646bfcee
14851 changed files with 1750626 additions and 0 deletions

View File

@@ -0,0 +1,636 @@
<?php
namespace KupShop\CatalogBundle\View;
use KupShop\CatalogBundle\Search\FulltextElastic;
use KupShop\CatalogBundle\Util\Product\ProductDiscountCalculator;
use KupShop\ContentBundle\Entity\ProductUnified;
use KupShop\I18nBundle\Entity\Country;
use KupShop\I18nBundle\Translations\ParametersListTranslation;
use KupShop\KupShopBundle\Config;
use KupShop\KupShopBundle\Context\CountryContext;
use KupShop\KupShopBundle\Util\Compat\ServiceContainer;
use KupShop\KupShopBundle\Util\System\UrlFinder;
use Query\Operator;
use Query\QueryBuilder;
use Query\Translation;
use Symfony\Component\HttpFoundation\JsonResponse;
class AutocompleteView
{
public $query;
/** @var UrlFinder */
protected $urlFinder;
/** @required */
public CountryContext $countryContext;
/** @required */
public ProductDiscountCalculator $productDiscountCalculator;
public function __construct()
{
$this->query = new \Query();
}
public function handleAutocomplete()
{
$type = getVal('type');
// Remove '-' and '_', Capitalize each word
$type = strtr(ucwords(strtr($type, ['_' => ' ', '-' => ' '])), [' ' => '']);
if (empty($type)) {
return new JsonResponse();
}
$search = $this->prepareSearch(getVal('term'));
$this->query->limit = getVal('limit', 10) ?: 100;
$result = $this->handle($type, $search);
return new JsonResponse($result);
}
public function handle($type, $search)
{
// Store search term to query data
$this->query->data['search'] = $search;
$this->query->data['search_both'] = "%{$search}%";
$this->query->data['search_left'] = "{$search}%";
if (method_exists($this, 'handle'.ucfirst($type))) {
return call_user_func([$this, 'handle'.ucfirst($type)]);
}
return null;
}
public function handleProduct()
{
$this->query->fields = 'title as label';
$this->query->from = 'products p';
$this->query->where = 'p.figure="Y" AND p.title LIKE :search_both';
$this->query->order = 'p.title';
return $this->getQueryData();
}
public function handleProductCategories()
{
return $this->handleProductCategoriesFulltext();
}
public function handleProductCategoriesFulltext()
{
// Tady si schválně říkám o FulltextElastic a ne Interface, protože nechci aby mi přišel LuigisBox. Ten nepoužívá naše autocomplete
$fulltext = ServiceContainer::getService(FulltextElastic::class);
$fulltext->setCurlTimeout(10);
$term = $this->query->data['search'];
$enabledTopics = $fulltext->getIndexTypes();
if ($disabled_topics = Config::get()['Modules'][\Modules::SEARCH]['disabled_topics'] ?? null) {
$enabledTopics = array_values(array_diff($enabledTopics, $disabled_topics));
}
$multiSearchResult = $fulltext->search(
$term,
$this->getFulltextSearchConfig($enabledTopics),
$enabledTopics
);
return $this->processFulltextResults($multiSearchResult, $fulltext->multiSearchResultTotals);
}
public function handleDeliveryInPersonSellers(): array
{
if (!findModule(\Modules::SELLERS)) {
return [];
}
$this->query->fields = 'id, psc as value, title as name, CONCAT_WS(", ", psc, city, street, number) as address';
$this->query->from = 'sellers';
$search = get_search_query($this->query->data['search'], [
['field' => 'title', 'match' => 'both'],
['field' => 'psc', 'match' => 'both'],
['field' => 'city', 'match' => 'both'],
['field' => 'street', 'match' => 'both'],
]);
$this->query->data = $search['data'];
$this->query->where = $search['where'];
return $this->getQueryData();
}
public function handleDeliveryBalikDoRuky()
{
$this->query->fields = 'psc as value, name, region, time_zone, address';
$this->query->order = 'psc';
$this->query->from = '`kupshop_shared`.`delivery_balik_na_postu`';
$this->query->where = 'name LIKE :search_left OR psc LIKE :search_left';
return $this->getQueryData();
}
public function handleDeliveryBalikovna()
{
$this->query->fields = 'zip as value, name, city, city_part, address';
$this->query->order = 'zip';
$this->query->from = '`kupshop_shared`.`delivery_balikovna`';
$this->query->where = 'name LIKE :search_left OR city LIKE :search_left OR city_part LIKE :search_left OR address LIKE :search_both OR zip LIKE :search_left';
return $this->getQueryData();
}
public function handleDeliveryZasilkovna()
{
$this->query->fields = 'zip, name, hours, labelRouting, id as value';
$this->query->order = 'zip';
$this->query->from = '`kupshop_shared`.`delivery_zasilkovna`';
$this->query->where = '(name LIKE :search_both OR zip LIKE :search_both)';
$onlyVisible = getVal('onlyVisible', null, 'Y');
if ($onlyVisible == 'Y') {
$this->query->where .= " AND visible = 'Y' ";
}
$except = getVal('except');
if (!empty($except)) {
$except = array_filter(
explode(',', $except),
function ($id) {
return (is_numeric($id)) ? true : false;
}
);
$this->query->where .= ' AND id NOT IN ('.implode(',', $except).')';
}
if ($currency = getVal('currency')) {
$currencyCode = $currency;
} else {
$currencyContext = ServiceContainer::getService(\KupShop\KupShopBundle\Context\CurrencyContext::class);
$currencyCode = $currencyContext->getActiveId();
}
if (!empty($currencyCode)) {
switch ($currencyCode) {
case 'CZK': $this->query->where .= ' AND country=\'cz\'';
break;
case 'EUR': $this->query->where .= ' AND country=\'sk\'';
break;
case 'HUF': $this->query->where .= ' AND country=\'hu\'';
break;
case 'RON': $this->query->where .= ' AND country=\'ro\'';
break;
case 'PLN': $this->query->where .= ' AND country=\'pl\'';
break;
}
}
return $this->getQueryData();
}
public function handleDeliveryPplparcelshop()
{
$this->query->fields = "zip, city, street, type, id as value, CONCAT(city, ', ', street, ', ', name) AS name";
$this->query->order = 'zip';
$this->query->from = '`kupshop_shared`.`delivery_pplparcelshop`';
$this->query->where = '(zip LIKE :search_both OR city LIKE :search_both OR street LIKE :search_both OR name LIKE :search_both)';
$countries = "'".implode("','", array_map(function (Country $c) {return $c->getId(); }, $this->countryContext->getSupported()))."'";
if ($country = getVal('country')) {
$countries = "'{$country}'";
}
if (!empty($countries)) {
$this->query->where .= ' AND country IN ('.$countries.')';
}
$except = getVal('except');
if (!empty($except)) {
$except = array_filter(
explode(',', $except),
function ($id) {
return (is_numeric($id)) ? true : false;
}
);
$this->query->where .= ' AND id NOT IN ('.implode(',', $except).')';
}
return $this->getQueryData();
}
public function handleDeliveryUlozenka()
{
$this->query->fields = "zip, city, street, type, id as value, CONCAT(city, ', ', street, ', ', name) AS name";
$this->query->order = 'zip';
$this->query->from = '`kupshop_shared`.`delivery_ulozenka`';
$this->query->where = '(zip LIKE :search_both OR city LIKE :search_both OR street LIKE :search_both OR name LIKE :search_both)';
$except = getVal('except');
if (!empty($except)) {
$except = array_filter(
explode(',', $except),
function ($id) {
return (is_numeric($id)) ? true : false;
}
);
$this->query->where .= ' AND id NOT IN ('.implode(',', $except).')';
}
return $this->getQueryData();
}
public function handleDeliveryInTime()
{
$this->query->fields = "zip, city, street, type, id as value, CONCAT(city, ', ', street, ', ', name) AS name";
$this->query->order = 'zip';
$this->query->from = '`kupshop_shared`.`delivery_intime`';
$this->query->where = '(zip LIKE :search_both OR city LIKE :search_both OR street LIKE :search_both OR name LIKE :search_both)';
$except = getVal('except');
if (!empty($except)) {
$except = array_filter(
explode(',', $except),
function ($id) {
return (is_numeric($id)) ? true : false;
}
);
$this->query->where .= ' AND id NOT IN ('.implode(',', $except).')';
}
return $this->getQueryData();
}
public function handleDeliveryDpdpickup()
{
$this->query->fields = 'place, zip, city, street, id as value';
$this->query->order = 'zip';
$this->query->from = '`kupshop_shared`.`delivery_dpdpickup`';
$this->query->where = '(LPAD(zip, 5, 0) LIKE :search_both OR city LIKE :search_both OR street LIKE :search_both OR place LIKE :search_both)';
$except = getVal('except');
if (!empty($except)) {
$except = array_filter(
explode(',', $except),
function ($id) {
return (is_numeric($id)) ? true : false;
}
);
$this->query->where .= ' AND id NOT IN ('.implode(',', $except).')';
}
return $this->getQueryData();
}
public function handleDeliverySpbalikobox()
{
$this->query->fields = 'zip, city, street, type, id as value, name';
$this->query->order = 'zip';
$this->query->from = '`kupshop_shared`.`delivery_sp_balikobox`';
$this->query->where = '(zip LIKE :search_both OR city LIKE :search_both OR street LIKE :search_both OR name LIKE :search_both)';
$except = getVal('except');
if (!empty($except)) {
$except = array_filter(
explode(',', $except),
function ($id) {
return (is_numeric($id)) ? true : false;
}
);
$this->query->where .= ' AND id NOT IN ('.implode(',', $except).')';
}
return $this->getQueryData();
}
public function handleDeliveryGlsparcelshop()
{
$this->query->fields = "zip, city, street, type, id as value, CONCAT(city, ', ', street, ', ', name) AS name";
$this->query->order = 'zip';
$this->query->from = '`kupshop_shared`.`delivery_glsparcelshop`';
$this->query->where = '(zip LIKE :search_both OR city LIKE :search_both OR street LIKE :search_both OR name LIKE :search_both)';
$except = getVal('except');
if (!empty($except)) {
$except = array_filter(
explode(',', $except),
function ($id) {
return (is_numeric($id)) ? true : false;
}
);
$this->query->where .= ' AND id NOT IN ('.implode(',', $except).')';
}
return $this->getQueryData();
}
public function handleDeliveryGeispoint()
{
$this->query->fields = "zip, city, street, type, id as value, CONCAT(city, ', ', street, ', ', name) AS name";
$this->query->order = 'zip';
$this->query->from = '`kupshop_shared`.`delivery_geispoint`';
$this->query->where = '(zip LIKE :search_both OR city LIKE :search_both OR street LIKE :search_both OR name LIKE :search_both)';
$except = getVal('except');
if (!empty($except)) {
$except = array_filter(
explode(',', $except),
function ($id) {
return (is_numeric($id)) ? true : false;
}
);
$this->query->where .= ' AND id NOT IN ('.implode(',', $except).')';
}
return $this->getQueryData();
}
public function handleDeliveryPaczkomaty()
{
$this->query->fields = "zip, city, street, type, id as value, CONCAT(city, ', ', street, ', ', name) AS name";
$this->query->order = 'zip';
$this->query->from = '`kupshop_shared`.`delivery_paczkomaty`';
$this->query->where = '(zip LIKE :search_both OR city LIKE :search_both OR street LIKE :search_both OR name LIKE :search_both)';
$except = getVal('except');
if (!empty($except)) {
$except = array_filter(
explode(',', $except),
function ($id) {
return (is_numeric($id)) ? true : false;
}
);
$this->query->where .= ' AND id NOT IN ('.implode(',', $except).')';
}
return $this->getQueryData();
}
public function handleParameterValues(): array
{
$term = $this->query->data['search'] ?? '';
$search = [$term];
if (getVal('fulltext')) {
$search = array_filter(explode(' ', $term));
}
$qb = sqlQueryBuilder()
->select('pl.id as value, pl.position')
->from('parameters_list', 'pl')
->andWhere(Operator::equals(['pl.id_parameter' => getVal('parameterId')]))
->andWhere(
Translation::joinTranslatedFields(
ParametersListTranslation::class,
function (QueryBuilder $qb, $columnName, $translatedField) use ($search) {
$searchField = Operator::coalesce($translatedField, "pl.{$columnName}");
$andX = [];
foreach ($search as $value) {
$andX[] = Operator::like([$searchField => "%{$value}%"]);
}
$qb->andWhere(Operator::andX($andX));
}, ['value' => 'name']
)
)
->orderBy('position');
if ($limit = getVal('limit')) {
$qb->setMaxResults((int) $limit);
}
return $qb->execute()->fetchAllAssociative();
}
public function getQueryData()
{
return sqlFetchAll($this->query->execute());
}
public function prepareSearch($search)
{
$search = trim(urldecode($search));
return $search;
}
protected function processProducts(array $sphinxResult)
{
$productList = $this->getProductList();
$sphinxIds = array_keys($sphinxResult);
$productList->andSpec(function (\Query\QueryBuilder $qb) use ($sphinxIds) {
$qb->setParameter('productIds', $sphinxIds, \Doctrine\DBAL\Connection::PARAM_INT_ARRAY);
return $qb->expr()->in('p.id', ':productIds');
});
$productList->orderBy('FIELD(p.id, :productIds)');
$productList->fetchProducers();
$productList->fetchImages(2);
$productList->fetchSets();
$productList->fetchStoresInStore();
return array_map(function (\Product $product) {
return $this->getProductResult($product);
}, $productList->getProducts()->getValues());
}
protected function processSections(array $sphinxResult)
{
foreach ($sphinxResult as &$section) {
$section['label'] = $section['path'] ?? '';
}
return array_values($sphinxResult);
}
protected function processProducers(array $sphinxResult)
{
foreach ($sphinxResult as &$producer) {
$producer['label'] = $producer['name'] ?? '';
}
return array_values($sphinxResult);
}
protected function processArticles(array $result)
{
foreach ($result as &$article) {
$article['label'] = $article['title'];
}
return array_values($result);
}
protected function processPages(array $result): array
{
foreach ($result as &$page) {
$page['label'] = $page['name'];
}
return array_values($result);
}
protected function getProductList(): \ProductList
{
$productList = ServiceContainer::getService(\KupShop\CatalogBundle\ProductList\ProductList::class);
$productList->applyDefaultFilterParams();
return $productList;
}
protected function getProductResult(\Product $product): array
{
$productPrice = $product['productPrice'];
$price = $productPrice['price_with_vat'];
$priceWithoutVat = $productPrice['price_without_vat'];
$productDiscountResult = $this->productDiscountCalculator->calculate(
new ProductUnified($product)
);
$priceOriginal = $productPrice->getOriginalPrice()->getPriceWithoutDiscount();
return [
'id' => $product->id,
'image' => ($product->image ? $this->urlFinder->staticUrl($product->image['src']) : ''),
'label' => $product->title,
'discount' => $product->discount->asFloat(),
'price' => printPrice($price),
'price_without_vat' => printPrice($priceWithoutVat),
'price_array' => $product->price_array,
'priceOriginal' => printPrice($priceOriginal),
'annotation' => $product->descr,
'producer' => $product->producer ?? [],
'inStore' => $product->inStore,
'deliveryTime' => $product->deliveryTime,
'deliveryTimeText' => $product->deliveryTimeText,
'deliveryTimeRaw' => $product->deliveryTimeRaw,
'storesInStore' => $product->storesInStore ?? [],
'productDiscount' => [
'discount' => $productDiscountResult->discount->asFloat(),
'priceForDiscount' => $productDiscountResult->priceForDiscount ? printPrice($productDiscountResult->priceForDiscount) : null,
'priceOriginal' => $productDiscountResult->priceOriginal ? printPrice($productDiscountResult->priceOriginal) : null,
'priceCommon' => $productDiscountResult->priceCommon ? printPrice($productDiscountResult->priceCommon) : null,
],
'campaign_codes' => $product->campaign_codes ?? [],
];
}
/**
* @required
*/
public function setUrlFinder(UrlFinder $urlFinder): void
{
$this->urlFinder = $urlFinder;
}
public function getFulltextSearchConfig(array $enabledTopics): array
{
$maxResults = 6;
$config = [];
foreach ($enabledTopics as $type) {
$config[$type] = [
'count' => $maxResults,
'offset' => 0,
];
}
$config[FulltextElastic::INDEX_PRODUCTS]['order'] = '-weight';
return $config;
}
public function processFulltextResults(array $multiSearchResult, array $totals): array
{
$result = [];
foreach ($multiSearchResult as $type => $results) {
if (!$results) {
continue;
}
switch ($type) {
case FulltextElastic::INDEX_PRODUCTS:
$result['Produkty'] = [
'label' => 'Produkty',
'rows' => $totals[$type] ?? 0,
'items' => $this->processProducts($results),
];
break;
case FulltextElastic::INDEX_SECTIONS:
$result['Kategorie'] = [
'label' => 'Kategorie',
// min(total, 20), protoze SearchView zobrazuje max 20 vysledku
'rows' => min($totals[$type] ?? 0, 20),
'items' => $this->processSections($results),
];
break;
case FulltextElastic::INDEX_PRODUCERS:
$result['Vyrobci'] = [
'label' => 'Výrobci',
// min(total, 20), protoze SearchView zobrazuje max 20 vysledku
'rows' => min($totals[$type] ?? 0, 20),
'items' => $this->processProducers($results),
];
break;
case FulltextElastic::INDEX_ARTICLES:
$result['Clanky'] = [
'label' => 'Články',
// min(total, 20), protoze SearchView zobrazuje max 20 vysledku
'rows' => min($totals[$type] ?? 0, 20),
'items' => $this->processArticles($results),
];
break;
case FulltextElastic::INDEX_PAGES:
$result['Stranky'] = [
'label' => 'Stránky',
// min(total, 20), protoze SearchView zobrazuje max 20 vysledku
'rows' => min($totals[$type] ?? 0, 20),
'items' => $this->processPages($results),
];
break;
default:
$result[$type] = [
'label' => '$type',
// min(total, 20), protoze SearchView zobrazuje max 20 vysledku
'rows' => min($totals[$type] ?? 0, 20),
'items' => $results,
];
}
}
return $result;
}
}