[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:
kirill.khorkov
2026-01-03 11:48:14 +03:00
parent 3f257120fa
commit d2c15ec37f
53 changed files with 8650 additions and 30 deletions

View File

@@ -0,0 +1,374 @@
<?php
namespace App\Controllers;
use App\Core\Controller;
use App\Models\Product;
use App\Models\Category;
use App\Models\Order;
use App\Models\User;
/**
* AdminController - контроллер админ-панели
*/
class AdminController extends Controller
{
private Product $productModel;
private Category $categoryModel;
private Order $orderModel;
private User $userModel;
public function __construct()
{
$this->productModel = new Product();
$this->categoryModel = new Category();
$this->orderModel = new Order();
$this->userModel = new User();
}
/**
* Дашборд
*/
public function dashboard(): void
{
$this->requireAdmin();
$stats = [
'total_products' => $this->productModel->count(),
'active_products' => $this->productModel->count(['is_available' => true]),
'total_orders' => $this->orderModel->count(),
'total_users' => $this->userModel->count(),
'revenue' => $this->orderModel->getStats()['revenue']
];
$this->view('admin/dashboard', [
'stats' => $stats,
'user' => $this->getCurrentUser()
], 'admin');
}
// ========== Товары ==========
/**
* Список товаров
*/
public function products(): void
{
$this->requireAdmin();
$showAll = $this->getQuery('show_all') === '1';
$products = $this->productModel->getAllForAdmin($showAll);
$this->view('admin/products/index', [
'products' => $products,
'showAll' => $showAll,
'message' => $this->getQuery('message'),
'error' => $this->getQuery('error'),
'user' => $this->getCurrentUser()
], 'admin');
}
/**
* Форма добавления товара
*/
public function addProduct(): void
{
$this->requireAdmin();
$categories = $this->categoryModel->getActive();
$this->view('admin/products/form', [
'product' => null,
'categories' => $categories,
'action' => 'add',
'user' => $this->getCurrentUser()
], 'admin');
}
/**
* Сохранение нового товара
*/
public function storeProduct(): void
{
$this->requireAdmin();
$data = [
'name' => $this->getPost('name'),
'category_id' => (int) $this->getPost('category_id'),
'description' => $this->getPost('description'),
'price' => (float) $this->getPost('price'),
'old_price' => $this->getPost('old_price') ? (float) $this->getPost('old_price') : null,
'sku' => $this->getPost('sku'),
'stock_quantity' => (int) $this->getPost('stock_quantity', 0),
'is_available' => $this->getPost('is_available') ? true : false,
'is_featured' => $this->getPost('is_featured') ? true : false,
'image_url' => $this->getPost('image_url'),
'color' => $this->getPost('color'),
'material' => $this->getPost('material'),
'card_size' => $this->getPost('card_size', 'small')
];
try {
$this->productModel->createProduct($data);
$this->redirect('/admin/products?message=' . urlencode('Товар успешно добавлен'));
} catch (\Exception $e) {
$this->redirect('/admin/products/add?error=' . urlencode($e->getMessage()));
}
}
/**
* Форма редактирования товара
*/
public function editProduct(int $id): void
{
$this->requireAdmin();
$product = $this->productModel->find($id);
if (!$product) {
$this->redirect('/admin/products?error=' . urlencode('Товар не найден'));
return;
}
$categories = $this->categoryModel->getActive();
$this->view('admin/products/form', [
'product' => $product,
'categories' => $categories,
'action' => 'edit',
'user' => $this->getCurrentUser()
], 'admin');
}
/**
* Обновление товара
*/
public function updateProduct(int $id): void
{
$this->requireAdmin();
$data = [
'name' => $this->getPost('name'),
'category_id' => (int) $this->getPost('category_id'),
'description' => $this->getPost('description'),
'price' => (float) $this->getPost('price'),
'old_price' => $this->getPost('old_price') ? (float) $this->getPost('old_price') : null,
'stock_quantity' => (int) $this->getPost('stock_quantity', 0),
'is_available' => $this->getPost('is_available') ? true : false,
'image_url' => $this->getPost('image_url'),
'color' => $this->getPost('color'),
'material' => $this->getPost('material')
];
try {
$this->productModel->updateProduct($id, $data);
$this->redirect('/admin/products?message=' . urlencode('Товар обновлен'));
} catch (\Exception $e) {
$this->redirect('/admin/products/edit/' . $id . '?error=' . urlencode($e->getMessage()));
}
}
/**
* Удаление товара (делаем недоступным)
*/
public function deleteProduct(int $id): void
{
$this->requireAdmin();
$this->productModel->update($id, ['is_available' => false]);
$this->redirect('/admin/products?message=' . urlencode('Товар скрыт'));
}
// ========== Категории ==========
/**
* Список категорий
*/
public function categories(): void
{
$this->requireAdmin();
$categories = $this->categoryModel->getAllWithProductCount();
$this->view('admin/categories/index', [
'categories' => $categories,
'message' => $this->getQuery('message'),
'error' => $this->getQuery('error'),
'user' => $this->getCurrentUser()
], 'admin');
}
/**
* Форма добавления категории
*/
public function addCategory(): void
{
$this->requireAdmin();
$parentCategories = $this->categoryModel->getParent();
$this->view('admin/categories/form', [
'category' => null,
'parentCategories' => $parentCategories,
'action' => 'add',
'user' => $this->getCurrentUser()
], 'admin');
}
/**
* Сохранение категории
*/
public function storeCategory(): void
{
$this->requireAdmin();
$data = [
'name' => $this->getPost('name'),
'parent_id' => $this->getPost('parent_id') ? (int) $this->getPost('parent_id') : null,
'description' => $this->getPost('description'),
'sort_order' => (int) $this->getPost('sort_order', 0),
'is_active' => $this->getPost('is_active') ? true : false
];
try {
$this->categoryModel->createCategory($data);
$this->redirect('/admin/categories?message=' . urlencode('Категория добавлена'));
} catch (\Exception $e) {
$this->redirect('/admin/categories/add?error=' . urlencode($e->getMessage()));
}
}
/**
* Форма редактирования категории
*/
public function editCategory(int $id): void
{
$this->requireAdmin();
$category = $this->categoryModel->find($id);
if (!$category) {
$this->redirect('/admin/categories?error=' . urlencode('Категория не найдена'));
return;
}
$parentCategories = $this->categoryModel->getParent();
$this->view('admin/categories/form', [
'category' => $category,
'parentCategories' => $parentCategories,
'action' => 'edit',
'user' => $this->getCurrentUser()
], 'admin');
}
/**
* Обновление категории
*/
public function updateCategory(int $id): void
{
$this->requireAdmin();
$data = [
'name' => $this->getPost('name'),
'parent_id' => $this->getPost('parent_id') ? (int) $this->getPost('parent_id') : null,
'description' => $this->getPost('description'),
'sort_order' => (int) $this->getPost('sort_order', 0),
'is_active' => $this->getPost('is_active') ? true : false
];
try {
$this->categoryModel->updateCategory($id, $data);
$this->redirect('/admin/categories?message=' . urlencode('Категория обновлена'));
} catch (\Exception $e) {
$this->redirect('/admin/categories/edit/' . $id . '?error=' . urlencode($e->getMessage()));
}
}
/**
* Удаление категории
*/
public function deleteCategory(int $id): void
{
$this->requireAdmin();
$result = $this->categoryModel->safeDelete($id);
if ($result['deleted']) {
$this->redirect('/admin/categories?message=' . urlencode('Категория удалена'));
} else {
$msg = $result['reason'] === 'has_products'
? 'Категория скрыта (содержит товары)'
: 'Категория скрыта (имеет дочерние категории)';
$this->redirect('/admin/categories?message=' . urlencode($msg));
}
}
// ========== Заказы ==========
/**
* Список заказов
*/
public function orders(): void
{
$this->requireAdmin();
$orders = $this->orderModel->getAllForAdmin();
$this->view('admin/orders/index', [
'orders' => $orders,
'user' => $this->getCurrentUser()
], 'admin');
}
/**
* Детали заказа
*/
public function orderDetails(int $id): void
{
$this->requireAdmin();
$order = $this->orderModel->getWithDetails($id);
if (!$order) {
$this->redirect('/admin/orders?error=' . urlencode('Заказ не найден'));
return;
}
$this->view('admin/orders/details', [
'order' => $order,
'user' => $this->getCurrentUser()
], 'admin');
}
/**
* Обновление статуса заказа
*/
public function updateOrderStatus(int $id): void
{
$this->requireAdmin();
$status = $this->getPost('status');
$this->orderModel->updateStatus($id, $status);
$this->redirect('/admin/orders/' . $id);
}
// ========== Пользователи ==========
/**
* Список пользователей
*/
public function users(): void
{
$this->requireAdmin();
$users = $this->userModel->getAllPaginated();
$this->view('admin/users/index', [
'users' => $users,
'user' => $this->getCurrentUser()
], 'admin');
}
}

View File

@@ -0,0 +1,217 @@
<?php
namespace App\Controllers;
use App\Core\Controller;
use App\Models\User;
/**
* AuthController - контроллер авторизации
*/
class AuthController extends Controller
{
private User $userModel;
public function __construct()
{
$this->userModel = new User();
}
/**
* Форма входа
*/
public function loginForm(): void
{
if ($this->isAuthenticated()) {
$this->redirect('/catalog');
}
$redirect = $this->getQuery('redirect', '/catalog');
$this->view('auth/login', [
'redirect' => $redirect,
'error' => $this->getFlash('error'),
'success' => $this->getFlash('success')
]);
}
/**
* Обработка входа
*/
public function login(): void
{
$email = $this->getPost('email', '');
$password = $this->getPost('password', '');
$redirect = $this->getPost('redirect', '/catalog');
if (empty($email) || empty($password)) {
$this->json([
'success' => false,
'message' => 'Заполните все поля'
]);
return;
}
$user = $this->userModel->authenticate($email, $password);
if (!$user) {
$this->json([
'success' => false,
'message' => 'Неверный email или пароль'
]);
return;
}
// Устанавливаем сессию
$this->setSession($user);
$this->json([
'success' => true,
'redirect' => $redirect
]);
}
/**
* Форма регистрации
*/
public function registerForm(): void
{
if ($this->isAuthenticated()) {
$this->redirect('/catalog');
}
$this->view('auth/register', [
'errors' => $_SESSION['registration_errors'] ?? [],
'old' => $_SESSION['old_data'] ?? [],
'success' => $_SESSION['registration_success'] ?? null
]);
// Очищаем flash данные
unset($_SESSION['registration_errors']);
unset($_SESSION['old_data']);
unset($_SESSION['registration_success']);
}
/**
* Обработка регистрации
*/
public function register(): void
{
$errors = [];
$fullName = trim($this->getPost('fio', ''));
$city = trim($this->getPost('city', ''));
$email = trim($this->getPost('email', ''));
$phone = trim($this->getPost('phone', ''));
$password = $this->getPost('password', '');
$confirmPassword = $this->getPost('confirm-password', '');
$privacy = $this->getPost('privacy');
// Валидация
if (empty($fullName) || strlen($fullName) < 3) {
$errors[] = 'ФИО должно содержать минимум 3 символа';
}
if (empty($city) || strlen($city) < 2) {
$errors[] = 'Введите корректное название города';
}
if (empty($email) || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
$errors[] = 'Введите корректный email адрес';
}
if (empty($phone)) {
$errors[] = 'Введите номер телефона';
}
if (empty($password) || strlen($password) < 6) {
$errors[] = 'Пароль должен содержать минимум 6 символов';
}
if ($password !== $confirmPassword) {
$errors[] = 'Пароли не совпадают';
}
if (!$privacy) {
$errors[] = 'Необходимо согласие с условиями обработки персональных данных';
}
// Проверяем существование email
if (empty($errors) && $this->userModel->emailExists($email)) {
$errors[] = 'Пользователь с таким email уже существует';
}
if (!empty($errors)) {
$_SESSION['registration_errors'] = $errors;
$_SESSION['old_data'] = [
'fio' => $fullName,
'city' => $city,
'email' => $email,
'phone' => $phone
];
$this->redirect('/register');
return;
}
// Создаем пользователя
try {
$userId = $this->userModel->register([
'email' => $email,
'password' => $password,
'full_name' => $fullName,
'phone' => $phone,
'city' => $city
]);
if (!$userId) {
throw new \Exception('Ошибка при создании пользователя');
}
// Получаем созданного пользователя
$user = $this->userModel->find($userId);
// Устанавливаем сессию
$this->setSession($user);
$_SESSION['registration_success'] = 'Регистрация прошла успешно!';
$this->redirect('/catalog');
} catch (\Exception $e) {
$_SESSION['registration_errors'] = [$e->getMessage()];
$_SESSION['old_data'] = [
'fio' => $fullName,
'city' => $city,
'email' => $email,
'phone' => $phone
];
$this->redirect('/register');
}
}
/**
* Выход из системы
*/
public function logout(): void
{
session_destroy();
session_start();
$this->redirect('/');
}
/**
* Установить сессию пользователя
*/
private function setSession(array $user): void
{
$_SESSION['user_id'] = $user['user_id'];
$_SESSION['user_email'] = $user['email'];
$_SESSION['full_name'] = $user['full_name'];
$_SESSION['user_phone'] = $user['phone'] ?? '';
$_SESSION['user_city'] = $user['city'] ?? '';
$_SESSION['isLoggedIn'] = true;
$_SESSION['isAdmin'] = (bool) $user['is_admin'];
$_SESSION['login_time'] = time();
}
}

View File

@@ -0,0 +1,218 @@
<?php
namespace App\Controllers;
use App\Core\Controller;
use App\Models\Cart;
use App\Models\Product;
/**
* CartController - контроллер корзины
*/
class CartController extends Controller
{
private Cart $cartModel;
private Product $productModel;
public function __construct()
{
$this->cartModel = new Cart();
$this->productModel = new Product();
}
/**
* Страница корзины
*/
public function index(): void
{
$this->requireAuth();
$user = $this->getCurrentUser();
$cartItems = $this->cartModel->getUserCart($user['id']);
$totals = $this->cartModel->getCartTotal($user['id']);
$this->view('cart/checkout', [
'user' => $user,
'cartItems' => $cartItems,
'totalQuantity' => $totals['quantity'],
'totalAmount' => $totals['amount']
]);
}
/**
* Добавить товар в корзину
*/
public function add(): void
{
if (!$this->isAuthenticated()) {
$this->json([
'success' => false,
'message' => 'Требуется авторизация'
]);
return;
}
$productId = (int) $this->getPost('product_id', 0);
$quantity = (int) $this->getPost('quantity', 1);
$userId = $this->getCurrentUser()['id'];
if ($productId <= 0) {
$this->json([
'success' => false,
'message' => 'Неверный товар'
]);
return;
}
// Проверяем наличие товара
$product = $this->productModel->find($productId);
if (!$product || !$product['is_available']) {
$this->json([
'success' => false,
'message' => 'Товар не найден'
]);
return;
}
// Проверяем количество на складе
$cartItem = $this->cartModel->getItem($userId, $productId);
$currentQty = $cartItem ? $cartItem['quantity'] : 0;
$newQty = $currentQty + $quantity;
if ($newQty > $product['stock_quantity']) {
$this->json([
'success' => false,
'message' => 'Недостаточно товара на складе'
]);
return;
}
// Добавляем в корзину
$result = $this->cartModel->addItem($userId, $productId, $quantity);
if ($result) {
$cartCount = $this->cartModel->getCount($userId);
$this->json([
'success' => true,
'cart_count' => $cartCount,
'message' => 'Товар добавлен в корзину'
]);
} else {
$this->json([
'success' => false,
'message' => 'Ошибка при добавлении в корзину'
]);
}
}
/**
* Обновить количество товара
*/
public function update(): void
{
if (!$this->isAuthenticated()) {
$this->json([
'success' => false,
'message' => 'Требуется авторизация'
]);
return;
}
$productId = (int) $this->getPost('product_id', 0);
$quantity = (int) $this->getPost('quantity', 1);
$userId = $this->getCurrentUser()['id'];
if ($quantity <= 0) {
// Если количество 0 или меньше - удаляем
$this->cartModel->removeItem($userId, $productId);
$cartCount = $this->cartModel->getCount($userId);
$this->json([
'success' => true,
'cart_count' => $cartCount
]);
return;
}
// Проверяем наличие на складе
$product = $this->productModel->find($productId);
if (!$product || $quantity > $product['stock_quantity']) {
$this->json([
'success' => false,
'message' => 'Недостаточно товара на складе'
]);
return;
}
$result = $this->cartModel->updateQuantity($userId, $productId, $quantity);
if ($result) {
$totals = $this->cartModel->getCartTotal($userId);
$this->json([
'success' => true,
'cart_count' => $totals['quantity'],
'total_amount' => $totals['amount']
]);
} else {
$this->json([
'success' => false,
'message' => 'Ошибка при обновлении'
]);
}
}
/**
* Удалить товар из корзины
*/
public function remove(): void
{
if (!$this->isAuthenticated()) {
$this->json([
'success' => false,
'message' => 'Требуется авторизация'
]);
return;
}
$productId = (int) $this->getPost('product_id', 0);
$userId = $this->getCurrentUser()['id'];
$result = $this->cartModel->removeItem($userId, $productId);
if ($result) {
$cartCount = $this->cartModel->getCount($userId);
$this->json([
'success' => true,
'cart_count' => $cartCount
]);
} else {
$this->json([
'success' => false,
'message' => 'Ошибка при удалении'
]);
}
}
/**
* Получить количество товаров в корзине
*/
public function count(): void
{
if (!$this->isAuthenticated()) {
$this->json([
'success' => true,
'cart_count' => 0
]);
return;
}
$userId = $this->getCurrentUser()['id'];
$count = $this->cartModel->getCount($userId);
$this->json([
'success' => true,
'cart_count' => $count
]);
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace App\Controllers;
use App\Core\Controller;
/**
* HomeController - контроллер главной страницы
*/
class HomeController extends Controller
{
/**
* Главная страница
*/
public function index(): void
{
$user = $this->getCurrentUser();
$this->view('home/index', [
'user' => $user,
'isLoggedIn' => $this->isAuthenticated(),
'isAdmin' => $this->isAdmin()
]);
}
}

View File

@@ -0,0 +1,125 @@
<?php
namespace App\Controllers;
use App\Core\Controller;
use App\Models\Order;
use App\Models\Cart;
/**
* OrderController - контроллер заказов
*/
class OrderController extends Controller
{
private Order $orderModel;
private Cart $cartModel;
public function __construct()
{
$this->orderModel = new Order();
$this->cartModel = new Cart();
}
/**
* Страница оформления заказа (корзина)
*/
public function checkout(): void
{
$this->requireAuth();
$user = $this->getCurrentUser();
$cartItems = $this->cartModel->getUserCart($user['id']);
$totals = $this->cartModel->getCartTotal($user['id']);
$this->view('cart/checkout', [
'user' => $user,
'cartItems' => $cartItems,
'totalQuantity' => $totals['quantity'],
'totalAmount' => $totals['amount']
]);
}
/**
* Создание заказа
*/
public function create(): void
{
if (!$this->isAuthenticated()) {
$this->json([
'success' => false,
'message' => 'Требуется авторизация'
]);
return;
}
$user = $this->getCurrentUser();
$cartItems = $this->cartModel->getUserCart($user['id']);
if (empty($cartItems)) {
$this->json([
'success' => false,
'message' => 'Корзина пуста'
]);
return;
}
// Получаем данные заказа
$orderData = [
'customer_name' => $this->getPost('full_name', $user['full_name']),
'customer_email' => $this->getPost('email', $user['email']),
'customer_phone' => $this->getPost('phone', $user['phone']),
'delivery_address' => $this->getPost('address', ''),
'delivery_region' => $this->getPost('region', ''),
'postal_code' => $this->getPost('postal_code', ''),
'delivery_method' => $this->getPost('delivery', 'courier'),
'payment_method' => $this->getPost('payment', 'card'),
'promo_code' => $this->getPost('promo_code', ''),
'discount' => (float) $this->getPost('discount', 0),
'delivery_price' => (float) $this->getPost('delivery_price', 2000),
'notes' => $this->getPost('notes', '')
];
// Валидация
if (empty($orderData['customer_name'])) {
$this->json([
'success' => false,
'message' => 'Укажите ФИО'
]);
return;
}
if (empty($orderData['customer_phone'])) {
$this->json([
'success' => false,
'message' => 'Укажите телефон'
]);
return;
}
if (empty($orderData['delivery_address'])) {
$this->json([
'success' => false,
'message' => 'Укажите адрес доставки'
]);
return;
}
try {
$result = $this->orderModel->createFromCart($user['id'], $cartItems, $orderData);
$this->json([
'success' => true,
'order_id' => $result['order_id'],
'order_number' => $result['order_number'],
'message' => 'Заказ успешно оформлен'
]);
} catch (\Exception $e) {
$this->json([
'success' => false,
'message' => $e->getMessage()
]);
}
}
}

View File

@@ -0,0 +1,45 @@
<?php
namespace App\Controllers;
use App\Core\Controller;
/**
* PageController - контроллер статических страниц
*/
class PageController extends Controller
{
/**
* Страница услуг
*/
public function services(): void
{
$this->view('pages/services', [
'user' => $this->getCurrentUser(),
'isLoggedIn' => $this->isAuthenticated()
]);
}
/**
* Страница доставки и оплаты
*/
public function delivery(): void
{
$this->view('pages/delivery', [
'user' => $this->getCurrentUser(),
'isLoggedIn' => $this->isAuthenticated()
]);
}
/**
* Страница гарантии
*/
public function warranty(): void
{
$this->view('pages/warranty', [
'user' => $this->getCurrentUser(),
'isLoggedIn' => $this->isAuthenticated()
]);
}
}

View File

@@ -0,0 +1,102 @@
<?php
namespace App\Controllers;
use App\Core\Controller;
use App\Models\Product;
use App\Models\Category;
/**
* ProductController - контроллер товаров и каталога
*/
class ProductController extends Controller
{
private Product $productModel;
private Category $categoryModel;
public function __construct()
{
$this->productModel = new Product();
$this->categoryModel = new Category();
}
/**
* Каталог товаров
*/
public function catalog(): void
{
$this->requireAuth();
$user = $this->getCurrentUser();
$isAdmin = $this->isAdmin();
// Получаем параметры фильтрации
$filters = [
'category_id' => (int) $this->getQuery('category', 0),
'search' => $this->getQuery('search', ''),
'min_price' => (int) $this->getQuery('min_price', 0),
'max_price' => (int) $this->getQuery('max_price', 1000000),
'colors' => $this->getQuery('colors', []),
'materials' => $this->getQuery('materials', [])
];
$showAll = $isAdmin && $this->getQuery('show_all') === '1';
// Получаем данные
$categories = $this->categoryModel->getActive();
$products = $showAll
? $this->productModel->getAllForAdmin(true)
: $this->productModel->getAvailable($filters);
$availableColors = $this->productModel->getAvailableColors();
$availableMaterials = $this->productModel->getAvailableMaterials();
// Подкатегории для выбранной категории
$subcategories = [];
if ($filters['category_id'] > 0) {
$subcategories = $this->categoryModel->getChildren($filters['category_id']);
}
$this->view('products/catalog', [
'user' => $user,
'isAdmin' => $isAdmin,
'categories' => $categories,
'subcategories' => $subcategories,
'products' => $products,
'filters' => $filters,
'showAll' => $showAll,
'availableColors' => $availableColors,
'availableMaterials' => $availableMaterials,
'success' => $this->getQuery('success'),
'error' => $this->getQuery('error')
]);
}
/**
* Страница товара
*/
public function show(int $id): void
{
$this->requireAuth();
$product = $this->productModel->findWithCategory($id);
if (!$product || (!$product['is_available'] && !$this->isAdmin())) {
$this->redirect('/catalog?error=product_not_found');
return;
}
$similarProducts = $this->productModel->getSimilar(
$id,
$product['category_id']
);
$this->view('products/show', [
'product' => $product,
'similarProducts' => $similarProducts,
'user' => $this->getCurrentUser(),
'isAdmin' => $this->isAdmin()
]);
}
}