Роли, которым разрешён доступ; пустой массив — любой авторизованный */ protected array $_auth_roles = []; /** * Проверяет CSRF-токен на POST/PUT/PATCH/DELETE и, если включено, * авторизацию + роль (редирект на /login при отсутствии авторизации, * 403 — при недостаточной роли). * * @return void * @throws HTTPException|MyException 403, если CSRF-токен не прошёл или роль не подходит */ protected function before(): void { if ($this->_csrf_protection) { $request = Request::$current; $method = $request?->method() ?? HTTPRequest::GET; $unsafe = [HTTPRequest::POST, HTTPRequest::PUT, HTTPRequest::PATCH, HTTPRequest::DELETE]; if (in_array($method, $unsafe, true) && !CSRF::validate($request->post(CSRF::$key))) { throw HTTPException::factory(403); } } if ($this->_auth_protection) { $user = Auth::instance($this->_auth_driver)->getUser(); if ($user === null) { // Не исключительная ситуация — штатное поведение (гостя отправляем логиниться), // поэтому обычный редирект, а не throw HTTPException: throw ушёл бы в // MyException::handler() и отрендерил бы страницу ошибки вместо перехода на /login. HTTP::redirect('/login'); } if ($this->_auth_roles && !in_array($user['role'] ?? null, $this->_auth_roles, true)) { throw HTTPException::factory(403); } } } /** * Для admin — заголовок X-Profiler с полной картиной запроса (время, память, список * SQL-запросов с временем каждого). Виден в devtools (Network → заголовки ответа) для * любого ответа, включая ajax — там HTML-панель ProfilerToolbar не рендерится вовсе * (renderContent()/json() не проходят через layout.html, где она подключена). Тело * ответа не трогаем осознанно — ajax-эндпоинты отдают JSON/HTML, менять их форму ради * дебага не нужно. * * @return void * @throws \JsonException */ protected function after(): void { $user = Auth::instance()->getUser(); if (($user['role'] ?? null) !== 'admin' || headers_sent()) { return; } $time_ms = (microtime(true) - ($_SERVER['REQUEST_TIME_FLOAT'] ?? microtime(true))) * 1000; $data = [ 'time_ms' => round($time_ms, 1), 'memory_mb' => round(memory_get_peak_usage(true) / 1024 / 1024, 1), 'sql_count' => Profiler::count(), 'sql_time_ms' => round(Profiler::totalTime(), 1), 'sql' => array_map( static fn (array $entry) => ['sql' => $entry['sql'], 'time_ms' => round($entry['time_ms'], 2)], Profiler::entries() ), ]; header('X-Profiler: ' . json_encode($data, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR)); } /** * Рендерит шаблон контента внутри layout. Меню — не забота движка: разметка/ссылки * навигации живут прямо в App/view/layout.html (см. этот файл в проекте), а не строятся * тут из PHP-массива. * * @param string $template Имя шаблона контента без расширения * @param array $data Данные, передаваемые в шаблон * @param string $dir Каталог шаблона относительно view/ (опционально) * @return string Готовый HTML * @throws MyException */ protected function render(string $template, array $data = [], string $dir = ''): string { return (new View($this->_layout, 'view', [ 'content' => $this->renderContent($template, $data, $dir), ]))->render(); } /** * Рендерит шаблон контента без layout — просто view, без меню и обвязки. * Если $dir пуст — определяется автоматически из имени класса * (App\Controller\FooController → view/Foo). Пригодится для ajax-фрагментов: * экшен сам решает, вызывать render() или renderContent(), без проверки isAjax(). * * @param string $template Имя шаблона контента без расширения * @param array $data Данные, передаваемые в шаблон * @param string $dir Каталог шаблона относительно view/ (опционально) * @return string Готовый HTML фрагмента * @throws MyException */ protected function renderContent(string $template, array $data = [], string $dir = ''): string { if ($dir === '') { $class = substr(get_class($this), strlen('App\\Controller\\')); // [Admin\]FooController $dir = 'view/' . str_replace('\\', '/', substr($class, 0, -10)); // view/[Admin/]Foo } return (new View($template, $dir, $data))->render(); } /** * Формирует JSON-ответ: ставит статус и Content-Type, кодирует данные. * * @param mixed $data Данные ответа * @param int $status HTTP-статус * @return string JSON * @throws \JsonException */ protected function json(mixed $data, int $status = 200): string { http_response_code($status); if (!headers_sent()) { header('Content-Type: application/json; charset=utf-8'); } return json_encode($data, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR); } }