[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:
163
app/Core/App.php
Normal file
163
app/Core/App.php
Normal file
@@ -0,0 +1,163 @@
|
||||
<?php
|
||||
|
||||
namespace App\Core;
|
||||
|
||||
/**
|
||||
* App - главный класс приложения
|
||||
*/
|
||||
class App
|
||||
{
|
||||
private Router $router;
|
||||
private static ?App $instance = null;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
self::$instance = $this;
|
||||
$this->router = new Router();
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить экземпляр приложения
|
||||
*/
|
||||
public static function getInstance(): ?self
|
||||
{
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить роутер
|
||||
*/
|
||||
public function getRouter(): Router
|
||||
{
|
||||
return $this->router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Инициализация приложения
|
||||
*/
|
||||
public function init(): self
|
||||
{
|
||||
// Запускаем сессию
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
|
||||
// Регистрируем автозагрузчик
|
||||
$this->registerAutoloader();
|
||||
|
||||
// Настраиваем обработку ошибок
|
||||
$this->setupErrorHandling();
|
||||
|
||||
// Загружаем маршруты
|
||||
$this->loadRoutes();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Регистрация автозагрузчика классов
|
||||
*/
|
||||
private function registerAutoloader(): void
|
||||
{
|
||||
spl_autoload_register(function ($class) {
|
||||
// Преобразуем namespace в путь к файлу
|
||||
$prefix = 'App\\';
|
||||
$baseDir = dirname(__DIR__) . '/';
|
||||
|
||||
$len = strlen($prefix);
|
||||
if (strncmp($prefix, $class, $len) !== 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$relativeClass = substr($class, $len);
|
||||
$file = $baseDir . str_replace('\\', '/', $relativeClass) . '.php';
|
||||
|
||||
if (file_exists($file)) {
|
||||
require $file;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Настройка обработки ошибок
|
||||
*/
|
||||
private function setupErrorHandling(): void
|
||||
{
|
||||
$config = require dirname(__DIR__, 2) . '/config/app.php';
|
||||
|
||||
if ($config['debug'] ?? false) {
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', '1');
|
||||
} else {
|
||||
error_reporting(0);
|
||||
ini_set('display_errors', '0');
|
||||
}
|
||||
|
||||
set_exception_handler(function (\Throwable $e) {
|
||||
$this->handleException($e);
|
||||
});
|
||||
|
||||
set_error_handler(function ($severity, $message, $file, $line) {
|
||||
throw new \ErrorException($message, 0, $severity, $file, $line);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Обработка исключений
|
||||
*/
|
||||
private function handleException(\Throwable $e): void
|
||||
{
|
||||
$config = require dirname(__DIR__, 2) . '/config/app.php';
|
||||
|
||||
http_response_code(500);
|
||||
|
||||
if ($config['debug'] ?? false) {
|
||||
echo "<h1>Ошибка приложения</h1>";
|
||||
echo "<p><strong>Сообщение:</strong> " . htmlspecialchars($e->getMessage()) . "</p>";
|
||||
echo "<p><strong>Файл:</strong> " . htmlspecialchars($e->getFile()) . ":" . $e->getLine() . "</p>";
|
||||
echo "<pre>" . htmlspecialchars($e->getTraceAsString()) . "</pre>";
|
||||
} else {
|
||||
echo View::render('errors/500', [], 'main');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Загрузка маршрутов
|
||||
*/
|
||||
private function loadRoutes(): void
|
||||
{
|
||||
$routesFile = dirname(__DIR__, 2) . '/config/routes.php';
|
||||
|
||||
if (file_exists($routesFile)) {
|
||||
$router = $this->router;
|
||||
require $routesFile;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Запуск приложения
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
$uri = $_SERVER['REQUEST_URI'] ?? '/';
|
||||
$method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
|
||||
|
||||
// Удаляем базовый путь, если он есть
|
||||
$basePath = '/cite_practica';
|
||||
if (strpos($uri, $basePath) === 0) {
|
||||
$uri = substr($uri, strlen($basePath));
|
||||
}
|
||||
|
||||
// Если URI пустой, делаем его корневым
|
||||
if (empty($uri) || $uri === false) {
|
||||
$uri = '/';
|
||||
}
|
||||
|
||||
try {
|
||||
$this->router->dispatch($uri, $method);
|
||||
} catch (\Exception $e) {
|
||||
$this->handleException($e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
148
app/Core/Controller.php
Normal file
148
app/Core/Controller.php
Normal file
@@ -0,0 +1,148 @@
|
||||
<?php
|
||||
|
||||
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);
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode($data, JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Редирект на другой URL
|
||||
*/
|
||||
protected function redirect(string $url): void
|
||||
{
|
||||
header("Location: {$url}");
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить текущего пользователя из сессии
|
||||
*/
|
||||
protected function getCurrentUser(): ?array
|
||||
{
|
||||
if (!isset($_SESSION['isLoggedIn']) || $_SESSION['isLoggedIn'] !== true) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => $_SESSION['user_id'] ?? null,
|
||||
'email' => $_SESSION['user_email'] ?? '',
|
||||
'full_name' => $_SESSION['full_name'] ?? '',
|
||||
'phone' => $_SESSION['user_phone'] ?? '',
|
||||
'city' => $_SESSION['user_city'] ?? '',
|
||||
'is_admin' => $_SESSION['isAdmin'] ?? false,
|
||||
'login_time' => $_SESSION['login_time'] ?? null
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверить, авторизован ли пользователь
|
||||
*/
|
||||
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()) {
|
||||
$redirect = urlencode($_SERVER['REQUEST_URI']);
|
||||
$this->redirect("/login?redirect={$redirect}");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Требовать права администратора
|
||||
*/
|
||||
protected function requireAdmin(): void
|
||||
{
|
||||
if (!$this->isAdmin()) {
|
||||
$this->redirect('/login');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить POST данные
|
||||
*/
|
||||
protected function getPost(?string $key = null, $default = null)
|
||||
{
|
||||
if ($key === null) {
|
||||
return $_POST;
|
||||
}
|
||||
return $_POST[$key] ?? $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить GET данные
|
||||
*/
|
||||
protected function getQuery(?string $key = null, $default = null)
|
||||
{
|
||||
if ($key === null) {
|
||||
return $_GET;
|
||||
}
|
||||
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;
|
||||
unset($_SESSION['flash'][$type]);
|
||||
return $message;
|
||||
}
|
||||
}
|
||||
|
||||
110
app/Core/Database.php
Normal file
110
app/Core/Database.php
Normal file
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
|
||||
namespace App\Core;
|
||||
|
||||
/**
|
||||
* Database - Singleton класс для подключения к PostgreSQL
|
||||
*/
|
||||
class Database
|
||||
{
|
||||
private static ?Database $instance = null;
|
||||
private \PDO $connection;
|
||||
|
||||
private function __construct()
|
||||
{
|
||||
$config = require dirname(__DIR__, 2) . '/config/database.php';
|
||||
|
||||
try {
|
||||
$dsn = "pgsql:host={$config['host']};port={$config['port']};dbname={$config['database']}";
|
||||
$this->connection = new \PDO($dsn, $config['username'], $config['password']);
|
||||
$this->connection->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
|
||||
$this->connection->setAttribute(\PDO::ATTR_DEFAULT_FETCH_MODE, \PDO::FETCH_ASSOC);
|
||||
$this->connection->exec("SET NAMES 'utf8'");
|
||||
} catch (\PDOException $e) {
|
||||
throw new \Exception("Ошибка подключения к базе данных: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public static function getInstance(): self
|
||||
{
|
||||
if (self::$instance === null) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public function getConnection(): \PDO
|
||||
{
|
||||
return $this->connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Выполнить SELECT запрос
|
||||
*/
|
||||
public function query(string $sql, array $params = []): array
|
||||
{
|
||||
$stmt = $this->connection->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
return $stmt->fetchAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Выполнить SELECT запрос и получить одну запись
|
||||
*/
|
||||
public function queryOne(string $sql, array $params = []): ?array
|
||||
{
|
||||
$stmt = $this->connection->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
$result = $stmt->fetch();
|
||||
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 запрещена");
|
||||
}
|
||||
}
|
||||
|
||||
180
app/Core/Model.php
Normal file
180
app/Core/Model.php
Normal file
@@ -0,0 +1,180 @@
|
||||
<?php
|
||||
|
||||
namespace App\Core;
|
||||
|
||||
/**
|
||||
* Model - базовый класс модели
|
||||
*/
|
||||
abstract class Model
|
||||
{
|
||||
protected Database $db;
|
||||
protected string $table = '';
|
||||
protected string $primaryKey = 'id';
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$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}";
|
||||
if ($orderBy) {
|
||||
$sql .= " ORDER BY {$orderBy}";
|
||||
}
|
||||
return $this->db->query($sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* Найти записи по условию
|
||||
*/
|
||||
public function where(array $conditions, ?string $orderBy = null): array
|
||||
{
|
||||
$where = [];
|
||||
$params = [];
|
||||
|
||||
foreach ($conditions as $column => $value) {
|
||||
$where[] = "{$column} = ?";
|
||||
$params[] = $value;
|
||||
}
|
||||
|
||||
$sql = "SELECT * FROM {$this->table} WHERE " . implode(' AND ', $where);
|
||||
|
||||
if ($orderBy) {
|
||||
$sql .= " ORDER BY {$orderBy}";
|
||||
}
|
||||
|
||||
return $this->db->query($sql, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Найти одну запись по условию
|
||||
*/
|
||||
public function findWhere(array $conditions): ?array
|
||||
{
|
||||
$where = [];
|
||||
$params = [];
|
||||
|
||||
foreach ($conditions as $column => $value) {
|
||||
$where[] = "{$column} = ?";
|
||||
$params[] = $value;
|
||||
}
|
||||
|
||||
$sql = "SELECT * FROM {$this->table} WHERE " . implode(' AND ', $where) . " LIMIT 1";
|
||||
return $this->db->queryOne($sql, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Создать новую запись
|
||||
*/
|
||||
public function create(array $data): ?int
|
||||
{
|
||||
$columns = array_keys($data);
|
||||
$placeholders = array_fill(0, count($data), '?');
|
||||
|
||||
$sql = sprintf(
|
||||
"INSERT INTO %s (%s) VALUES (%s) RETURNING %s",
|
||||
$this->table,
|
||||
implode(', ', $columns),
|
||||
implode(', ', $placeholders),
|
||||
$this->primaryKey
|
||||
);
|
||||
|
||||
$stmt = $this->db->getConnection()->prepare($sql);
|
||||
$stmt->execute(array_values($data));
|
||||
|
||||
return (int) $stmt->fetchColumn();
|
||||
}
|
||||
|
||||
/**
|
||||
* Обновить запись
|
||||
*/
|
||||
public function update(int $id, array $data): bool
|
||||
{
|
||||
$set = [];
|
||||
$params = [];
|
||||
|
||||
foreach ($data as $column => $value) {
|
||||
$set[] = "{$column} = ?";
|
||||
$params[] = $value;
|
||||
}
|
||||
$params[] = $id;
|
||||
|
||||
$sql = sprintf(
|
||||
"UPDATE %s SET %s WHERE %s = ?",
|
||||
$this->table,
|
||||
implode(', ', $set),
|
||||
$this->primaryKey
|
||||
);
|
||||
|
||||
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}";
|
||||
$params = [];
|
||||
|
||||
if (!empty($conditions)) {
|
||||
$where = [];
|
||||
foreach ($conditions as $column => $value) {
|
||||
$where[] = "{$column} = ?";
|
||||
$params[] = $value;
|
||||
}
|
||||
$sql .= " WHERE " . implode(' AND ', $where);
|
||||
}
|
||||
|
||||
$stmt = $this->db->getConnection()->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
155
app/Core/Router.php
Normal file
155
app/Core/Router.php
Normal file
@@ -0,0 +1,155 @@
|
||||
<?php
|
||||
|
||||
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[] = [
|
||||
'method' => strtoupper($method),
|
||||
'route' => $route,
|
||||
'controller' => $controller,
|
||||
'action' => $action
|
||||
];
|
||||
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);
|
||||
$url = $this->removeQueryString($url);
|
||||
$url = trim($url, '/');
|
||||
|
||||
foreach ($this->routes as $route) {
|
||||
if ($route['method'] !== $method) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$pattern = $this->convertRouteToRegex($route['route']);
|
||||
|
||||
if (preg_match($pattern, $url, $matches)) {
|
||||
// Извлекаем параметры из URL
|
||||
$this->params = $this->extractParams($route['route'], $matches);
|
||||
|
||||
return [
|
||||
'controller' => $route['controller'],
|
||||
'action' => $route['action'],
|
||||
'params' => $this->params
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
if (isset($matches[$index + 1])) {
|
||||
$params[$name] = $matches[$index + 1];
|
||||
}
|
||||
}
|
||||
|
||||
return $params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Удалить query string из URL
|
||||
*/
|
||||
private function removeQueryString(string $url): string
|
||||
{
|
||||
if ($pos = strpos($url, '?')) {
|
||||
$url = substr($url, 0, $pos);
|
||||
}
|
||||
return $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить параметры маршрута
|
||||
*/
|
||||
public function getParams(): array
|
||||
{
|
||||
return $this->params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Диспетчеризация запроса
|
||||
*/
|
||||
public function dispatch(string $url, string $method): void
|
||||
{
|
||||
$match = $this->match($url, $method);
|
||||
|
||||
if ($match === null) {
|
||||
http_response_code(404);
|
||||
echo View::render('errors/404', ['url' => $url], 'main');
|
||||
return;
|
||||
}
|
||||
|
||||
$controllerClass = "App\\Controllers\\" . $match['controller'];
|
||||
$action = $match['action'];
|
||||
|
||||
if (!class_exists($controllerClass)) {
|
||||
throw new \Exception("Контроллер {$controllerClass} не найден");
|
||||
}
|
||||
|
||||
$controller = new $controllerClass();
|
||||
|
||||
if (!method_exists($controller, $action)) {
|
||||
throw new \Exception("Метод {$action} не найден в контроллере {$controllerClass}");
|
||||
}
|
||||
|
||||
// Вызываем метод контроллера с параметрами
|
||||
call_user_func_array([$controller, $action], $match['params']);
|
||||
}
|
||||
}
|
||||
|
||||
184
app/Core/View.php
Normal file
184
app/Core/View.php
Normal file
@@ -0,0 +1,184 @@
|
||||
<?php
|
||||
|
||||
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)) {
|
||||
self::$viewsPath = dirname(__DIR__) . '/Views';
|
||||
}
|
||||
return self::$viewsPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Отрендерить представление
|
||||
*/
|
||||
public static function render(string $view, array $data = [], ?string $layout = 'main'): string
|
||||
{
|
||||
$viewPath = self::getViewsPath() . '/' . str_replace('.', '/', $view) . '.php';
|
||||
|
||||
if (!file_exists($viewPath)) {
|
||||
throw new \Exception("Представление не найдено: {$viewPath}");
|
||||
}
|
||||
|
||||
// Извлекаем данные в переменные
|
||||
extract($data);
|
||||
|
||||
// Буферизируем вывод контента
|
||||
ob_start();
|
||||
require $viewPath;
|
||||
$content = ob_get_clean();
|
||||
|
||||
// Если есть layout, оборачиваем контент
|
||||
if ($layout !== null) {
|
||||
$layoutPath = self::getViewsPath() . '/layouts/' . $layout . '.php';
|
||||
|
||||
if (!file_exists($layoutPath)) {
|
||||
throw new \Exception("Layout не найден: {$layoutPath}");
|
||||
}
|
||||
|
||||
ob_start();
|
||||
require $layoutPath;
|
||||
$content = ob_get_clean();
|
||||
}
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Отрендерить partial (часть шаблона)
|
||||
*/
|
||||
public static function partial(string $partial, array $data = []): string
|
||||
{
|
||||
$partialPath = self::getViewsPath() . '/partials/' . $partial . '.php';
|
||||
|
||||
if (!file_exists($partialPath)) {
|
||||
throw new \Exception("Partial не найден: {$partialPath}");
|
||||
}
|
||||
|
||||
extract($data);
|
||||
|
||||
ob_start();
|
||||
require $partialPath;
|
||||
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'] ?? [];
|
||||
unset($_SESSION['flash']);
|
||||
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()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => $_SESSION['user_id'] ?? null,
|
||||
'email' => $_SESSION['user_email'] ?? '',
|
||||
'full_name' => $_SESSION['full_name'] ?? '',
|
||||
'is_admin' => $_SESSION['isAdmin'] ?? false,
|
||||
'login_time' => $_SESSION['login_time'] ?? null
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Генерация URL
|
||||
*/
|
||||
public static function url(string $path): string
|
||||
{
|
||||
return '/' . ltrim($path, '/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Генерация URL для ассетов
|
||||
*/
|
||||
public static function asset(string $path): string
|
||||
{
|
||||
return '/assets/' . ltrim($path, '/');
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user