[MVC] Полная миграция на MVC архитектуру
- Создано ядро MVC: App, Router, Controller, Model, View, Database - Созданы модели: User, Product, Category, Cart, Order - Созданы контроллеры: Home, Auth, Product, Cart, Order, Page, Admin - Созданы layouts и partials для представлений - Добавлены все views для страниц - Настроена маршрутизация с чистыми URL - Обновлена конфигурация Docker и Apache для mod_rewrite - Добавлена единая точка входа public/index.php
This commit is contained in:
145
app/Models/Cart.php
Normal file
145
app/Models/Cart.php
Normal file
@@ -0,0 +1,145 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Core\Model;
|
||||
|
||||
/**
|
||||
* Cart - модель корзины
|
||||
*/
|
||||
class Cart extends Model
|
||||
{
|
||||
protected string $table = 'cart';
|
||||
protected string $primaryKey = 'cart_id';
|
||||
|
||||
/**
|
||||
* Получить корзину пользователя
|
||||
*/
|
||||
public function getUserCart(int $userId): array
|
||||
{
|
||||
$sql = "SELECT
|
||||
c.cart_id,
|
||||
c.product_id,
|
||||
c.quantity,
|
||||
p.name,
|
||||
p.price,
|
||||
p.image_url,
|
||||
p.stock_quantity,
|
||||
p.is_available
|
||||
FROM {$this->table} c
|
||||
JOIN products p ON c.product_id = p.product_id
|
||||
WHERE c.user_id = ? AND p.is_available = TRUE
|
||||
ORDER BY c.created_at DESC";
|
||||
return $this->query($sql, [$userId]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить общую сумму корзины
|
||||
*/
|
||||
public function getCartTotal(int $userId): array
|
||||
{
|
||||
$sql = "SELECT
|
||||
SUM(c.quantity) as total_quantity,
|
||||
SUM(c.quantity * p.price) as total_amount
|
||||
FROM {$this->table} c
|
||||
JOIN products p ON c.product_id = p.product_id
|
||||
WHERE c.user_id = ? AND p.is_available = TRUE";
|
||||
$result = $this->queryOne($sql, [$userId]);
|
||||
|
||||
return [
|
||||
'quantity' => (int) ($result['total_quantity'] ?? 0),
|
||||
'amount' => (float) ($result['total_amount'] ?? 0)
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить количество товаров в корзине
|
||||
*/
|
||||
public function getCount(int $userId): int
|
||||
{
|
||||
$sql = "SELECT COALESCE(SUM(quantity), 0) as total FROM {$this->table} WHERE user_id = ?";
|
||||
$result = $this->queryOne($sql, [$userId]);
|
||||
return (int) $result['total'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Добавить товар в корзину
|
||||
*/
|
||||
public function addItem(int $userId, int $productId, int $quantity = 1): bool
|
||||
{
|
||||
// Проверяем, есть ли уже этот товар в корзине
|
||||
$existing = $this->findWhere([
|
||||
'user_id' => $userId,
|
||||
'product_id' => $productId
|
||||
]);
|
||||
|
||||
if ($existing) {
|
||||
// Увеличиваем количество
|
||||
$newQuantity = $existing['quantity'] + $quantity;
|
||||
return $this->update($existing['cart_id'], [
|
||||
'quantity' => $newQuantity,
|
||||
'updated_at' => date('Y-m-d H:i:s')
|
||||
]);
|
||||
}
|
||||
|
||||
// Добавляем новую запись
|
||||
return $this->create([
|
||||
'user_id' => $userId,
|
||||
'product_id' => $productId,
|
||||
'quantity' => $quantity
|
||||
]) !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Обновить количество товара в корзине
|
||||
*/
|
||||
public function updateQuantity(int $userId, int $productId, int $quantity): bool
|
||||
{
|
||||
$sql = "UPDATE {$this->table}
|
||||
SET quantity = ?, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE user_id = ? AND product_id = ?";
|
||||
return $this->execute($sql, [$quantity, $userId, $productId]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Удалить товар из корзины
|
||||
*/
|
||||
public function removeItem(int $userId, int $productId): bool
|
||||
{
|
||||
$sql = "DELETE FROM {$this->table} WHERE user_id = ? AND product_id = ?";
|
||||
return $this->execute($sql, [$userId, $productId]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Очистить корзину пользователя
|
||||
*/
|
||||
public function clearCart(int $userId): bool
|
||||
{
|
||||
$sql = "DELETE FROM {$this->table} WHERE user_id = ?";
|
||||
return $this->execute($sql, [$userId]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверить, есть ли товар в корзине
|
||||
*/
|
||||
public function hasItem(int $userId, int $productId): bool
|
||||
{
|
||||
$item = $this->findWhere([
|
||||
'user_id' => $userId,
|
||||
'product_id' => $productId
|
||||
]);
|
||||
return $item !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить товар из корзины
|
||||
*/
|
||||
public function getItem(int $userId, int $productId): ?array
|
||||
{
|
||||
return $this->findWhere([
|
||||
'user_id' => $userId,
|
||||
'product_id' => $productId
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
159
app/Models/Category.php
Normal file
159
app/Models/Category.php
Normal file
@@ -0,0 +1,159 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Core\Model;
|
||||
|
||||
/**
|
||||
* Category - модель категории товаров
|
||||
*/
|
||||
class Category extends Model
|
||||
{
|
||||
protected string $table = 'categories';
|
||||
protected string $primaryKey = 'category_id';
|
||||
|
||||
/**
|
||||
* Получить активные категории
|
||||
*/
|
||||
public function getActive(): array
|
||||
{
|
||||
$sql = "SELECT * FROM {$this->table}
|
||||
WHERE is_active = TRUE
|
||||
ORDER BY sort_order, name";
|
||||
return $this->query($sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить родительские категории (без parent_id)
|
||||
*/
|
||||
public function getParent(): array
|
||||
{
|
||||
$sql = "SELECT * FROM {$this->table}
|
||||
WHERE parent_id IS NULL AND is_active = TRUE
|
||||
ORDER BY sort_order, name";
|
||||
return $this->query($sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить подкатегории
|
||||
*/
|
||||
public function getChildren(int $parentId): array
|
||||
{
|
||||
$sql = "SELECT * FROM {$this->table}
|
||||
WHERE parent_id = ? AND is_active = TRUE
|
||||
ORDER BY sort_order, name";
|
||||
return $this->query($sql, [$parentId]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить категорию по slug
|
||||
*/
|
||||
public function findBySlug(string $slug): ?array
|
||||
{
|
||||
return $this->findWhere(['slug' => $slug]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить все категории с количеством товаров
|
||||
*/
|
||||
public function getAllWithProductCount(): array
|
||||
{
|
||||
$sql = "SELECT c1.*, c2.name as parent_name,
|
||||
(SELECT COUNT(*) FROM products p WHERE p.category_id = c1.category_id) as product_count
|
||||
FROM {$this->table} c1
|
||||
LEFT JOIN {$this->table} c2 ON c1.parent_id = c2.category_id
|
||||
ORDER BY c1.sort_order, c1.name";
|
||||
return $this->query($sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* Создать категорию
|
||||
*/
|
||||
public function createCategory(array $data): ?int
|
||||
{
|
||||
$slug = $this->generateSlug($data['name']);
|
||||
|
||||
return $this->create([
|
||||
'name' => $data['name'],
|
||||
'slug' => $slug,
|
||||
'parent_id' => $data['parent_id'] ?? null,
|
||||
'description' => $data['description'] ?? null,
|
||||
'sort_order' => $data['sort_order'] ?? 0,
|
||||
'is_active' => $data['is_active'] ?? true
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Обновить категорию
|
||||
*/
|
||||
public function updateCategory(int $id, array $data): bool
|
||||
{
|
||||
$updateData = [
|
||||
'name' => $data['name'],
|
||||
'slug' => $this->generateSlug($data['name']),
|
||||
'parent_id' => $data['parent_id'] ?? null,
|
||||
'description' => $data['description'] ?? null,
|
||||
'sort_order' => $data['sort_order'] ?? 0,
|
||||
'is_active' => $data['is_active'] ?? true,
|
||||
'updated_at' => date('Y-m-d H:i:s')
|
||||
];
|
||||
|
||||
return $this->update($id, $updateData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Безопасное удаление категории
|
||||
*/
|
||||
public function safeDelete(int $id): array
|
||||
{
|
||||
// Проверяем наличие товаров
|
||||
$sql = "SELECT COUNT(*) as cnt FROM products WHERE category_id = ?";
|
||||
$result = $this->queryOne($sql, [$id]);
|
||||
$productCount = (int) $result['cnt'];
|
||||
|
||||
// Проверяем наличие дочерних категорий
|
||||
$sql = "SELECT COUNT(*) as cnt FROM {$this->table} WHERE parent_id = ?";
|
||||
$result = $this->queryOne($sql, [$id]);
|
||||
$childCount = (int) $result['cnt'];
|
||||
|
||||
if ($productCount > 0 || $childCount > 0) {
|
||||
// Скрываем вместо удаления
|
||||
$this->update($id, ['is_active' => false]);
|
||||
return [
|
||||
'deleted' => false,
|
||||
'hidden' => true,
|
||||
'reason' => $productCount > 0 ? 'has_products' : 'has_children'
|
||||
];
|
||||
}
|
||||
|
||||
// Удаляем полностью
|
||||
$this->delete($id);
|
||||
return ['deleted' => true, 'hidden' => false];
|
||||
}
|
||||
|
||||
/**
|
||||
* Генерация slug из названия
|
||||
*/
|
||||
private function generateSlug(string $name): string
|
||||
{
|
||||
$slug = mb_strtolower($name);
|
||||
|
||||
// Транслитерация
|
||||
$transliteration = [
|
||||
'а' => 'a', 'б' => 'b', 'в' => 'v', 'г' => 'g', 'д' => 'd',
|
||||
'е' => 'e', 'ё' => 'yo', 'ж' => 'zh', 'з' => 'z', 'и' => 'i',
|
||||
'й' => 'y', 'к' => 'k', 'л' => 'l', 'м' => 'm', 'н' => 'n',
|
||||
'о' => 'o', 'п' => 'p', 'р' => 'r', 'с' => 's', 'т' => 't',
|
||||
'у' => 'u', 'ф' => 'f', 'х' => 'h', 'ц' => 'ts', 'ч' => 'ch',
|
||||
'ш' => 'sh', 'щ' => 'sch', 'ъ' => '', 'ы' => 'y', 'ь' => '',
|
||||
'э' => 'e', 'ю' => 'yu', 'я' => 'ya'
|
||||
];
|
||||
|
||||
$slug = strtr($slug, $transliteration);
|
||||
$slug = preg_replace('/[^a-z0-9]+/', '-', $slug);
|
||||
$slug = trim($slug, '-');
|
||||
|
||||
return $slug;
|
||||
}
|
||||
}
|
||||
|
||||
217
app/Models/Order.php
Normal file
217
app/Models/Order.php
Normal file
@@ -0,0 +1,217 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Core\Model;
|
||||
use App\Core\Database;
|
||||
|
||||
/**
|
||||
* Order - модель заказа
|
||||
*/
|
||||
class Order extends Model
|
||||
{
|
||||
protected string $table = 'orders';
|
||||
protected string $primaryKey = 'order_id';
|
||||
|
||||
/**
|
||||
* Создать заказ из корзины
|
||||
*/
|
||||
public function createFromCart(int $userId, array $cartItems, array $orderData): ?array
|
||||
{
|
||||
$db = Database::getInstance();
|
||||
|
||||
try {
|
||||
$db->beginTransaction();
|
||||
|
||||
// Считаем итоговую сумму
|
||||
$subtotal = 0;
|
||||
foreach ($cartItems as $item) {
|
||||
$subtotal += $item['price'] * $item['quantity'];
|
||||
}
|
||||
|
||||
$discountAmount = (float) ($orderData['discount'] ?? 0);
|
||||
$deliveryPrice = (float) ($orderData['delivery_price'] ?? 2000);
|
||||
$finalAmount = $subtotal - $discountAmount + $deliveryPrice;
|
||||
|
||||
// Генерируем номер заказа
|
||||
$orderNumber = 'ORD-' . date('Ymd-His') . '-' . rand(1000, 9999);
|
||||
|
||||
// Создаем заказ
|
||||
$orderId = $this->create([
|
||||
'user_id' => $userId,
|
||||
'order_number' => $orderNumber,
|
||||
'customer_name' => $orderData['customer_name'],
|
||||
'customer_email' => $orderData['customer_email'],
|
||||
'customer_phone' => $orderData['customer_phone'],
|
||||
'delivery_address' => $orderData['delivery_address'],
|
||||
'delivery_region' => $orderData['delivery_region'] ?? null,
|
||||
'postal_code' => $orderData['postal_code'] ?? null,
|
||||
'delivery_method' => $orderData['delivery_method'] ?? 'courier',
|
||||
'payment_method' => $orderData['payment_method'] ?? 'card',
|
||||
'subtotal' => $subtotal,
|
||||
'discount_amount' => $discountAmount,
|
||||
'delivery_price' => $deliveryPrice,
|
||||
'final_amount' => $finalAmount,
|
||||
'promo_code' => $orderData['promo_code'] ?? null,
|
||||
'status' => 'pending',
|
||||
'notes' => $orderData['notes'] ?? null
|
||||
]);
|
||||
|
||||
if (!$orderId) {
|
||||
throw new \Exception('Не удалось создать заказ');
|
||||
}
|
||||
|
||||
// Добавляем товары в заказ
|
||||
foreach ($cartItems as $item) {
|
||||
$this->addOrderItem($orderId, $item);
|
||||
}
|
||||
|
||||
// Уменьшаем количество товаров на складе
|
||||
$productModel = new Product();
|
||||
foreach ($cartItems as $item) {
|
||||
$productModel->decreaseStock($item['product_id'], $item['quantity']);
|
||||
}
|
||||
|
||||
// Очищаем корзину
|
||||
$cartModel = new Cart();
|
||||
$cartModel->clearCart($userId);
|
||||
|
||||
$db->commit();
|
||||
|
||||
return [
|
||||
'order_id' => $orderId,
|
||||
'order_number' => $orderNumber,
|
||||
'final_amount' => $finalAmount
|
||||
];
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$db->rollBack();
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Добавить товар в заказ
|
||||
*/
|
||||
private function addOrderItem(int $orderId, array $item): void
|
||||
{
|
||||
$sql = "INSERT INTO order_items
|
||||
(order_id, product_id, product_name, quantity, product_price, total_price)
|
||||
VALUES (?, ?, ?, ?, ?, ?)";
|
||||
|
||||
$totalPrice = $item['price'] * $item['quantity'];
|
||||
|
||||
$this->execute($sql, [
|
||||
$orderId,
|
||||
$item['product_id'],
|
||||
$item['name'],
|
||||
$item['quantity'],
|
||||
$item['price'],
|
||||
$totalPrice
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить заказ с подробностями
|
||||
*/
|
||||
public function getWithDetails(int $orderId): ?array
|
||||
{
|
||||
$sql = "SELECT o.*, u.email as user_email, u.full_name as user_full_name
|
||||
FROM {$this->table} o
|
||||
LEFT JOIN users u ON o.user_id = u.user_id
|
||||
WHERE o.order_id = ?";
|
||||
|
||||
$order = $this->queryOne($sql, [$orderId]);
|
||||
|
||||
if ($order) {
|
||||
$order['items'] = $this->getOrderItems($orderId);
|
||||
}
|
||||
|
||||
return $order;
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить товары заказа
|
||||
*/
|
||||
public function getOrderItems(int $orderId): array
|
||||
{
|
||||
$sql = "SELECT oi.*, p.image_url
|
||||
FROM order_items oi
|
||||
LEFT JOIN products p ON oi.product_id = p.product_id
|
||||
WHERE oi.order_id = ?";
|
||||
return $this->query($sql, [$orderId]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить заказы пользователя
|
||||
*/
|
||||
public function getUserOrders(int $userId, int $limit = 50): array
|
||||
{
|
||||
$sql = "SELECT * FROM {$this->table}
|
||||
WHERE user_id = ?
|
||||
ORDER BY created_at DESC
|
||||
LIMIT ?";
|
||||
return $this->query($sql, [$userId, $limit]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить все заказы для админки
|
||||
*/
|
||||
public function getAllForAdmin(int $limit = 50): array
|
||||
{
|
||||
$sql = "SELECT o.*, u.email as user_email
|
||||
FROM {$this->table} o
|
||||
LEFT JOIN users u ON o.user_id = u.user_id
|
||||
ORDER BY o.created_at DESC
|
||||
LIMIT ?";
|
||||
return $this->query($sql, [$limit]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Обновить статус заказа
|
||||
*/
|
||||
public function updateStatus(int $orderId, string $status): bool
|
||||
{
|
||||
$updateData = [
|
||||
'status' => $status,
|
||||
'updated_at' => date('Y-m-d H:i:s')
|
||||
];
|
||||
|
||||
if ($status === 'completed') {
|
||||
$updateData['completed_at'] = date('Y-m-d H:i:s');
|
||||
}
|
||||
|
||||
return $this->update($orderId, $updateData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить статистику заказов
|
||||
*/
|
||||
public function getStats(): array
|
||||
{
|
||||
$stats = [];
|
||||
|
||||
// Общее количество заказов
|
||||
$result = $this->queryOne("SELECT COUNT(*) as cnt FROM {$this->table}");
|
||||
$stats['total'] = (int) $result['cnt'];
|
||||
|
||||
// Выручка
|
||||
$result = $this->queryOne(
|
||||
"SELECT COALESCE(SUM(final_amount), 0) as revenue
|
||||
FROM {$this->table} WHERE status = 'completed'"
|
||||
);
|
||||
$stats['revenue'] = (float) $result['revenue'];
|
||||
|
||||
// По статусам
|
||||
$statuses = $this->query(
|
||||
"SELECT status, COUNT(*) as cnt FROM {$this->table} GROUP BY status"
|
||||
);
|
||||
$stats['by_status'] = [];
|
||||
foreach ($statuses as $s) {
|
||||
$stats['by_status'][$s['status']] = (int) $s['cnt'];
|
||||
}
|
||||
|
||||
return $stats;
|
||||
}
|
||||
}
|
||||
|
||||
239
app/Models/Product.php
Normal file
239
app/Models/Product.php
Normal file
@@ -0,0 +1,239 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Core\Model;
|
||||
|
||||
/**
|
||||
* Product - модель товара
|
||||
*/
|
||||
class Product extends Model
|
||||
{
|
||||
protected string $table = 'products';
|
||||
protected string $primaryKey = 'product_id';
|
||||
|
||||
/**
|
||||
* Получить товар с категорией
|
||||
*/
|
||||
public function findWithCategory(int $id): ?array
|
||||
{
|
||||
$sql = "SELECT p.*, c.name as category_name, c.slug as category_slug
|
||||
FROM {$this->table} p
|
||||
LEFT JOIN categories c ON p.category_id = c.category_id
|
||||
WHERE p.product_id = ?";
|
||||
return $this->queryOne($sql, [$id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить доступные товары
|
||||
*/
|
||||
public function getAvailable(array $filters = [], int $limit = 50): array
|
||||
{
|
||||
$sql = "SELECT p.*, c.name as category_name
|
||||
FROM {$this->table} p
|
||||
LEFT JOIN categories c ON p.category_id = c.category_id
|
||||
WHERE p.is_available = TRUE";
|
||||
$params = [];
|
||||
|
||||
// Фильтр по категории
|
||||
if (!empty($filters['category_id'])) {
|
||||
$sql .= " AND p.category_id = ?";
|
||||
$params[] = $filters['category_id'];
|
||||
}
|
||||
|
||||
// Фильтр по цене
|
||||
if (!empty($filters['min_price'])) {
|
||||
$sql .= " AND p.price >= ?";
|
||||
$params[] = $filters['min_price'];
|
||||
}
|
||||
if (!empty($filters['max_price'])) {
|
||||
$sql .= " AND p.price <= ?";
|
||||
$params[] = $filters['max_price'];
|
||||
}
|
||||
|
||||
// Фильтр по цветам
|
||||
if (!empty($filters['colors']) && is_array($filters['colors'])) {
|
||||
$placeholders = implode(',', array_fill(0, count($filters['colors']), '?'));
|
||||
$sql .= " AND p.color IN ({$placeholders})";
|
||||
$params = array_merge($params, $filters['colors']);
|
||||
}
|
||||
|
||||
// Фильтр по материалам
|
||||
if (!empty($filters['materials']) && is_array($filters['materials'])) {
|
||||
$placeholders = implode(',', array_fill(0, count($filters['materials']), '?'));
|
||||
$sql .= " AND p.material IN ({$placeholders})";
|
||||
$params = array_merge($params, $filters['materials']);
|
||||
}
|
||||
|
||||
// Поиск по названию/описанию
|
||||
if (!empty($filters['search'])) {
|
||||
$sql .= " AND (p.name ILIKE ? OR p.description ILIKE ?)";
|
||||
$search = '%' . $filters['search'] . '%';
|
||||
$params[] = $search;
|
||||
$params[] = $search;
|
||||
}
|
||||
|
||||
$sql .= " ORDER BY p.product_id ASC LIMIT ?";
|
||||
$params[] = $limit;
|
||||
|
||||
return $this->query($sql, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить все товары (включая недоступные) для админки
|
||||
*/
|
||||
public function getAllForAdmin(bool $showAll = true): array
|
||||
{
|
||||
$sql = "SELECT p.*, c.name as category_name
|
||||
FROM {$this->table} p
|
||||
LEFT JOIN categories c ON p.category_id = c.category_id";
|
||||
|
||||
if (!$showAll) {
|
||||
$sql .= " WHERE p.is_available = TRUE";
|
||||
}
|
||||
|
||||
$sql .= " ORDER BY p.created_at DESC";
|
||||
|
||||
return $this->query($sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить похожие товары
|
||||
*/
|
||||
public function getSimilar(int $productId, int $categoryId, int $limit = 3): array
|
||||
{
|
||||
$sql = "SELECT * FROM {$this->table}
|
||||
WHERE category_id = ?
|
||||
AND product_id != ?
|
||||
AND is_available = TRUE
|
||||
ORDER BY RANDOM()
|
||||
LIMIT ?";
|
||||
return $this->query($sql, [$categoryId, $productId, $limit]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить доступные цвета
|
||||
*/
|
||||
public function getAvailableColors(): array
|
||||
{
|
||||
$sql = "SELECT DISTINCT color FROM {$this->table}
|
||||
WHERE color IS NOT NULL AND color != '' AND is_available = TRUE
|
||||
ORDER BY color";
|
||||
$result = $this->query($sql);
|
||||
return array_column($result, 'color');
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить доступные материалы
|
||||
*/
|
||||
public function getAvailableMaterials(): array
|
||||
{
|
||||
$sql = "SELECT DISTINCT material FROM {$this->table}
|
||||
WHERE material IS NOT NULL AND material != '' AND is_available = TRUE
|
||||
ORDER BY material";
|
||||
$result = $this->query($sql);
|
||||
return array_column($result, 'material');
|
||||
}
|
||||
|
||||
/**
|
||||
* Создать товар
|
||||
*/
|
||||
public function createProduct(array $data): ?int
|
||||
{
|
||||
$slug = $this->generateSlug($data['name']);
|
||||
$sku = $data['sku'] ?? $this->generateSku($data['name']);
|
||||
|
||||
return $this->create([
|
||||
'category_id' => $data['category_id'],
|
||||
'name' => $data['name'],
|
||||
'slug' => $slug,
|
||||
'description' => $data['description'] ?? null,
|
||||
'price' => $data['price'],
|
||||
'old_price' => $data['old_price'] ?? null,
|
||||
'sku' => $sku,
|
||||
'stock_quantity' => $data['stock_quantity'] ?? 0,
|
||||
'is_available' => $data['is_available'] ?? true,
|
||||
'is_featured' => $data['is_featured'] ?? false,
|
||||
'image_url' => $data['image_url'] ?? null,
|
||||
'color' => $data['color'] ?? null,
|
||||
'material' => $data['material'] ?? null,
|
||||
'card_size' => $data['card_size'] ?? 'small'
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Обновить товар
|
||||
*/
|
||||
public function updateProduct(int $id, array $data): bool
|
||||
{
|
||||
$updateData = [
|
||||
'name' => $data['name'],
|
||||
'category_id' => $data['category_id'],
|
||||
'description' => $data['description'] ?? null,
|
||||
'price' => $data['price'],
|
||||
'old_price' => $data['old_price'] ?? null,
|
||||
'stock_quantity' => $data['stock_quantity'] ?? 0,
|
||||
'is_available' => $data['is_available'] ?? true,
|
||||
'image_url' => $data['image_url'] ?? null,
|
||||
'color' => $data['color'] ?? null,
|
||||
'material' => $data['material'] ?? null,
|
||||
'updated_at' => date('Y-m-d H:i:s')
|
||||
];
|
||||
|
||||
return $this->update($id, $updateData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Уменьшить количество товара на складе
|
||||
*/
|
||||
public function decreaseStock(int $productId, int $quantity): bool
|
||||
{
|
||||
$sql = "UPDATE {$this->table}
|
||||
SET stock_quantity = stock_quantity - ?,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE product_id = ?";
|
||||
return $this->execute($sql, [$quantity, $productId]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверить наличие товара
|
||||
*/
|
||||
public function checkStock(int $productId, int $quantity): bool
|
||||
{
|
||||
$product = $this->find($productId);
|
||||
return $product && $product['is_available'] && $product['stock_quantity'] >= $quantity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Генерация slug
|
||||
*/
|
||||
private function generateSlug(string $name): string
|
||||
{
|
||||
$slug = mb_strtolower($name);
|
||||
|
||||
$transliteration = [
|
||||
'а' => 'a', 'б' => 'b', 'в' => 'v', 'г' => 'g', 'д' => 'd',
|
||||
'е' => 'e', 'ё' => 'yo', 'ж' => 'zh', 'з' => 'z', 'и' => 'i',
|
||||
'й' => 'y', 'к' => 'k', 'л' => 'l', 'м' => 'm', 'н' => 'n',
|
||||
'о' => 'o', 'п' => 'p', 'р' => 'r', 'с' => 's', 'т' => 't',
|
||||
'у' => 'u', 'ф' => 'f', 'х' => 'h', 'ц' => 'ts', 'ч' => 'ch',
|
||||
'ш' => 'sh', 'щ' => 'sch', 'ъ' => '', 'ы' => 'y', 'ь' => '',
|
||||
'э' => 'e', 'ю' => 'yu', 'я' => 'ya'
|
||||
];
|
||||
|
||||
$slug = strtr($slug, $transliteration);
|
||||
$slug = preg_replace('/[^a-z0-9]+/', '-', $slug);
|
||||
|
||||
return trim($slug, '-');
|
||||
}
|
||||
|
||||
/**
|
||||
* Генерация артикула
|
||||
*/
|
||||
private function generateSku(string $name): string
|
||||
{
|
||||
$prefix = strtoupper(substr(preg_replace('/[^a-zA-Z0-9]/', '', $name), 0, 6));
|
||||
return 'PROD-' . $prefix . '-' . rand(100, 999);
|
||||
}
|
||||
}
|
||||
|
||||
153
app/Models/User.php
Normal file
153
app/Models/User.php
Normal file
@@ -0,0 +1,153 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Core\Model;
|
||||
|
||||
/**
|
||||
* User - модель пользователя
|
||||
*/
|
||||
class User extends Model
|
||||
{
|
||||
protected string $table = 'users';
|
||||
protected string $primaryKey = 'user_id';
|
||||
|
||||
/**
|
||||
* Найти пользователя по email
|
||||
*/
|
||||
public function findByEmail(string $email): ?array
|
||||
{
|
||||
return $this->findWhere(['email' => $email]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверить пароль пользователя
|
||||
*/
|
||||
public function verifyPassword(string $password, string $hash): bool
|
||||
{
|
||||
return password_verify($password, $hash);
|
||||
}
|
||||
|
||||
/**
|
||||
* Хешировать пароль
|
||||
*/
|
||||
public function hashPassword(string $password): string
|
||||
{
|
||||
return password_hash($password, PASSWORD_DEFAULT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Создать нового пользователя
|
||||
*/
|
||||
public function register(array $data): ?int
|
||||
{
|
||||
$config = require dirname(__DIR__, 2) . '/config/app.php';
|
||||
|
||||
// Проверяем, является ли email администраторским
|
||||
$isAdmin = in_array(strtolower($data['email']), $config['admin_emails'] ?? []);
|
||||
|
||||
return $this->create([
|
||||
'email' => $data['email'],
|
||||
'password_hash' => $this->hashPassword($data['password']),
|
||||
'full_name' => $data['full_name'],
|
||||
'phone' => $data['phone'] ?? null,
|
||||
'city' => $data['city'] ?? null,
|
||||
'is_admin' => $isAdmin,
|
||||
'is_active' => true
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Авторизация пользователя
|
||||
*/
|
||||
public function authenticate(string $email, string $password): ?array
|
||||
{
|
||||
$user = $this->findByEmail($email);
|
||||
|
||||
if (!$user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!$user['is_active']) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!$this->verifyPassword($password, $user['password_hash'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Обновляем время последнего входа
|
||||
$this->update($user['user_id'], [
|
||||
'last_login' => date('Y-m-d H:i:s')
|
||||
]);
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить активных пользователей
|
||||
*/
|
||||
public function getActive(int $limit = 50): array
|
||||
{
|
||||
$sql = "SELECT * FROM {$this->table}
|
||||
WHERE is_active = TRUE
|
||||
ORDER BY created_at DESC
|
||||
LIMIT ?";
|
||||
return $this->query($sql, [$limit]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить всех пользователей с пагинацией
|
||||
*/
|
||||
public function getAllPaginated(int $limit = 50, int $offset = 0): array
|
||||
{
|
||||
$sql = "SELECT * FROM {$this->table}
|
||||
ORDER BY created_at DESC
|
||||
LIMIT ? OFFSET ?";
|
||||
return $this->query($sql, [$limit, $offset]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверить существование email
|
||||
*/
|
||||
public function emailExists(string $email): bool
|
||||
{
|
||||
$user = $this->findByEmail($email);
|
||||
return $user !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Обновить профиль пользователя
|
||||
*/
|
||||
public function updateProfile(int $userId, array $data): bool
|
||||
{
|
||||
$allowedFields = ['full_name', 'phone', 'city'];
|
||||
$updateData = array_intersect_key($data, array_flip($allowedFields));
|
||||
$updateData['updated_at'] = date('Y-m-d H:i:s');
|
||||
|
||||
return $this->update($userId, $updateData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Изменить пароль
|
||||
*/
|
||||
public function changePassword(int $userId, string $newPassword): bool
|
||||
{
|
||||
return $this->update($userId, [
|
||||
'password_hash' => $this->hashPassword($newPassword),
|
||||
'updated_at' => date('Y-m-d H:i:s')
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Заблокировать/разблокировать пользователя
|
||||
*/
|
||||
public function setActive(int $userId, bool $active): bool
|
||||
{
|
||||
return $this->update($userId, [
|
||||
'is_active' => $active,
|
||||
'updated_at' => date('Y-m-d H:i:s')
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user