254 lines
10 KiB
PHP
254 lines
10 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Controllers;
|
|
|
|
use App\Repositories\CategoryRepository;
|
|
use App\Repositories\ProductRepository;
|
|
|
|
final class CatalogController
|
|
{
|
|
private ?string $searchBasePath = null;
|
|
|
|
public function index(): void
|
|
{
|
|
$categoryRepository = new CategoryRepository();
|
|
$productRepository = new ProductRepository();
|
|
$pagination = $this->paginationInput();
|
|
$searchQuery = trim((string) ($_GET['q'] ?? $this->queryParameter('q')));
|
|
$basePath = '/catalog';
|
|
|
|
if ($searchQuery !== '') {
|
|
$searchResult = $this->searchProducts($productRepository, $searchQuery, $pagination['limit'], $pagination['offset']);
|
|
$totalProducts = $searchResult['total'];
|
|
$products = $searchResult['products'];
|
|
$basePath = $this->searchBasePath ?? ($basePath . '?q=' . rawurlencode($searchQuery));
|
|
} else {
|
|
$totalProducts = $productRepository->catalogTotal(true);
|
|
$products = $productRepository->catalogPreview($pagination['limit'], $pagination['offset'], true);
|
|
}
|
|
|
|
view('catalog/index', [
|
|
'title' => 'Каталог Рыбсток - рыба, морепродукты, мясо, сыры и продукты',
|
|
'metaDescription' => 'Полный каталог Рыбсток: рыба, морепродукты, икра, мясо, сыры, полуфабрикаты, овощи, ягоды и бакалея по стоковым ценам с доставкой по Москве и Московской области.',
|
|
'metaKeywords' => 'каталог Рыбсток, купить рыбу Москва, морепродукты Москва, мясо доставка Москва, сыры доставка, икра, полуфабрикаты, овощи ягоды заморозка, бакалея, продукты по стоковым ценам',
|
|
'breadcrumbs' => [
|
|
['title' => 'Главная', 'url' => '/'],
|
|
['title' => 'Каталог'],
|
|
],
|
|
'categories' => $categoryRepository->tree(),
|
|
'currentCategory' => null,
|
|
'products' => $products,
|
|
'searchQuery' => $searchQuery,
|
|
'pagination' => $this->paginationData($basePath, $totalProducts, count($products), $pagination),
|
|
]);
|
|
}
|
|
|
|
public function search(string $query): void
|
|
{
|
|
$_GET['q'] = trim($query);
|
|
$this->searchBasePath = '/search/' . rawurlencode(trim($query));
|
|
$this->index();
|
|
}
|
|
|
|
public function category(string $slug): void
|
|
{
|
|
$categoryRepository = new CategoryRepository();
|
|
$productRepository = new ProductRepository();
|
|
$category = $categoryRepository->findBySlug($slug);
|
|
|
|
if ($category === null) {
|
|
http_response_code(404);
|
|
view('pages/404', [
|
|
'title' => 'Категория не найдена - Рыбсток',
|
|
'breadcrumbs' => [
|
|
['title' => 'Главная', 'url' => '/'],
|
|
['title' => 'Каталог', 'url' => '/catalog'],
|
|
['title' => 'Категория не найдена'],
|
|
],
|
|
]);
|
|
return;
|
|
}
|
|
|
|
$categoryName = (string) $category['name'];
|
|
$pagination = $this->paginationInput();
|
|
$totalProducts = $productRepository->categoryTotal((int) $category['id'], true);
|
|
$products = $productRepository->forCategory((int) $category['id'], $pagination['limit'], $pagination['offset'], true);
|
|
|
|
view('catalog/index', [
|
|
'title' => $category['seo_title'] ?: $categoryName . ' - купить в Рыбсток с доставкой по Москве',
|
|
'metaDescription' => $category['seo_description'] ?: $categoryName . ' в интернет-складе Рыбсток: качественная продукция по стоковым ценам, фасовки для дома и бизнеса, доставка по Москве и Московской области.',
|
|
'metaKeywords' => $category['seo_keywords'] ?: $categoryName . ', купить ' . $categoryName . ' Москва, ' . $categoryName . ' доставка, Рыбсток, продукты по стоковым ценам',
|
|
'breadcrumbs' => $categoryRepository->breadcrumbs((int) $category['id']),
|
|
'categories' => $categoryRepository->tree(),
|
|
'currentCategory' => $category,
|
|
'products' => $products,
|
|
'pagination' => $this->paginationData('/catalog/' . (string) $category['slug'], $totalProducts, count($products), $pagination),
|
|
]);
|
|
}
|
|
|
|
public function product(string $slug): void
|
|
{
|
|
$categoryRepository = new CategoryRepository();
|
|
$productRepository = new ProductRepository();
|
|
$product = $productRepository->findBySlug($slug);
|
|
|
|
if ($product === null) {
|
|
http_response_code(404);
|
|
view('pages/404', [
|
|
'title' => 'Товар не найден - Рыбсток',
|
|
'breadcrumbs' => [
|
|
['title' => 'Главная', 'url' => '/'],
|
|
['title' => 'Каталог', 'url' => '/catalog'],
|
|
['title' => 'Товар не найден'],
|
|
],
|
|
]);
|
|
return;
|
|
}
|
|
|
|
$breadcrumbs = [
|
|
['title' => 'Главная', 'url' => '/'],
|
|
['title' => 'Каталог', 'url' => '/catalog'],
|
|
];
|
|
|
|
if (!empty($product['display_category_id'] ?? $product['category_id'] ?? null)) {
|
|
$breadcrumbs = $categoryRepository->breadcrumbs((int) ($product['display_category_id'] ?? $product['category_id']));
|
|
}
|
|
|
|
$breadcrumbs[] = ['title' => (string) $product['name']];
|
|
|
|
view('catalog/product', [
|
|
'title' => $product['seo_title'] ?: $product['name'] . ' - купить в Рыбсток',
|
|
'metaDescription' => $product['seo_description'] ?: $product['short_description'] ?: '',
|
|
'metaKeywords' => $product['seo_keywords'] ?: '',
|
|
'breadcrumbs' => $breadcrumbs,
|
|
'product' => $product,
|
|
'variants' => $productRepository->variantsForProduct((int) $product['id']),
|
|
'images' => $productRepository->imagesForProduct((int) $product['id']),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* @return array{page:int, limit:int, offset:int, per_page:int, cumulative:bool}
|
|
*/
|
|
private function paginationInput(): array
|
|
{
|
|
$perPage = 48;
|
|
$cumulativeLimit = isset($_GET['limit']) ? (int) $_GET['limit'] : 0;
|
|
|
|
if ($cumulativeLimit > 0) {
|
|
$limit = max($perPage, min($cumulativeLimit, 5000));
|
|
|
|
return [
|
|
'page' => 1,
|
|
'limit' => $limit,
|
|
'offset' => 0,
|
|
'per_page' => $perPage,
|
|
'cumulative' => true,
|
|
];
|
|
}
|
|
|
|
$page = max(1, (int) ($_GET['page'] ?? 1));
|
|
|
|
return [
|
|
'page' => $page,
|
|
'limit' => $perPage,
|
|
'offset' => ($page - 1) * $perPage,
|
|
'per_page' => $perPage,
|
|
'cumulative' => false,
|
|
];
|
|
}
|
|
|
|
private function queryParameter(string $name): string
|
|
{
|
|
$query = (string) (parse_url($_SERVER['REQUEST_URI'] ?? '', PHP_URL_QUERY) ?? '');
|
|
if ($query === '') {
|
|
return '';
|
|
}
|
|
|
|
parse_str($query, $parameters);
|
|
|
|
return (string) ($parameters[$name] ?? '');
|
|
}
|
|
|
|
/**
|
|
* @return array{products: array<int, array<string, mixed>>, total: int}
|
|
*/
|
|
private function searchProducts(ProductRepository $productRepository, string $query, int $limit, int $offset): array
|
|
{
|
|
if (method_exists($productRepository, 'searchCatalog')) {
|
|
return $productRepository->searchCatalog($query, $limit, $offset, true);
|
|
}
|
|
|
|
$needle = $this->lower($query);
|
|
$words = array_values(array_filter(preg_split('/\s+/u', $needle) ?: []));
|
|
$ranked = [];
|
|
|
|
foreach ($productRepository->catalogPreview(5000, 0, true) as $product) {
|
|
$haystack = $this->lower(implode(' ', [
|
|
(string) ($product['name'] ?? ''),
|
|
(string) ($product['short_description'] ?? ''),
|
|
(string) ($product['category_name'] ?? ''),
|
|
(string) ($product['category_display_name'] ?? ''),
|
|
]));
|
|
|
|
$score = 0;
|
|
if ($needle !== '' && str_contains($haystack, $needle)) {
|
|
$score += 100;
|
|
}
|
|
|
|
foreach ($words as $word) {
|
|
if ($word !== '' && str_contains($haystack, $word)) {
|
|
$score += 20;
|
|
}
|
|
}
|
|
|
|
if ($score > 0) {
|
|
$product['_search_score'] = $score;
|
|
$ranked[] = $product;
|
|
}
|
|
}
|
|
|
|
usort($ranked, static fn (array $left, array $right): int => ($right['_search_score'] ?? 0) <=> ($left['_search_score'] ?? 0));
|
|
|
|
foreach ($ranked as &$product) {
|
|
unset($product['_search_score']);
|
|
}
|
|
unset($product);
|
|
|
|
return [
|
|
'products' => array_slice($ranked, $offset, $limit),
|
|
'total' => count($ranked),
|
|
];
|
|
}
|
|
|
|
private function lower(string $value): string
|
|
{
|
|
return function_exists('mb_strtolower') ? mb_strtolower($value, 'UTF-8') : strtolower($value);
|
|
}
|
|
|
|
/**
|
|
* @param array{page:int, limit:int, offset:int, per_page:int, cumulative:bool} $input
|
|
* @return array<string, mixed>
|
|
*/
|
|
private function paginationData(string $basePath, int $total, int $currentCount, array $input): array
|
|
{
|
|
$shown = min($total, $input['offset'] + $currentCount);
|
|
$nextLimit = min($total, max($shown, $input['limit']) + $input['per_page']);
|
|
|
|
return [
|
|
'base_path' => $basePath,
|
|
'total' => $total,
|
|
'shown' => $shown,
|
|
'page' => $input['page'],
|
|
'pages' => max(1, (int) ceil($total / $input['per_page'])),
|
|
'per_page' => $input['per_page'],
|
|
'limit' => $input['limit'],
|
|
'has_more' => $shown < $total,
|
|
'next_limit' => $nextLimit,
|
|
'cumulative' => $input['cumulative'],
|
|
];
|
|
}
|
|
}
|