> */ public function active(): array { $statement = $this->pdo()->query( 'SELECT id, parent_id, name, slug, legacy_path, h1, seo_title, seo_description, seo_keywords, image_path, sort_order FROM categories WHERE is_active = 1 ORDER BY parent_id IS NOT NULL, sort_order, name' ); return $statement->fetchAll(); } /** * @return array> */ public function flatActive(): array { $statement = $this->pdo()->query( 'SELECT id, parent_id, name, slug, sort_order FROM categories WHERE is_active = 1 ORDER BY parent_id IS NOT NULL, sort_order, name' ); return $statement->fetchAll(); } /** * @return array> */ public function tree(): array { $items = $this->active(); $childrenByParent = []; foreach ($items as $item) { $parentKey = $item['parent_id'] === null ? 0 : (int) $item['parent_id']; $childrenByParent[$parentKey][] = $item; } $childrenByParent[0] = $this->sortRootCategories($childrenByParent[0] ?? []); return $this->buildTree($childrenByParent, 0); } /** * @param array> $categories * @return array> */ private function sortRootCategories(array $categories): array { usort($categories, function (array $left, array $right): int { return [ $this->rootCategoryWeight((string) ($left['name'] ?? '')), (int) ($left['sort_order'] ?? 100), (string) ($left['name'] ?? ''), ] <=> [ $this->rootCategoryWeight((string) ($right['name'] ?? '')), (int) ($right['sort_order'] ?? 100), (string) ($right['name'] ?? ''), ]; }); return $categories; } private function rootCategoryWeight(string $name): int { $lowerName = trim(mb_strtolower($name)); return match (true) { str_contains($lowerName, 'рыб') => 10, str_contains($lowerName, 'морепроду') => 20, str_contains($lowerName, 'икра') => 30, $lowerName === 'мясо' => 40, str_contains($lowerName, 'сыр') => 50, str_contains($lowerName, 'полуфаб') => 60, str_contains($lowerName, 'фрукт') || str_contains($lowerName, 'овощ') || str_contains($lowerName, 'ягод') || str_contains($lowerName, 'гриб') => 70, str_contains($lowerName, 'бакале') => 80, str_contains($lowerName, 'мясная гастрономия') => 90, str_contains($lowerName, 'молоч') => 100, default => 1000, }; } /** * @param array>> $childrenByParent * @return array> */ private function buildTree(array $childrenByParent, int $parentId): array { $branch = []; foreach ($childrenByParent[$parentId] ?? [] as $item) { $item['children'] = $this->buildTree($childrenByParent, (int) $item['id']); $branch[] = $item; } return $branch; } /** * @return array|null */ public function findBySlug(string $slug): ?array { $statement = $this->pdo()->prepare( 'SELECT id, parent_id, name, slug, legacy_path, description, h1, seo_title, seo_description, seo_keywords, image_path FROM categories WHERE slug = :slug AND is_active = 1 LIMIT 1' ); $statement->execute(['slug' => $slug]); $category = $statement->fetch(); return $category ?: null; } /** * @return array> */ public function breadcrumbs(int $categoryId): array { $items = []; $currentId = $categoryId; while ($currentId > 0) { $statement = $this->pdo()->prepare( 'SELECT id, parent_id, name, slug FROM categories WHERE id = :id LIMIT 1' ); $statement->execute(['id' => $currentId]); $category = $statement->fetch(); if (!$category) { break; } array_unshift($items, [ 'title' => $category['name'], 'url' => '/catalog/' . $category['slug'], ]); $currentId = $category['parent_id'] === null ? 0 : (int) $category['parent_id']; } array_unshift($items, [ 'title' => 'Главная', 'url' => '/', ]); return $items; } /** * @return array */ public function descendantIds(int $categoryId): array { $items = $this->flatActive(); $childrenByParent = []; foreach ($items as $item) { $parentId = $item['parent_id'] === null ? 0 : (int) $item['parent_id']; $childrenByParent[$parentId][] = (int) $item['id']; } $ids = [$categoryId]; $queue = [$categoryId]; while ($queue !== []) { $currentId = array_shift($queue); foreach ($childrenByParent[$currentId] ?? [] as $childId) { $ids[] = $childId; $queue[] = $childId; } } return array_values(array_unique($ids)); } /** * @param array $data */ public function create(array $data): int { if (($data['name'] ?? '') === '' || ($data['slug'] ?? '') === '') { throw new \InvalidArgumentException('Название и slug обязательны.'); } $statement = $this->pdo()->prepare( 'INSERT INTO categories (parent_id, name, slug, h1, seo_title, seo_description, seo_keywords, sort_order, is_active) VALUES (:parent_id, :name, :slug, :h1, :seo_title, :seo_description, :seo_keywords, :sort_order, :is_active)' ); $statement->execute([ 'parent_id' => $data['parent_id'], 'name' => $data['name'], 'slug' => $data['slug'], 'h1' => $data['h1'] ?: null, 'seo_title' => $data['seo_title'] ?: null, 'seo_description' => $data['seo_description'] ?: null, 'seo_keywords' => $data['seo_keywords'] ?: null, 'sort_order' => $data['sort_order'] ?? 100, 'is_active' => $data['is_active'] ?? 1, ]); return (int) $this->pdo()->lastInsertId(); } }