diff --git a/packages/http/src/IsRequest.php b/packages/http/src/IsRequest.php index 4c3ce8b917..73f8f9310c 100644 --- a/packages/http/src/IsRequest.php +++ b/packages/http/src/IsRequest.php @@ -10,8 +10,10 @@ use Tempest\Validation\SkipValidation; use function Tempest\get; +use function Tempest\Support\Arr\every; use function Tempest\Support\Arr\get_by_key; use function Tempest\Support\Arr\has_key; +use function Tempest\Support\str; /** @phpstan-require-implements \Tempest\Http\Request */ trait IsRequest @@ -136,6 +138,46 @@ public function hasQuery(string $key): bool return has_key($this->query, $key); } + public function accepts(ContentType ...$contentTypes): bool + { + $header = $this->headers->get(name: 'accept') ?? ''; + + /** @var array{mediaType:string,subType:string} */ + $mediaTypes = []; + + foreach (str($header)->explode(separator: ',') as $acceptedType) { + $acceptedType = str($acceptedType)->trim(); + + if ($acceptedType->isEmpty()) { + continue; + } + + $mediaTypes[] = [ + 'mediaType' => $acceptedType->before('/')->toString(), + 'subType' => $acceptedType->afterFirst('/')->beforeLast(';q')->toString(), + ]; + } + + if (count($mediaTypes) === 0) { + return true; + } + + foreach ($contentTypes as $contentType) { + [$mediaType, $subType] = explode('/', $contentType->value); + + foreach ($mediaTypes as $acceptedType) { + if ( + ($acceptedType['mediaType'] === '*' || $acceptedType['mediaType'] === $mediaType) + && ($acceptedType['subType'] === '*' || $acceptedType['subType'] === $subType) + ) { + return true; + } + } + } + + return false; + } + public function withMethod(Method $method): self { $clone = clone $this; diff --git a/packages/http/src/Request.php b/packages/http/src/Request.php index 77b6d7f430..f2eb0ef31e 100644 --- a/packages/http/src/Request.php +++ b/packages/http/src/Request.php @@ -57,4 +57,10 @@ public function get(string $key, mixed $default = null): mixed; public function getSessionValue(string $name): mixed; public function getCookie(string $name): ?Cookie; + + /** + * Determines if the request's "Content-Type" header matches the given content type. + * If multiple content types are provided, the method returns true if any of them matches. + */ + public function accepts(ContentType ...$contentType): bool; } diff --git a/packages/http/tests/GenericRequestTest.php b/packages/http/tests/GenericRequestTest.php index b128fe20b4..f4a2e9f61a 100644 --- a/packages/http/tests/GenericRequestTest.php +++ b/packages/http/tests/GenericRequestTest.php @@ -6,6 +6,7 @@ use LogicException; use PHPUnit\Framework\TestCase; +use Tempest\Http\ContentType; use Tempest\Http\GenericRequest; use Tempest\Http\Header; use Tempest\Http\Method; @@ -61,4 +62,122 @@ public function test_throws_on_unset(): void $this->expectException(LogicException::class); unset($headers['x']); } + + public function test_accepts_with_accept_header(): void + { + $request = new GenericRequest( + method: Method::GET, + uri: '/', + headers: [ + 'Accept' => 'application/json', + ], + ); + + $this->assertTrue($request->accepts(ContentType::JSON)); + $this->assertFalse($request->accepts(ContentType::HTML)); + $this->assertFalse($request->accepts(ContentType::XML)); + } + + public function test_accepts_with_no_accept_header(): void + { + $request = new GenericRequest( + method: Method::GET, + uri: '/', + ); + + $this->assertTrue($request->accepts(ContentType::JSON)); + $this->assertTrue($request->accepts(ContentType::HTML)); + $this->assertTrue($request->accepts(ContentType::XML)); + } + + public function test_accepts_with_empty_accept_header(): void + { + $request = new GenericRequest( + method: Method::GET, + uri: '/', + headers: [ + 'Accept' => '', + ], + ); + + $this->assertTrue($request->accepts(ContentType::JSON)); + $this->assertTrue($request->accepts(ContentType::HTML)); + $this->assertTrue($request->accepts(ContentType::XML)); + } + + public function test_accepts_with_wildcard(): void + { + $request = new GenericRequest( + method: Method::GET, + uri: '/', + headers: [ + 'Accept' => '*/*', + ], + ); + + $this->assertTrue($request->accepts(ContentType::JSON)); + $this->assertTrue($request->accepts(ContentType::HTML)); + $this->assertTrue($request->accepts(ContentType::XML)); + } + + public function test_accepts_with_multiple_values(): void + { + $request = new GenericRequest( + method: Method::GET, + uri: '/', + headers: [ + 'Accept' => 'application/json, text/html', + ], + ); + + $this->assertTrue($request->accepts(ContentType::JSON)); + $this->assertTrue($request->accepts(ContentType::HTML)); + $this->assertFalse($request->accepts(ContentType::XML)); + } + + public function test_accepts_with_wildcard_subtype(): void + { + $request = new GenericRequest( + method: Method::GET, + uri: '/', + headers: [ + 'Accept' => 'application/*', + ], + ); + + $this->assertTrue($request->accepts(ContentType::JSON)); + $this->assertFalse($request->accepts(ContentType::HTML)); + $this->assertTrue($request->accepts(ContentType::XML)); + } + + public function test_accepts_can_handle_priorities(): void + { + $request = new GenericRequest( + method: Method::GET, + uri: '/', + headers: [ + 'Accept' => 'text/html, application/xhtml+xml;q=0.8, application/xml;q=0.8, image/webp', + ], + ); + + $this->assertTrue($request->accepts(ContentType::XHTML)); + $this->assertTrue($request->accepts(ContentType::XML)); + } + + public function test_accepts_returns_true_on_first_match(): void + { + $request = new GenericRequest( + method: Method::GET, + uri: '/', + headers: [ + 'Accept' => 'application/*, image/avif', + ], + ); + + $this->assertTrue($request->accepts(ContentType::HTML, ContentType::JSON)); + $this->assertTrue($request->accepts(ContentType::XML, ContentType::JSON)); + $this->assertTrue($request->accepts(ContentType::JSON, ContentType::AVIF)); + $this->assertTrue($request->accepts(ContentType::AVIF, ContentType::PNG)); + $this->assertFalse($request->accepts(ContentType::HTML, ContentType::PNG)); + } }