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>, 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 */ 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'], ]; } }