Bicycle/System/Classes/Request.php
Egor Isaev b65603b4d0 dev
2026-06-23 10:44:18 +03:00

257 lines
8.3 KiB
PHP

<?php
/**
* @package Bicycle
* @author Egor Isaev
* @description Request.php
* @copyright (c) 03/06/2026
*/
namespace System\Classes;
use System\Classes\HTTP\HTTPException;
use System\Classes\HTTP\Request as HTTPRequest;
/**
* Входящий HTTP-запрос от браузера. Реализует System\Classes\HTTP\Request.
*/
class Request implements HTTPRequest
{
/** @var static|null Первый созданный запрос */
public static ?self $initial = null;
/** @var static|null Текущий запрос */
public static ?self $current = null;
/** @var string Исходный URI запроса */
private string $_uri;
/** @var string Имя контроллера */
private string $_controller = 'Index';
/** @var string Имя действия */
private string $_action = 'index';
/** @var array Позиционные параметры из URI */
private array $_params = [];
/** @var string HTTP-метод */
private string $_method = HTTPRequest::GET;
/** @var array POST-данные */
private array $_post = [];
/** @var array GET-параметры */
private array $_get = [];
/** @var string|null Тело запроса */
private ?string $_body = null;
/** @var string Значение заголовка X-Requested-With (lowercase) */
private string $_requested_with = '';
/**
* @param string $uri URI запроса; сразу резолвится в контроллер/действие/параметры
*/
private function __construct(string $uri)
{
$this->_uri = $uri;
$route = Route::resolve($uri);
$this->_controller = $route['controller'];
$this->_action = $route['action'];
$this->_params = $route['params'];
}
/**
* Создаёт запрос из суперглобалов: URI, метод, $_GET, $_POST, тело, X-Requested-With.
*
* @return static
*/
public static function factory(): static
{
$uri = self::detectUri();
$instance = new static($uri);
if (self::$initial === null) {
self::$initial = $instance;
}
self::$current = $instance;
$instance->method($_SERVER['REQUEST_METHOD'] ?? HTTPRequest::GET);
$instance->query($_GET);
$instance->post($_POST);
$instance->_requested_with = strtolower($_SERVER['HTTP_X_REQUESTED_WITH'] ?? '');
if ($instance->method() !== HTTPRequest::GET) {
$body = file_get_contents('php://input');
if ($body !== false && $body !== '') {
$instance->body($body);
}
}
return $instance;
}
/**
* Определяет URI через PATH_INFO → REQUEST_URI → PHP_SELF.
*
* @return string URI без ведущего/замыкающего слэша
*/
public static function detectUri(): string
{
if (!empty($_SERVER['PATH_INFO'])) {
$uri = $_SERVER['PATH_INFO'];
} elseif (isset($_SERVER['REQUEST_URI'])) {
$uri = $_SERVER['REQUEST_URI'];
if ($path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH)) {
$uri = $path;
}
$uri = rawurldecode($uri);
} elseif (isset($_SERVER['PHP_SELF'])) {
$uri = $_SERVER['PHP_SELF'];
} else {
$uri = '/';
}
return trim($uri, '/');
}
/**
* Подключает контроллер и вызывает {action}Action(). При отсутствии — 404.
*
* @return Response
*/
public function execute(): Response
{
$method = lcfirst($this->_action) . 'Action';
$parts = explode('/', $this->_controller);
$file = DOCROOT . '/App/Controller/' . implode('/', array_map('ucfirst', $parts)) . 'Controller.php';
if (!file_exists($file)) {
return HTTPException::factory(404)->getResponse();
}
require_once $file;
$class = 'App\\Controller\\' . implode('\\', array_map('ucfirst', $parts)) . 'Controller';
if (!method_exists($class, $method)) {
return HTTPException::factory(404)->getResponse();
}
return (new Response())->body((new $class())->$method());
}
/**
* Геттер/сеттер HTTP-метода.
*
* @param string|null $method Новый метод или null для чтения
* @return string|static
*/
public function method(?string $method = null): string|static
{
if ($method === null) {
return $this->_method;
}
$this->_method = strtoupper($method);
return $this;
}
/**
* Геттер/сеттер POST-данных.
*
* @param string|array|null $key Массив (замена всех), ключ (чтение/запись) или null (все)
* @param mixed $value Значение при записи по ключу
* @return array|string|static|null
*/
public function post(null|string|array $key = null, mixed $value = null): null|array|string|static
{
if (is_array($key)) {
$this->_post = $key;
return $this;
}
if ($key === null) {
return $this->_post;
}
if ($value === null) {
return $this->_post[$key] ?? null;
}
$this->_post[$key] = $value;
return $this;
}
/**
* Геттер/сеттер GET-параметров.
*
* @param string|array|null $key Массив (замена всех), ключ (чтение/запись) или null (все)
* @param mixed $value Значение при записи по ключу
* @return array|string|static|null
*/
public function query(null|string|array $key = null, mixed $value = null): null|array|string|static
{
if (is_array($key)) {
$this->_get = $key;
return $this;
}
if ($key === null) {
return $this->_get;
}
if ($value === null) {
return $this->_get[$key] ?? null;
}
$this->_get[$key] = $value;
return $this;
}
/**
* Геттер/сеттер тела запроса.
*
* @param string|null $content Новое тело или null для чтения
* @return string|static
*/
public function body(?string $content = null): string|static
{
if ($content === null) {
return $this->_body ?? '';
}
$this->_body = $content;
return $this;
}
/**
* Геттер/сеттер заголовка X-Requested-With.
*
* @param string|null $value Новое значение или null для чтения
* @return string|static
*/
public function requestedWith(?string $value = null): string|static
{
if ($value === null) {
return $this->_requested_with;
}
$this->_requested_with = strtolower($value);
return $this;
}
/**
* Проверяет, является ли запрос AJAX (X-Requested-With: XMLHttpRequest).
*
* @return bool
*/
public function isAjax(): bool
{
return $this->_requested_with === 'xmlhttprequest';
}
/** @return string Имя контроллера */
public function controller(): string { return $this->_controller; }
/** @return string Имя действия */
public function action(): string { return $this->_action; }
/** @return array Позиционные параметры */
public function params(): array { return $this->_params; }
/** @return string Исходный URI */
public function uri(): string { return $this->_uri; }
/**
* Возвращает позиционный параметр по индексу.
*
* @param int $index Индекс параметра
* @param mixed $default Значение по умолчанию
* @return mixed
*/
public function param(int $index, mixed $default = null): mixed
{
return $this->_params[$index] ?? $default;
}
}