99 lines
3.6 KiB
PHP
99 lines
3.6 KiB
PHP
<?php
|
||
/**
|
||
* @package Bicycle
|
||
* @author Egor Isaev
|
||
* @description Controller.php
|
||
* @copyright (c) 03/06/2026
|
||
*/
|
||
|
||
namespace System\Classes;
|
||
|
||
use System\Classes\HTTP\HTTPException;
|
||
use System\Classes\HTTP\Request as HTTPRequest;
|
||
|
||
/**
|
||
* Веб-контроллер: рендеринг layout + content, авто-проверка CSRF на небезопасных
|
||
* методах и JSON-ответы. Контроллеры приложения наследуют его.
|
||
*/
|
||
class Controller extends BaseController
|
||
{
|
||
/** @var string Имя layout-шаблона в System/view/views */
|
||
protected string $_layout = 'layout';
|
||
|
||
/** @var bool Проверять ли CSRF-токен на небезопасных методах */
|
||
protected bool $_csrf_protection = true;
|
||
|
||
/**
|
||
* Проверяет CSRF-токен на POST/PUT/PATCH/DELETE.
|
||
* Отключается флагом $_csrf_protection (например, для API/вебхуков).
|
||
*
|
||
* @return void
|
||
* @throws HTTPException 403, если токен не прошёл
|
||
*/
|
||
protected function before(): void
|
||
{
|
||
if (!$this->_csrf_protection) {
|
||
return;
|
||
}
|
||
|
||
$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);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Рендерит шаблон контента внутри layout.
|
||
* Если $dir пуст — определяется автоматически из имени класса
|
||
* (App\Controller\FooController → view/Foo).
|
||
*
|
||
* @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
|
||
{
|
||
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($this->_layout, 'view/views', [
|
||
'content' => (new View($template, $dir, $data))->render(),
|
||
] + $this->layoutData()))->render();
|
||
}
|
||
|
||
/**
|
||
* Дополнительные данные, передаваемые в layout помимо 'content'
|
||
* (например меню). Переопределяется наследниками. По умолчанию пусто.
|
||
*
|
||
* @return array
|
||
*/
|
||
protected function layoutData(): array
|
||
{
|
||
return [];
|
||
}
|
||
|
||
/**
|
||
* Формирует JSON-ответ: ставит статус и Content-Type, кодирует данные.
|
||
*
|
||
* @param mixed $data Данные ответа
|
||
* @param int $status HTTP-статус
|
||
* @return string JSON
|
||
*/
|
||
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);
|
||
}
|
||
}
|