<?php
namespace App\Application\Controller;
use App\Admin\Document\AbstractSEODocument;
use App\Admin\Document\Article;
use App\Admin\Document\Category;
use App\Admin\Document\Color;
use App\Admin\Document\Page;
use App\Admin\Document\Product;
use App\Admin\Document\Property;
use App\Admin\Document\PropertyValue;
use App\Admin\Document\Tag;
use Symfony\Component\HttpFoundation\Cookie;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use \Symfony\Component\Routing\Annotation\Route;
class CatalogController extends AbstractController
{
const CATALOG_URL = 'catalog';
const RECENTLY_WATCHED_PRODUCTS_COOKIE_NAME = 'RecentlyWatchedProducts';
const RECENTLY_WATCHED_PRODUCTS_LIMIT = 8;
protected function getDefaultItemCountPerPage(){
return 9;
}
protected function setCategorySeo(Category $document, $locale, $page = null) {
$document = clone $document;
if (!$document->getSeoTitle()) {
$document->setSeoTitle('Купить ' . $document->getName($locale)
. ' от производителя, производство СПб');
}
if (!$document->getSeoDescription()) {
$document->setSeoDescription($document->getName($locale)
. ' от производителя Cosca. Производство в Санкт-Петербурге. Оптовые и розничные продажи. Доставка транспортной компанией по всей России и странам СНГ');
}
$this->setSeo(
$document,
$locale,
$page
);
}
protected function setProductSeo(Product $document, $locale, $page = null) {
$document = clone $document;
if (!$document->getSeoTitle()) {
$document->setSeoTitle($document->getName($locale)
. ', купить в интернет-магазине Decostore'
. ', цена ' . $document->getPrice());
}
if (!$document->getSeoDescription()) {
$document->setSeoDescription($document->getName($locale)
. ' — предлагаем к покупке в интернет-магазине Decostore оптом и в розницу по доступной цене. Доставка по всей России и странам СНГ. Собственное производство, широкий ассортимент, всегда в наличии.');
}
$this->setSeo(
$document,
$locale,
$page
);
}
/**
* @Route("/catalog", name="catalog")
* @param Request $request
* @return \Symfony\Component\HttpFoundation\Response
*/
public function index(Request $request)
{
$page = $this->getCatalogPage();
$this->setSeo($page, $request->getLocale());
return $this->render('application/catalog/index.html.twig', [
'page' => $page,
]);
}
/**
* @Route("/search", name="search")
* @param Request $request
* @return \Symfony\Component\HttpFoundation\Response
*/
public function search(Request $request)
{
return $this->getSearchViewModel($request);
}
/**
* @Route("/catalog/{url}", name="catalog2")
* @param Request $request
* @return \Symfony\Component\HttpFoundation\Response
*/
public function level2(Request $request, $url)
{
/**
* @var Category $category
*/
$category = $this->getDocumentRepository(Category::class)->findOneBy(['url' => $url, 'active' => true]);
if (!$category) {
throw $this->createNotFoundException("Category with url '{$url}' not active");
}
return $this->getCatalogViewModel($request, $category);
}
/**
* @Route("/catalog/{parentUrl}/{url}", name="catalog3")
* @param Request $request
* @return \Symfony\Component\HttpFoundation\Response
*/
public function level3(Request $request, $parentUrl, $url)
{
/**
* @var Category $parent
*/
$parent = $this->getDocumentRepository(Category::class)->findOneBy(['url' => $parentUrl, 'active' => true]);
if (!$parent) {
throw $this->createNotFoundException("Category with url '{$parentUrl}' does not exist or not active");
}
/**
* @var Category $category
*/
$category = $this->getDocumentRepository(Category::class)->findOneBy(['url' => $url, 'active' => true, 'parent' => $parent->getId()]);
if (!$category) {
throw $this->createNotFoundException("Category with url '{$url}' does not have parent {$parentUrl} or not active");
}
return $this->getCatalogViewModel($request, $category);
}
private function getCatalogPage() {
$page = $this->getDocumentRepository(Page::class)->findOneBy(['url' => self::CATALOG_URL, 'active' => true]);
if (!$page) {
throw $this->createNotFoundException("Page with url " . self::CATALOG_URL . " not found or not active");
}
return $page;
}
/**
* @param Request $request
* @param Category|null $category
* @param string $searchText
* @return array
* @throws \Doctrine\ODM\MongoDB\MongoDBException
*/
private function getItemsParams(Request $request, $category, $searchText = '') {
$filtered = false;
$priceFrom = str_replace(' ', '', $request->get('priceFrom'));
$priceTo = str_replace(' ', '', $request->get('priceTo'));
$color = $request->get('color');
$properties = $request->get('property', []);
$sort = $request->get('sort');
// Находим популярные фильтры, если есть категория
$effectivePopularFilters = [];
if ($category) {
$effectivePopularFilters = $category->getEffectivePopularFilters();
}
$qb = $this->getQueryBuilder(Product::class);
$qb->field('active')->equals(true);
if ($category) {
if ($category->getSale()) {
$qb->field('sale')->equals(true);
} else {
$qb->field('categories')->equals($category->getId());
$qb->field('sale')->notEqual(true);
}
} elseif ($searchText) {
$expr = $qb->expr();
$regex = new \MongoDB\BSON\Regex('.*' . preg_quote(trim($searchText)) . '.*', 'i');
$propertyValueQb = $this->getQueryBuilder(PropertyValue::class);
$propertyValueQb->field('name')->equals($regex);
$pipeline = [
['$match' => $propertyValueQb->getQueryArray()],
['$group' => ['_id' => '$_id']],
];
$collection = $this->getDocumentManager()->getDocumentCollection(PropertyValue::class);
$docs = $collection->aggregate($pipeline, $this->options);
$propertyValueIds = array_column($docs->toArray(), '_id');
$expr->addOr($qb->expr()->field('properties.propertyValue')->in($propertyValueIds));
$expr->addOr($qb->expr()->field('name')->equals($regex));
$expr->addOr($qb->expr()->field('text')->equals($regex));
$expr->addOr($qb->expr()->field('shortText')->equals($regex));
$expr->addOr($qb->expr()->field('sku')->equals($regex));
$qb->addAnd($expr);
} else {
$qb->field('name')->equals([]); // no results
}
$query = $qb->getQueryArray();
if ($priceFrom) {
$qb->field('price')->gte((float)$priceFrom);
$filtered = true;
}
if ($priceTo) {
$qb->field('price')->lte((float)$priceTo);
$filtered = true;
}
if ($color) {
if (is_array($color)) {
if (count($color) > 0) {
$expr = $qb->expr();
foreach ($color as $colorId) {
if ($colorId) {
$expr->addOr($qb->expr()->field('color')->equals($colorId));
$filtered = true;
}
}
$qb->addAnd($expr);
}
} else {
$qb->field('color')->equals($color);
$filtered = true;
}
}
if (!empty($properties)) {
foreach ($properties as $propertyId => $propertyValueId) {
if ($propertyValueId) {
if (is_array($propertyValueId)) {
$expr = $qb->expr();
// Проверяем, является ли массив ассоциативным (чекбоксы) или нумерованным (select multiple)
$isAssoc = false;
foreach (array_keys($propertyValueId) as $key) {
if (!is_numeric($key)) {
$isAssoc = true;
break;
}
}
if ($isAssoc) {
// Для чекбоксов (ассоциативный массив)
foreach ($propertyValueId as $id => $v) {
if ($v) {
$expr->addOr($qb->expr()->field('propertyMap1C.' . $propertyId)->equals($id));
$filtered = true;
}
}
} else {
// Для select multiple (нумерованный массив)
foreach ($propertyValueId as $value) {
if ($value) {
$expr->addOr($qb->expr()->field('propertyMap1C.' . $propertyId)->equals($value));
$filtered = true;
}
}
}
$qb->addAnd($expr);
} else {
$qb->field('propertyMap1C.' . $propertyId)->equals($propertyValueId);
$filtered = true;
}
}
}
}
$qb->sort('inStock', 'desc');
switch ($sort) {
case 'name':
$qb->sort('name');
$qb->sort('price');
$qb->sort('popularity', 'desc');
break;
case 'priceDesc':
$qb->sort('price', 'desc');
$qb->sort('name', 'desc');
$qb->sort('popularity', 'desc');
break;
case 'priceAsc':
$qb->sort('price');
$qb->sort('name');
$qb->sort('popularity', 'desc');
break;
default:
$qb->sort('popularity', 'desc');
$qb->sort('price');
$qb->sort('name');
}
$qb->sort('_id');
if ($request->get('debug')) {
var_dump($qb->getQuery()->getQuery());
exit;
}
$pagination = $this->paginate($qb);
$products = $pagination->getItems();
$pipeline = [
['$match' => $query],
['$group' => [
'_id' => '$color',
]],
];
$collection = $this->getDocumentManager()->getDocumentCollection(Product::class);
$docs = $collection->aggregate($pipeline, $this->options);
$colorIds = [];
foreach ($docs as $doc) {
$colorIds[] = $doc['_id'];
}
$qb = $this->getQueryBuilder(Color::class);
$qb->field('_id')->in($colorIds);
$qb->sort('name', 'ASC');
$colors = [];
foreach ($qb->getQuery()->execute() as $colorObj) {
$colors[$colorObj->getId()] = $colorObj;
}
$filters = [];
$popularFilters = []; // Массив для популярных фильтров
/**
* @var Property $property
*/
foreach ($this->getDocumentRepository(Property::class)->findBy(['active' => true, 'id1C' => ['$exists' => true]], ['name' =>'asc']) as $property) {
if (!$property->getId1C()) {
continue;
}
$pipeline = [
['$match' => array_merge($query, ['propertyMap1C.' . $property->getId1C() => ['$exists' => true]])],
['$group' => [
'_id' => '$propertyMap1C.' . $property->getId1C(),
]],
];
$docs = $collection->aggregate($pipeline, $this->options);
$valueIds = [];
foreach ($docs as $doc) {
$valueIds[] = $doc['_id'];
}
if (count($valueIds)) {
$qb = $this->getQueryBuilder(PropertyValue::class);
$qb->field('id1C')->in($valueIds);
$qb->sort('name', 'ASC');
$values = [];
/**
* @var PropertyValue $value
*/
foreach ($qb->getQuery()->execute() as $value) {
$values[$value->getId1C()] = $value;
}
foreach ($valueIds as $id) {
if (!isset($values[$id])) {
$pv = new PropertyValue();
$pv->setName($id);
$pv->setId1C($id);
$values[$id] = $pv;
}
}
uasort($values,
/**
* @param PropertyValue $a
* @param PropertyValue $b
*/
function ($a, $b) {
return $a->compare($b);
}
);
$properties = $request->get('property', []);
if ($property->isCheckboxFilter()) {
$selected = [];
if (isset($properties[$property->getId1C()])) {
foreach ($properties[$property->getId1C()] as $id => $v) {
if ($v) {
$selected[] = $id;
}
}
}
} else {
$selected = [];
if (isset($properties[$property->getId1C()])) {
$propValues = $properties[$property->getId1C()];
if (is_array($propValues)) {
foreach ($propValues as $value) {
if ($value) {
$selected[] = $value;
}
}
} elseif ($propValues) {
$selected[] = $propValues;
}
}
}
$filters[] = [
'property' => $property,
'values' => $values,
'selected' => $selected
];
// Проверяем, является ли фильтр популярным
if ($category && !empty($effectivePopularFilters) && in_array($property->getId1C(), $effectivePopularFilters)) {
$popularFilters[] = [
'property' => $property,
'values' => $values,
'selected' => $selected
];
}
}
}
// Находим популярные фильтры, если есть категория
$popularFilters = [];
if ($category) {
$effectivePopularFilters = $category->getEffectivePopularFilters();
if (!empty($effectivePopularFilters)) {
$propertyFilters = [];
$specialFilters = [];
// Разделяем фильтры на свойства и специальные
foreach ($effectivePopularFilters as $filterId) {
if ($filterId === \App\Admin\Document\Category::SPECIAL_FILTER_PRICE) {
$specialFilters[] = $filterId;
} else if ($filterId === \App\Admin\Document\Category::SPECIAL_FILTER_COLOR) {
$specialFilters[] = $filterId;
} else {
$propertyFilters[] = $filterId;
}
}
// Получаем диапазон цен для фильтра цены
$priceRange = [0, 0];
if (in_array(\App\Admin\Document\Category::SPECIAL_FILTER_PRICE, $specialFilters)) {
$priceRange = $this->getMinMaxPrices(['query' => $qb->getQueryArray()]);
}
$minFilterPrice = $priceRange[0];
$maxFilterPrice = $priceRange[1];
// Получаем обычные свойства
if (!empty($propertyFilters)) {
$popularProperties = $this->getDocumentRepository(\App\Admin\Document\Property::class)
->findBy(['id1C' => ['$in' => $propertyFilters]]);
// Преобразуем в массив для популярных фильтров
foreach ($popularProperties as $property) {
if ($property->isCheckboxFilter()) {
continue; // Пропускаем чекбоксы, так как они не подходят для быстрых фильтров
}
// Находим соответствующий фильтр в общем списке
foreach ($filters as $filter) {
if ($filter['property']->getId1C() === $property->getId1C()) {
$popularFilters[] = [
'property' => $property,
'values' => $filter['values'],
'selected' => $filter['selected'],
'type' => 'property'
];
break;
}
}
}
}
// Добавляем специальные фильтры
foreach ($specialFilters as $specialFilter) {
if ($specialFilter === \App\Admin\Document\Category::SPECIAL_FILTER_PRICE) {
$popularFilters[] = [
'property' => (object)['id1C' => 'price', 'name' => 'Цена', 'checkboxFilter' => false],
'values' => [], // Для цены не используются значения
'selected' => [$priceFrom, $priceTo],
'type' => 'price',
'min' => $minFilterPrice,
'max' => $maxFilterPrice
];
} else if ($specialFilter === \App\Admin\Document\Category::SPECIAL_FILTER_COLOR && !empty($colors)) {
$popularFilters[] = [
'property' => (object)['id1C' => 'color', 'name' => 'Цвет', 'checkboxFilter' => false],
'values' => $colors,
'selected' => $color,
'type' => 'color'
];
}
}
}
}
return [
'category' => $category,
'products' => $products,
'pagination' => $pagination->getPaginationData(),
'page' => $pagination->getCurrentPageNumber(),
'priceFrom' => (int)$priceFrom,
'priceTo' => (int)$priceTo,
'color' => $color,
'filtered' => $filtered,
'sort' => $sort,
'query' => $query,
'view' => $request->get('view'),
'colors' => $colors,
'filters' => $filters,
'popularFilters' => $popularFilters,
];
}
private $options = ['cursor' => true, 'allowDiskUse' => true];
private function getCatalogViewModel(Request $request, Category $category) {
$catalogPage = $this->getCatalogPage();
$categoryList = $this->getDocumentRepository(Category::class)->findBy(['active' => true, 'parent' => null],
['orderNumber' => 'asc', 'name' => 'asc']);
$this->setCategorySeo($category, $request->getLocale());
$itemsParams = $this->getItemsParams($request, $category);
$prices = $this->getMinMaxPrices($itemsParams);
$minPrice = $prices[0];
$maxPrice = $prices[1];
unset($itemsParams['query']);
return $this->render('application/catalog/filteredResults.html.twig', array_merge($itemsParams, [
'searchText' => null,
'searchResultsQuantity' => null,
'catalogPage' => $catalogPage,
'category' => $category,
'categoryList' => $categoryList,
'minPrice' => (int)$minPrice,
'maxPrice' => (int)$maxPrice,
'pageNumber' => $request->get('page'),
]));
}
private function getSearchViewModel(Request $request) {
$categoryList = $this->getDocumentRepository(Category::class)->findBy(['active' => true, 'parent' => null],
['orderNumber' => 'asc', 'name' => 'asc']);
$this->setSeoByUrl('search', $request->getLocale());
$searchText = $request->get('searchText');
$itemsParams = $this->getItemsParams($request, null, $searchText);
$prices = $this->getMinMaxPrices($itemsParams);
$minPrice = $prices[0];
$maxPrice = $prices[1];
unset($itemsParams['query']);
return $this->render('application/catalog/filteredResults.html.twig', array_merge($itemsParams, [
'searchText' => $searchText,
'searchResultsQuantity' => isset($itemsParams['pagination']['totalCount']) ? $itemsParams['pagination']['totalCount'] : null,
'catalogPage' => null,
'category' => null,
'categoryList' => $categoryList,
'minPrice' => (int)$minPrice,
'maxPrice' => (int)$maxPrice,
'pageNumber' => $request->get('page'),
]));
}
private function getMinMaxPrices($itemsParams) {
$collection = $this->getDocumentManager()->getDocumentCollection(Product::class);
$pipeline = [
['$match' => $itemsParams['query']],
['$group' => [
'_id' => null,
'minPrice' => ['$min' => '$price'],
'maxPrice' => ['$max' => '$price'],
]],
];
$docs = $collection->aggregate($pipeline, $this->options);
$minPrice = 0;
$maxPrice = 0;
foreach ($docs as $doc) {
$minPrice = floor($doc['minPrice']);
$maxPrice = ceil($doc['maxPrice']);
break;
}
return [$minPrice, $maxPrice];
}
/**
* @Route("/catalog-filter-ajax", name="catalog-filter-ajax")
* @param Request $request
* @return \Symfony\Component\HttpFoundation\Response
*/
public function filterAjax(Request $request) {
$categoryId = $request->get('categoryId');
$searchText = $request->get('searchText');
/**
* @var Category $category
*/
$category = $this->getDocumentRepository(Category::class)->find($categoryId);
$params = $this->getItemsParams($request, $category, $searchText);
return new JsonResponse([
'html' => $this->renderView('application/catalog/products.html.twig', $params),
'paginationHtml' => $this->renderView('application/catalog/pagination.html.twig', $params),
'tags' => $this->renderView('application/catalog/tags.html.twig', $params),
'filtered' => $params['filtered'],
'page' => $params['page'],
]);
}
/**
* @Route("/product/{url}", name="catalog/item")
* @param Request $request
* @return \Symfony\Component\HttpFoundation\Response
*/
public function item(Request $request, $url) {
/**
* @var Product $product
*/
$product = $this->getDocumentRepository(Product::class)->findOneBy(['_id' => $url, 'active' => true]);
if (!$product) {
$product = $this->getDocumentRepository(Product::class)->findOneBy(['url' => $url, 'active' => true]);
}
if (!$product) {
throw $this->createNotFoundException("Product with url '{$url}' does not exist or not active");
}
$this->setProductSeo($product, $request->getLocale());
$category = null;
if ($product->getSale()) {
$category = $this->getDocumentRepository(Category::class)->findOneBy(['sale' => true]);
} else {
foreach ($product->getCategories() as $category) {
if ($category->getActive()) {
break;
}
}
}
// Получаем форматированную цену с учетом настроек категории
$formattedPrice = null;
if ($category) {
$formattedPrice = $product->getFormattedPriceByCategory($category, $this->getDocumentManager());
}
return $this->addWatched($product, $request, $this->render('application/catalog/product.html.twig', [
'product' => $product,
'category' => $category,
'catalogPage' => $this->getCatalogPage(),
'videos' => $product->getVideos(),
'formattedPrice' => $formattedPrice
]));
}
private function addWatched(Product $product, Request $request, Response $response)
{
$ids = explode(';', $request->cookies->get(static::RECENTLY_WATCHED_PRODUCTS_COOKIE_NAME, ''));
$key = array_search((string)$product->getId(), $ids);
if ($key !== false) {
unset($ids[$key]);
}
array_unshift($ids, $product->getId());
if (count($ids) > self::RECENTLY_WATCHED_PRODUCTS_LIMIT) {
$ids = array_slice($ids, 0, self::RECENTLY_WATCHED_PRODUCTS_LIMIT);
}
$response->headers->setCookie(Cookie::create(static::RECENTLY_WATCHED_PRODUCTS_COOKIE_NAME, implode(';', $ids)));
return $response;
}
/**
* @Route("/catalog-show-more", name="catalog-show-more")
* @param Request $request
* @return Response
*/
public function showMore(Request $request)
{
$categoryId = $request->get('categoryId');
$searchText = $request->get('searchText');
/**
* @var Category $category
*/
$category = $this->getDocumentRepository(Category::class)->find($categoryId);
$params = $this->getItemsParams($request, $category, $searchText);
return new JsonResponse([
'html' => $this->renderView('application/catalog/products.html.twig', $params),
'paginationHtml' => $this->renderView('application/catalog/pagination.html.twig', $params),
'page' => $params['page']
]);
}
}