шаг 3 завершен

This commit is contained in:
Alisa
2026-06-04 00:27:17 +03:00
parent 420e67c9bd
commit 62ce8c4d01
27 changed files with 1565 additions and 42 deletions
+16
View File
@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace App\Repositories;
use App\Core\Database;
use PDO;
abstract class BaseRepository
{
protected function pdo(): PDO
{
return Database::pdo();
}
}
+110
View File
@@ -0,0 +1,110 @@
<?php
declare(strict_types=1);
namespace App\Repositories;
final class CategoryRepository extends BaseRepository
{
/**
* @return array<int, array<string, mixed>>
*/
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<int, array<string, mixed>>
*/
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;
}
return $this->buildTree($childrenByParent, 0);
}
/**
* @param array<int, array<int, array<string, mixed>>> $childrenByParent
* @return array<int, array<string, mixed>>
*/
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<string, mixed>|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<int, array<string, string>>
*/
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;
}
}
+103
View File
@@ -0,0 +1,103 @@
<?php
declare(strict_types=1);
namespace App\Repositories;
use App\Services\PriceTierService;
final class ProductRepository extends BaseRepository
{
/**
* @return array<int, array<string, mixed>>
*/
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<int, array<string, mixed>>
*/
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());
}
private function previewSelectSql(string $orderBy, 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
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
' . $orderBy . '
LIMIT ' . $limit;
}
/**
* @param array<int, array<string, mixed>> $products
* @return array<int, array<string, mixed>>
*/
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;
}
}
@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace App\Repositories;
final class ProductVariantRepository extends BaseRepository
{
/**
* @return array<int, array<string, mixed>>
*/
public function publishedForProduct(int $productId): array
{
$statement = $this->pdo()->prepare(
'SELECT id, product_id, name, unit, package_quantity, step_quantity, price_per_unit, package_price, is_default
FROM product_variants
WHERE product_id = :product_id
AND is_published = 1
AND is_available = 1
ORDER BY is_default DESC, sort_order, id'
);
$statement->execute(['product_id' => $productId]);
return $statement->fetchAll();
}
}
+25
View File
@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace App\Repositories;
final class PromocodeRepository extends BaseRepository
{
/**
* @return array<int, array<string, mixed>>
*/
public function priceTiers(): array
{
$statement = $this->pdo()->query(
'SELECT code, title, discount_type, discount_value, min_order_amount, price_tier_label, payment_note
FROM promocodes
WHERE is_active = 1
AND show_in_price_tiers = 1
AND discount_type = "percent"
ORDER BY min_order_amount, discount_value'
);
return $statement->fetchAll();
}
}
+38
View File
@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace App\Repositories;
final class SettingRepository extends BaseRepository
{
/**
* @return array<string, mixed>
*/
public function allKeyed(): array
{
$statement = $this->pdo()->query(
'SELECT setting_key, setting_value, value_type
FROM site_settings
ORDER BY group_name, setting_key'
);
$settings = [];
foreach ($statement->fetchAll() as $row) {
$settings[$row['setting_key']] = $this->castValue($row['setting_value'], $row['value_type']);
}
return $settings;
}
private function castValue(?string $value, string $type): mixed
{
return match ($type) {
'number' => $value === null ? null : (float) $value,
'bool' => $value === '1' || $value === 'true',
'json' => $value === null ? null : json_decode($value, true),
default => $value,
};
}
}