> */ public function popularPreview(int $limit = 10): array { $limit = max(1, min($limit, 30)); $sql = $this->previewSelectSql( 'ORDER BY p.sales_count DESC, RAND()', $limit ); $statement = $this->pdo()->query($sql); return $this->attachPriceTiers($statement->fetchAll()); } /** * @return array> */ public function catalogPreview(int $limit = 24): array { $limit = max(1, min($limit, 60)); $sql = $this->previewSelectSql( 'ORDER BY p.sort_order, p.name', $limit ); $statement = $this->pdo()->query($sql); return $this->attachPriceTiers($statement->fetchAll()); } /** * @return array> */ public function forCategory(int $categoryId, int $limit = 48): array { $limit = max(1, min($limit, 96)); $categoryIds = (new CategoryRepository())->descendantIds($categoryId); $placeholders = implode(',', array_fill(0, count($categoryIds), '?')); $sql = $this->previewSelectSql( 'AND p.category_id IN (' . $placeholders . ') ORDER BY p.sort_order, p.name', $limit ); $statement = $this->pdo()->prepare($sql); $statement->execute($categoryIds); return $this->attachPriceTiers($statement->fetchAll()); } /** * @return array> */ public function adminLatest(int $limit = 30): array { $limit = max(1, min($limit, 100)); $statement = $this->pdo()->query( 'SELECT p.id, p.name, p.slug, p.is_published, c.name AS category_name, p.created_at FROM products p LEFT JOIN categories c ON c.id = p.category_id ORDER BY p.created_at DESC, p.id DESC LIMIT ' . $limit ); return $statement->fetchAll(); } /** * @param array $data */ public function createWithVariant(array $data): int { if (($data['name'] ?? '') === '' || ($data['slug'] ?? '') === '') { throw new \InvalidArgumentException('Название и slug обязательны.'); } $pdo = $this->pdo(); $pdo->beginTransaction(); try { $statement = $pdo->prepare( 'INSERT INTO products (category_id, name, slug, short_description, seo_title, seo_description, seo_keywords, base_unit, is_published, is_available, published_at) VALUES (:category_id, :name, :slug, :short_description, :seo_title, :seo_description, :seo_keywords, :base_unit, :is_published, 1, :published_at)' ); $statement->execute([ 'category_id' => $data['category_id'], 'name' => $data['name'], 'slug' => $data['slug'], 'short_description' => $data['short_description'] ?: null, 'seo_title' => $data['seo_title'] ?: null, 'seo_description' => $data['seo_description'] ?: null, 'seo_keywords' => $data['seo_keywords'] ?: null, 'base_unit' => $data['base_unit'] ?: 'kg', 'is_published' => $data['is_published'] ?? 0, 'published_at' => !empty($data['is_published']) ? date('Y-m-d H:i:s') : null, ]); $productId = (int) $pdo->lastInsertId(); if (($data['variant_name'] ?? '') !== '' && (float) ($data['package_price'] ?? 0) > 0) { $variantStatement = $pdo->prepare( 'INSERT INTO product_variants (product_id, name, unit, package_quantity, step_quantity, price_per_unit, package_price, old_package_price, discount_type, discount_percent, is_default, is_published, is_available) VALUES (:product_id, :name, :unit, :package_quantity, :step_quantity, :price_per_unit, :package_price, :old_package_price, :discount_type, :discount_percent, 1, 1, 1)' ); $variantStatement->execute([ 'product_id' => $productId, 'name' => $data['variant_name'], 'unit' => $data['base_unit'] ?: 'kg', 'package_quantity' => $data['package_quantity'] ?: 1, 'step_quantity' => $data['step_quantity'] ?: ($data['package_quantity'] ?: 1), 'price_per_unit' => $data['price_per_unit'], 'package_price' => $data['package_price'], 'old_package_price' => $data['old_package_price'] ?? null, 'discount_type' => $data['discount_type'] ?? 'none', 'discount_percent' => $data['discount_percent'] ?? null, ]); } $pdo->commit(); return $productId; } catch (\Throwable $exception) { $pdo->rollBack(); throw $exception; } } private function previewSelectSql(string $tailSql, int $limit): string { return 'SELECT p.id, p.name, p.slug, p.legacy_path, p.h1, p.seo_title, p.seo_description, p.seo_keywords, c.name AS category_name, c.slug AS category_slug, p.main_image_path, p.base_unit, p.package_display_mode, p.sales_count, v.id AS variant_id, v.name AS variant_name, v.unit, v.package_quantity, v.price_per_unit, v.package_price, v.old_package_price, v.discount_type, v.discount_percent FROM products p LEFT JOIN categories c ON c.id = p.category_id LEFT JOIN product_variants v ON v.id = ( SELECT pv.id FROM product_variants pv WHERE pv.product_id = p.id AND pv.is_published = 1 AND pv.is_available = 1 ORDER BY pv.is_default DESC, pv.sort_order, pv.id LIMIT 1 ) WHERE p.is_published = 1 AND p.is_available = 1 ' . $tailSql . ' LIMIT ' . $limit; } /** * @param array> $products * @return array> */ private function attachPriceTiers(array $products): array { $priceTierService = new PriceTierService(); foreach ($products as &$product) { $basePrice = $product['price_per_unit'] ?? $product['package_price'] ?? null; $product['price_tiers'] = $basePrice === null ? [] : $priceTierService->forAmount((float) $basePrice); } unset($product); return $products; } }