feat: add Request and Response value objects

This commit is contained in:
Keith Solomon
2026-07-06 17:52:29 -05:00
parent 9ce0c2ac20
commit 3263975d2a
3 changed files with 134 additions and 0 deletions
+29
View File
@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace BattleForge\Http;
final readonly class Request
{
/**
* @param array<string, mixed> $get
* @param array<string, mixed> $post
* @param array<string, array<string, mixed>> $files
* @param array<string, string> $cookies
* @param array<string, string> $server
*/
public function __construct(
public array $get,
public array $post,
public array $files,
public array $cookies,
public array $server,
public string $queryString,
public string $method,
public string $path,
public ?string $contentType,
public string $rawBody,
) {
}
}
+49
View File
@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace BattleForge\Http;
final class Response
{
private const SECURITY_HEADERS = [
'X-Content-Type-Options' => 'nosniff',
'Referrer-Policy' => 'same-origin',
'Content-Security-Policy' => "default-src 'self'; img-src 'self' data:; style-src 'self'",
];
/** @param array<string, string> $headers */
public function __construct(
public int $status,
public array $headers,
public string $body,
) {
}
public static function html(int $status, string $body): self
{
return new self(
$status,
['Content-Type' => 'text/html; charset=utf-8'] + self::SECURITY_HEADERS,
$body,
);
}
public static function json(int $status, mixed $body): self
{
return new self(
$status,
['Content-Type' => 'application/json; charset=utf-8'] + self::SECURITY_HEADERS,
json_encode($body, JSON_THROW_ON_ERROR),
);
}
public static function redirect(string $location, int $status = 303): self
{
return new self(
$status,
['Location' => $location] + self::SECURITY_HEADERS,
'',
);
}
}
+56
View File
@@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
namespace BattleForge\Tests\Unit\Http;
use BattleForge\Http\Request;
use PHPUnit\Framework\TestCase;
final class RequestTest extends TestCase
{
public function testItExposesAllSuperglobalsAsConstructorArgs(): void
{
$request = new Request(
get: ['q' => '1'],
post: ['name' => 'alpha'],
files: ['image' => ['name' => 'a.png', 'tmp_name' => '/tmp/x', 'error' => 0, 'size' => 12, 'type' => 'image/png']],
cookies: ['__csrf' => 'cookie-value'],
server: ['HTTP_HOST' => 'localhost'],
queryString: 'q=1',
method: 'POST',
path: '/scenarios/demo/edit/team',
contentType: 'application/x-www-form-urlencoded',
rawBody: '',
);
self::assertSame(['q' => '1'], $request->get);
self::assertSame(['name' => 'alpha'], $request->post);
self::assertSame('a.png', $request->files['image']['name']);
self::assertSame('cookie-value', $request->cookies['__csrf']);
self::assertSame('localhost', $request->server['HTTP_HOST']);
self::assertSame('q=1', $request->queryString);
self::assertSame('POST', $request->method);
self::assertSame('/scenarios/demo/edit/team', $request->path);
self::assertSame('application/x-www-form-urlencoded', $request->contentType);
self::assertSame('', $request->rawBody);
}
public function testItAllowsNullContentType(): void
{
$request = new Request(
get: [],
post: [],
files: [],
cookies: [],
server: [],
queryString: '',
method: 'GET',
path: '/',
contentType: null,
rawBody: '',
);
self::assertNull($request->contentType);
}
}