Fix LESS import error and refactor project structure

This commit is contained in:
kirill.khorkov
2026-01-03 18:59:56 +03:00
parent 1bb0fc02e6
commit 4a8d4f8c3f
201 changed files with 891 additions and 14311 deletions

View File

@@ -2,66 +2,68 @@
namespace App\Core;
/**
* App - главный класс приложения
*/
class App
{
private Router $router;
private static ?App $instance = null;
private array $config = [];
public function __construct()
{
self::$instance = $this;
// Регистрируем автозагрузчик сразу
$this->registerAutoloader();
$this->router = new Router();
}
/**
* Получить экземпляр приложения
*/
public static function getInstance(): ?self
{
return self::$instance;
}
/**
* Получить роутер
*/
public function getRouter(): Router
{
return $this->router;
}
/**
* Инициализация приложения
*/
public function getConfig(string $key = null)
{
if ($key === null) {
return $this->config;
}
return $this->config[$key] ?? null;
}
public function init(): self
{
// Запускаем сессию
$this->loadConfig();
date_default_timezone_set($this->config['timezone'] ?? 'Europe/Moscow');
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
// Настраиваем обработку ошибок
$this->setupErrorHandling();
// Загружаем маршруты
$this->loadRoutes();
return $this;
}
/**
* Регистрация автозагрузчика классов
*/
private function loadConfig(): void
{
$configPath = $this->getBasePath() . '/config/app.php';
if (file_exists($configPath)) {
$this->config = require $configPath;
}
}
public function getBasePath(): string
{
return defined('ROOT_PATH') ? ROOT_PATH : dirname(__DIR__, 2);
}
private function registerAutoloader(): void
{
spl_autoload_register(function ($class) {
// Преобразуем namespace в путь к файлу
$prefix = 'App\\';
$baseDir = dirname(__DIR__) . '/';
@@ -79,14 +81,9 @@ class App
});
}
/**
* Настройка обработки ошибок
*/
private function setupErrorHandling(): void
{
$config = require dirname(__DIR__, 2) . '/config/app.php';
if ($config['debug'] ?? false) {
if ($this->config['debug'] ?? false) {
error_reporting(E_ALL);
ini_set('display_errors', '1');
} else {
@@ -103,16 +100,11 @@ class App
});
}
/**
* Обработка исключений
*/
private function handleException(\Throwable $e): void
{
$config = require dirname(__DIR__, 2) . '/config/app.php';
http_response_code(500);
if ($config['debug'] ?? false) {
if ($this->config['debug'] ?? false) {
echo "<h1>Ошибка приложения</h1>";
echo "<p><strong>Сообщение:</strong> " . htmlspecialchars($e->getMessage()) . "</p>";
echo "<p><strong>Файл:</strong> " . htmlspecialchars($e->getFile()) . ":" . $e->getLine() . "</p>";
@@ -122,12 +114,9 @@ class App
}
}
/**
* Загрузка маршрутов
*/
private function loadRoutes(): void
{
$routesFile = dirname(__DIR__, 2) . '/config/routes.php';
$routesFile = $this->getBasePath() . '/config/routes.php';
if (file_exists($routesFile)) {
$router = $this->router;
@@ -135,21 +124,17 @@ class App
}
}
/**
* Запуск приложения
*/
public function run(): void
{
$uri = $_SERVER['REQUEST_URI'] ?? '/';
$method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
// Удаляем базовый путь, если он есть
$basePath = '/cite_practica';
if (strpos($uri, $basePath) === 0) {
$basePath = $this->config['base_path'] ?? '';
if (!empty($basePath) && strpos($uri, $basePath) === 0) {
$uri = substr($uri, strlen($basePath));
}
// Если URI пустой, делаем его корневым
if (empty($uri) || $uri === false) {
$uri = '/';
}
@@ -161,4 +146,3 @@ class App
}
}
}

View File

@@ -2,35 +2,20 @@
namespace App\Core;
/**
* Controller - базовый класс контроллера
*/
abstract class Controller
{
/**
* Данные для передачи в представление
*/
protected array $data = [];
/**
* Отрендерить представление
*/
protected function view(string $view, array $data = [], string $layout = 'main'): void
{
echo View::render($view, $data, $layout);
}
/**
* Отрендерить представление без layout
*/
protected function viewPartial(string $view, array $data = []): void
{
echo View::render($view, $data, null);
}
/**
* Вернуть JSON ответ
*/
protected function json(array $data, int $statusCode = 200): void
{
http_response_code($statusCode);
@@ -39,18 +24,12 @@ abstract class Controller
exit;
}
/**
* Редирект на другой URL
*/
protected function redirect(string $url): void
{
header("Location: {$url}");
exit;
}
/**
* Получить текущего пользователя из сессии
*/
protected function getCurrentUser(): ?array
{
if (!isset($_SESSION['isLoggedIn']) || $_SESSION['isLoggedIn'] !== true) {
@@ -68,25 +47,16 @@ abstract class Controller
];
}
/**
* Проверить, авторизован ли пользователь
*/
protected function isAuthenticated(): bool
{
return isset($_SESSION['isLoggedIn']) && $_SESSION['isLoggedIn'] === true;
}
/**
* Проверить, является ли пользователь администратором
*/
protected function isAdmin(): bool
{
return $this->isAuthenticated() && isset($_SESSION['isAdmin']) && $_SESSION['isAdmin'] === true;
}
/**
* Требовать авторизацию
*/
protected function requireAuth(): void
{
if (!$this->isAuthenticated()) {
@@ -95,9 +65,6 @@ abstract class Controller
}
}
/**
* Требовать права администратора
*/
protected function requireAdmin(): void
{
if (!$this->isAdmin()) {
@@ -105,9 +72,6 @@ abstract class Controller
}
}
/**
* Получить POST данные
*/
protected function getPost(?string $key = null, $default = null)
{
if ($key === null) {
@@ -116,9 +80,6 @@ abstract class Controller
return $_POST[$key] ?? $default;
}
/**
* Получить GET данные
*/
protected function getQuery(?string $key = null, $default = null)
{
if ($key === null) {
@@ -127,17 +88,11 @@ abstract class Controller
return $_GET[$key] ?? $default;
}
/**
* Установить flash-сообщение
*/
protected function setFlash(string $type, string $message): void
{
$_SESSION['flash'][$type] = $message;
}
/**
* Получить flash-сообщение
*/
protected function getFlash(string $type): ?string
{
$message = $_SESSION['flash'][$type] ?? null;
@@ -145,4 +100,3 @@ abstract class Controller
return $message;
}
}

View File

@@ -2,9 +2,6 @@
namespace App\Core;
/**
* Database - Singleton класс для подключения к PostgreSQL
*/
class Database
{
private static ?Database $instance = null;
@@ -38,9 +35,6 @@ class Database
return $this->connection;
}
/**
* Выполнить SELECT запрос
*/
public function query(string $sql, array $params = []): array
{
$stmt = $this->connection->prepare($sql);
@@ -48,9 +42,6 @@ class Database
return $stmt->fetchAll();
}
/**
* Выполнить SELECT запрос и получить одну запись
*/
public function queryOne(string $sql, array $params = []): ?array
{
$stmt = $this->connection->prepare($sql);
@@ -59,52 +50,36 @@ class Database
return $result ?: null;
}
/**
* Выполнить INSERT/UPDATE/DELETE запрос
*/
public function execute(string $sql, array $params = []): bool
{
$stmt = $this->connection->prepare($sql);
return $stmt->execute($params);
}
/**
* Получить ID последней вставленной записи
*/
public function lastInsertId(): string
{
return $this->connection->lastInsertId();
}
/**
* Начать транзакцию
*/
public function beginTransaction(): bool
{
return $this->connection->beginTransaction();
}
/**
* Подтвердить транзакцию
*/
public function commit(): bool
{
return $this->connection->commit();
}
/**
* Откатить транзакцию
*/
public function rollBack(): bool
{
return $this->connection->rollBack();
}
// Запрещаем клонирование и десериализацию
private function __clone() {}
public function __wakeup()
{
throw new \Exception("Десериализация Singleton запрещена");
}
}

View File

@@ -2,9 +2,6 @@
namespace App\Core;
/**
* Model - базовый класс модели
*/
abstract class Model
{
protected Database $db;
@@ -16,18 +13,12 @@ abstract class Model
$this->db = Database::getInstance();
}
/**
* Найти запись по первичному ключу
*/
public function find(int $id): ?array
{
$sql = "SELECT * FROM {$this->table} WHERE {$this->primaryKey} = ?";
return $this->db->queryOne($sql, [$id]);
}
/**
* Получить все записи
*/
public function all(?string $orderBy = null): array
{
$sql = "SELECT * FROM {$this->table}";
@@ -37,9 +28,6 @@ abstract class Model
return $this->db->query($sql);
}
/**
* Найти записи по условию
*/
public function where(array $conditions, ?string $orderBy = null): array
{
$where = [];
@@ -59,9 +47,6 @@ abstract class Model
return $this->db->query($sql, $params);
}
/**
* Найти одну запись по условию
*/
public function findWhere(array $conditions): ?array
{
$where = [];
@@ -76,9 +61,6 @@ abstract class Model
return $this->db->queryOne($sql, $params);
}
/**
* Создать новую запись
*/
public function create(array $data): ?int
{
$columns = array_keys($data);
@@ -98,9 +80,6 @@ abstract class Model
return (int) $stmt->fetchColumn();
}
/**
* Обновить запись
*/
public function update(int $id, array $data): bool
{
$set = [];
@@ -122,18 +101,12 @@ abstract class Model
return $this->db->execute($sql, $params);
}
/**
* Удалить запись
*/
public function delete(int $id): bool
{
$sql = "DELETE FROM {$this->table} WHERE {$this->primaryKey} = ?";
return $this->db->execute($sql, [$id]);
}
/**
* Подсчитать количество записей
*/
public function count(array $conditions = []): int
{
$sql = "SELECT COUNT(*) FROM {$this->table}";
@@ -153,28 +126,18 @@ abstract class Model
return (int) $stmt->fetchColumn();
}
/**
* Выполнить произвольный SQL запрос
*/
protected function query(string $sql, array $params = []): array
{
return $this->db->query($sql, $params);
}
/**
* Выполнить произвольный SQL запрос и получить одну запись
*/
protected function queryOne(string $sql, array $params = []): ?array
{
return $this->db->queryOne($sql, $params);
}
/**
* Выполнить произвольный SQL запрос (INSERT/UPDATE/DELETE)
*/
protected function execute(string $sql, array $params = []): bool
{
return $this->db->execute($sql, $params);
}
}

View File

@@ -2,17 +2,11 @@
namespace App\Core;
/**
* Router - маршрутизатор запросов
*/
class Router
{
private array $routes = [];
private array $params = [];
/**
* Добавить маршрут
*/
public function add(string $method, string $route, string $controller, string $action): self
{
$this->routes[] = [
@@ -24,25 +18,16 @@ class Router
return $this;
}
/**
* GET маршрут
*/
public function get(string $route, string $controller, string $action): self
{
return $this->add('GET', $route, $controller, $action);
}
/**
* POST маршрут
*/
public function post(string $route, string $controller, string $action): self
{
return $this->add('POST', $route, $controller, $action);
}
/**
* Найти маршрут по URL и методу
*/
public function match(string $url, string $method): ?array
{
$method = strtoupper($method);
@@ -57,7 +42,6 @@ class Router
$pattern = $this->convertRouteToRegex($route['route']);
if (preg_match($pattern, $url, $matches)) {
// Извлекаем параметры из URL
$this->params = $this->extractParams($route['route'], $matches);
return [
@@ -71,27 +55,16 @@ class Router
return null;
}
/**
* Преобразовать маршрут в регулярное выражение
*/
private function convertRouteToRegex(string $route): string
{
$route = trim($route, '/');
// Заменяем {param} на regex группу
$pattern = preg_replace('/\{([a-zA-Z_]+)\}/', '([^/]+)', $route);
return '#^' . $pattern . '$#';
}
/**
* Извлечь параметры из совпадений
*/
private function extractParams(string $route, array $matches): array
{
$params = [];
// Находим все {param} в маршруте
preg_match_all('/\{([a-zA-Z_]+)\}/', $route, $paramNames);
foreach ($paramNames[1] as $index => $name) {
@@ -103,9 +76,6 @@ class Router
return $params;
}
/**
* Удалить query string из URL
*/
private function removeQueryString(string $url): string
{
if ($pos = strpos($url, '?')) {
@@ -114,17 +84,11 @@ class Router
return $url;
}
/**
* Получить параметры маршрута
*/
public function getParams(): array
{
return $this->params;
}
/**
* Диспетчеризация запроса
*/
public function dispatch(string $url, string $method): void
{
$match = $this->match($url, $method);
@@ -148,8 +112,6 @@ class Router
throw new \Exception("Метод {$action} не найден в контроллере {$controllerClass}");
}
// Вызываем метод контроллера с параметрами
call_user_func_array([$controller, $action], $match['params']);
}
}

View File

@@ -2,24 +2,15 @@
namespace App\Core;
/**
* View - класс для рендеринга представлений
*/
class View
{
private static string $viewsPath = '';
/**
* Установить путь к директории представлений
*/
public static function setViewsPath(string $path): void
{
self::$viewsPath = rtrim($path, '/');
}
/**
* Получить путь к директории представлений
*/
public static function getViewsPath(): string
{
if (empty(self::$viewsPath)) {
@@ -28,9 +19,6 @@ class View
return self::$viewsPath;
}
/**
* Отрендерить представление
*/
public static function render(string $view, array $data = [], ?string $layout = 'main'): string
{
$viewPath = self::getViewsPath() . '/' . str_replace('.', '/', $view) . '.php';
@@ -39,15 +27,12 @@ class View
throw new \Exception("Представление не найдено: {$viewPath}");
}
// Извлекаем данные в переменные
extract($data);
// Буферизируем вывод контента
ob_start();
require $viewPath;
$content = ob_get_clean();
// Если есть layout, оборачиваем контент
if ($layout !== null) {
$layoutPath = self::getViewsPath() . '/layouts/' . $layout . '.php';
@@ -63,9 +48,6 @@ class View
return $content;
}
/**
* Отрендерить partial (часть шаблона)
*/
public static function partial(string $partial, array $data = []): string
{
$partialPath = self::getViewsPath() . '/partials/' . $partial . '.php';
@@ -81,49 +63,31 @@ class View
return ob_get_clean();
}
/**
* Экранирование HTML
*/
public static function escape(string $value): string
{
return htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
}
/**
* Сокращенный алиас для escape
*/
public static function e(string $value): string
{
return self::escape($value);
}
/**
* Форматирование цены
*/
public static function formatPrice($price): string
{
return number_format((float)$price, 0, '', ' ') . ' ₽';
}
/**
* Форматирование даты
*/
public static function formatDate(string $date, string $format = 'd.m.Y'): string
{
return date($format, strtotime($date));
}
/**
* Форматирование даты и времени
*/
public static function formatDateTime(string $date, string $format = 'd.m.Y H:i'): string
{
return date($format, strtotime($date));
}
/**
* Получить flash-сообщения
*/
public static function getFlashMessages(): array
{
$messages = $_SESSION['flash'] ?? [];
@@ -131,25 +95,16 @@ class View
return $messages;
}
/**
* Проверить, авторизован ли пользователь
*/
public static function isAuthenticated(): bool
{
return isset($_SESSION['isLoggedIn']) && $_SESSION['isLoggedIn'] === true;
}
/**
* Проверить, является ли пользователь администратором
*/
public static function isAdmin(): bool
{
return self::isAuthenticated() && isset($_SESSION['isAdmin']) && $_SESSION['isAdmin'] === true;
}
/**
* Получить данные текущего пользователя
*/
public static function currentUser(): ?array
{
if (!self::isAuthenticated()) {
@@ -165,20 +120,13 @@ class View
];
}
/**
* Генерация URL
*/
public static function url(string $path): string
{
return '/' . ltrim($path, '/');
}
/**
* Генерация URL для ассетов
*/
public static function asset(string $path): string
{
return '/assets/' . ltrim($path, '/');
}
}