шаг 3 завершен
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Core\Database;
|
||||
use App\Repositories\CategoryRepository;
|
||||
use App\Repositories\ProductRepository;
|
||||
use App\Repositories\SettingRepository;
|
||||
use Throwable;
|
||||
|
||||
final class HomeController
|
||||
{
|
||||
public function index(): void
|
||||
{
|
||||
$dbStatus = 'not_checked';
|
||||
$categories = [];
|
||||
$catalogProducts = [];
|
||||
$popularProducts = [];
|
||||
$settings = [];
|
||||
$notice = null;
|
||||
|
||||
try {
|
||||
Database::pdo()->query('SELECT 1');
|
||||
$dbStatus = 'connected';
|
||||
|
||||
$categories = (new CategoryRepository())->tree();
|
||||
$productRepository = new ProductRepository();
|
||||
$catalogProducts = $productRepository->catalogPreview(24);
|
||||
$popularProducts = $productRepository->popularPreview(10);
|
||||
$settings = (new SettingRepository())->allKeyed();
|
||||
} catch (Throwable $exception) {
|
||||
$dbStatus = app_env('APP_DEBUG', 'false') === 'true'
|
||||
? 'error: ' . $exception->getMessage()
|
||||
: 'error';
|
||||
$notice = 'Если таблицы еще не созданы, откройте страницу установки базы.';
|
||||
}
|
||||
|
||||
view('pages/home', [
|
||||
'title' => 'Рыбсток - продукты и рыба с доставкой по Москве и Московской области',
|
||||
'metaDescription' => 'Рыбсток - интернет-магазин рыбы, морепродуктов, мяса, сыров, полуфабрикатов и продуктов с доставкой по Москве и Московской области.',
|
||||
'metaKeywords' => 'Рыбсток, рыба, морепродукты, мясо, сыры, полуфабрикаты, доставка продуктов Москва',
|
||||
'breadcrumbs' => [
|
||||
['title' => 'Главная'],
|
||||
],
|
||||
'showBackButton' => false,
|
||||
'dbStatus' => $dbStatus,
|
||||
'categories' => $categories,
|
||||
'catalogProducts' => $catalogProducts,
|
||||
'popularProducts' => $popularProducts,
|
||||
'settings' => $settings,
|
||||
'notice' => $notice,
|
||||
'buildVersion' => '2026-06-04-02',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,12 @@ final class View
|
||||
|
||||
extract($data, EXTR_SKIP);
|
||||
|
||||
$title = $title ?? 'Рыбсток';
|
||||
$metaDescription = $metaDescription ?? '';
|
||||
$metaKeywords = $metaKeywords ?? '';
|
||||
$breadcrumbs = $breadcrumbs ?? [];
|
||||
$showBackButton = $showBackButton ?? true;
|
||||
|
||||
require base_path('views/layouts/main.php');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Repositories\PromocodeRepository;
|
||||
|
||||
final class PriceTierService
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ?PromocodeRepository $promocodes = null
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
public function forAmount(float $basePrice): array
|
||||
{
|
||||
$tiers = [[
|
||||
'label' => 'Базовая цена',
|
||||
'min_order_amount' => 0.0,
|
||||
'discount_percent' => 0.0,
|
||||
'price' => round($basePrice, 2),
|
||||
'note' => 'Без промокода',
|
||||
]];
|
||||
|
||||
foreach ($this->repository()->priceTiers() as $promocode) {
|
||||
$discount = (float) $promocode['discount_value'];
|
||||
$tiers[] = [
|
||||
'label' => $promocode['price_tier_label'] ?: $promocode['title'],
|
||||
'code' => $promocode['code'],
|
||||
'min_order_amount' => (float) $promocode['min_order_amount'],
|
||||
'discount_percent' => $discount,
|
||||
'price' => round($basePrice * (100 - $discount) / 100, 2),
|
||||
'note' => $promocode['payment_note'],
|
||||
];
|
||||
}
|
||||
|
||||
return $tiers;
|
||||
}
|
||||
|
||||
private function repository(): PromocodeRepository
|
||||
{
|
||||
return $this->promocodes ?? new PromocodeRepository();
|
||||
}
|
||||
}
|
||||
@@ -26,3 +26,8 @@ function view(string $template, array $data = []): void
|
||||
{
|
||||
View::render($template, $data);
|
||||
}
|
||||
|
||||
function e(?string $value): string
|
||||
{
|
||||
return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user