feat: initial foundation for collaborative canteen ordering system in vanilla PHP

This commit is contained in:
2026-08-27 17:29:13 +07:00
commit 5effce30cb
52 changed files with 4915 additions and 0 deletions
+114
View File
@@ -0,0 +1,114 @@
<?php
declare(strict_types=1);
namespace App\Core\Http;
class Request
{
private string $method;
private string $uri;
private string $path;
private array $queryParams;
private array $bodyParams;
private array $headers;
public function __construct()
{
$this->method = strtoupper($_SERVER['REQUEST_METHOD'] ?? 'GET');
// Handle method overriding via _method for forms (PUT/DELETE/PATCH)
if ($this->method === 'POST' && isset($_POST['_method'])) {
$this->method = strtoupper($_POST['_method']);
}
$this->uri = $_SERVER['REQUEST_URI'] ?? '/';
$parsedUrl = parse_url($this->uri);
$this->path = $parsedUrl['path'] ?? '/';
// Trim trailing slash for consistent routing (except root)
if ($this->path !== '/' && str_ends_with($this->path, '/')) {
$this->path = rtrim($this->path, '/');
}
$this->queryParams = $_GET;
// Safely extract headers in all SAPIs
if (function_exists('getallheaders')) {
$this->headers = getallheaders() ?: [];
} else {
$this->headers = [];
foreach ($_SERVER as $name => $value) {
if (str_starts_with($name, 'HTTP_')) {
$headerName = str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))));
$this->headers[$headerName] = $value;
} elseif (in_array($name, ['CONTENT_TYPE', 'CONTENT_LENGTH'], true)) {
$headerName = str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', $name))));
$this->headers[$headerName] = $value;
}
}
}
// Parse JSON or form POST body
$contentType = $_SERVER['CONTENT_TYPE'] ?? '';
if (str_contains($contentType, 'application/json')) {
$rawBody = file_get_contents('php://input');
$this->bodyParams = json_decode($rawBody, true) ?: [];
} else {
$this->bodyParams = $_POST;
}
}
public function getMethod(): string
{
return $this->method;
}
public function getPath(): string
{
return $this->path;
}
public function getUri(): string
{
return $this->uri;
}
public function get(string $key, mixed $default = null): mixed
{
return $this->queryParams[$key] ?? $default;
}
public function post(string $key, mixed $default = null): mixed
{
return $this->bodyParams[$key] ?? $default;
}
public function all(): array
{
return array_merge($this->queryParams, $this->bodyParams);
}
public function getBody(): array
{
return $this->bodyParams;
}
public function getHeader(string $name, ?string $default = null): ?string
{
$name = strtolower($name);
foreach ($this->headers as $key => $value) {
if (strtolower($key) === $name) {
return $value;
}
}
return $default;
}
public function isJson(): bool
{
$contentType = $_SERVER['CONTENT_TYPE'] ?? '';
$accept = $_SERVER['HTTP_ACCEPT'] ?? '';
return str_contains($contentType, 'application/json') || str_contains($accept, 'application/json');
}
}
+61
View File
@@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
namespace App\Core\Http;
class Response
{
private int $statusCode = 200;
private array $headers = [];
private string $content = '';
public function setStatusCode(int $code): self
{
$this->statusCode = $code;
return $this;
}
public function getStatusCode(): int
{
return $this->statusCode;
}
public function setHeader(string $name, string $value): self
{
$this->headers[$name] = $value;
return $this;
}
public function setContent(string $content): self
{
$this->content = $content;
return $this;
}
public function json(mixed $data, int $statusCode = 200): self
{
$this->statusCode = $statusCode;
$this->setHeader('Content-Type', 'application/json');
$this->content = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
return $this;
}
public function redirect(string $url, int $statusCode = 302): self
{
$this->statusCode = $statusCode;
$this->setHeader('Location', $url);
return $this;
}
public function send(): void
{
http_response_code($this->statusCode);
foreach ($this->headers as $name => $value) {
header("{$name}: {$value}");
}
echo $this->content;
}
}