257 lines
8.3 KiB
PHP
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())->executeAction($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;
|
|
}
|
|
}
|