From f5fdd7bec9b1d6f9ff7b40bf6a7f79a58dd3dda9 Mon Sep 17 00:00:00 2001 From: NeoIsRecursive Date: Sun, 12 Oct 2025 17:34:51 +0200 Subject: [PATCH 1/5] wip --- packages/http/src/IsRequest.php | 21 ++++++++ packages/http/src/Request.php | 2 + packages/http/tests/GenericRequestTest.php | 58 ++++++++++++++++++++++ 3 files changed, 81 insertions(+) diff --git a/packages/http/src/IsRequest.php b/packages/http/src/IsRequest.php index 4c3ce8b917..8fca7fed1d 100644 --- a/packages/http/src/IsRequest.php +++ b/packages/http/src/IsRequest.php @@ -12,6 +12,7 @@ use function Tempest\get; 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 +137,26 @@ public function hasQuery(string $key): bool return has_key($this->query, $key); } + public function accepts(ContentType $contentType): bool + { + $header = $this->headers->get(name: 'accept'); + + if ($header === null) { + return true; + } + + $accepts = str($header) + ->explode(separator: ',') + ->map(static fn (string $item) => trim($item)) + ->filter(static fn (string $item) => $item !== ''); + + if ($accepts->isEmpty() || $accepts->contains(search: '*/*')) { + return true; + } + + return $accepts->contains(search: $contentType->value); + } + 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..3d1bdd7955 100644 --- a/packages/http/src/Request.php +++ b/packages/http/src/Request.php @@ -57,4 +57,6 @@ public function get(string $key, mixed $default = null): mixed; public function getSessionValue(string $name): mixed; public function getCookie(string $name): ?Cookie; + + public function accepts(ContentType $contentType): bool; } diff --git a/packages/http/tests/GenericRequestTest.php b/packages/http/tests/GenericRequestTest.php index b128fe20b4..529c2a0af6 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,61 @@ 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_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)); + } } From a9c77acf5cbba295a48ce160988ab877b93f5161 Mon Sep 17 00:00:00 2001 From: NeoIsRecursive Date: Sun, 12 Oct 2025 19:19:10 +0200 Subject: [PATCH 2/5] wip support wildcards --- packages/http/src/IsRequest.php | 45 +++++++++++++++------ packages/http/src/Request.php | 7 +++- packages/http/tests/GenericRequestTest.php | 46 ++++++++++++++++++++++ 3 files changed, 86 insertions(+), 12 deletions(-) diff --git a/packages/http/src/IsRequest.php b/packages/http/src/IsRequest.php index 8fca7fed1d..f3aa455c18 100644 --- a/packages/http/src/IsRequest.php +++ b/packages/http/src/IsRequest.php @@ -10,6 +10,7 @@ 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; @@ -137,24 +138,46 @@ public function hasQuery(string $key): bool return has_key($this->query, $key); } - public function accepts(ContentType $contentType): bool + public function accepts(ContentType ...$contentTypes): bool { - $header = $this->headers->get(name: 'accept'); + $header = $this->headers->get(name: 'accept') ?? ''; - if ($header === null) { - return true; + /** @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('/')->toString(), + ]; } - $accepts = str($header) - ->explode(separator: ',') - ->map(static fn (string $item) => trim($item)) - ->filter(static fn (string $item) => $item !== ''); + /** @var array */ + $supported = []; - if ($accepts->isEmpty() || $accepts->contains(search: '*/*')) { - 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) + ) { + $supported[$contentType->value] = true; + break; + } + + $supported[$contentType->value] = false; + } } - return $accepts->contains(search: $contentType->value); + return every($supported, static fn (bool $isSupported) => $isSupported); } public function withMethod(Method $method): self diff --git a/packages/http/src/Request.php b/packages/http/src/Request.php index 3d1bdd7955..f9901709b9 100644 --- a/packages/http/src/Request.php +++ b/packages/http/src/Request.php @@ -58,5 +58,10 @@ public function getSessionValue(string $name): mixed; public function getCookie(string $name): ?Cookie; - public function accepts(ContentType $contentType): bool; + /** + * Determines if the request's "Content-Type" header matches the given content type. + * + * If multiple content types are provided, the method returns true if all are matched. + */ + public function accepts(ContentType ...$contentType): bool; } diff --git a/packages/http/tests/GenericRequestTest.php b/packages/http/tests/GenericRequestTest.php index 529c2a0af6..3d3eee8f70 100644 --- a/packages/http/tests/GenericRequestTest.php +++ b/packages/http/tests/GenericRequestTest.php @@ -90,6 +90,21 @@ public function test_accepts_with_no_accept_header(): void $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( @@ -119,4 +134,35 @@ public function test_accepts_with_multiple_values(): void $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_evaluates_all_content_types(): void + { + $request = new GenericRequest( + method: Method::GET, + uri: '/', + headers: [ + 'Accept' => 'application/*, image/avif', + ], + ); + + $this->assertFalse($request->accepts(ContentType::JSON, ContentType::HTML)); + $this->assertTrue($request->accepts(ContentType::JSON, ContentType::XML)); + $this->assertTrue($request->accepts(ContentType::JSON, ContentType::AVIF)); + $this->assertFalse($request->accepts(ContentType::AVIF, ContentType::PNG)); + } } From c168e8346d7cba22ed5feef5d57a0f688dab3bfb Mon Sep 17 00:00:00 2001 From: NeoIsRecursive Date: Sun, 12 Oct 2025 19:31:58 +0200 Subject: [PATCH 3/5] change to do "or" instead of "and" --- packages/http/src/IsRequest.php | 12 +++++------- packages/http/src/Request.php | 3 +-- packages/http/tests/GenericRequestTest.php | 9 +++++---- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/packages/http/src/IsRequest.php b/packages/http/src/IsRequest.php index f3aa455c18..7ee01b6a56 100644 --- a/packages/http/src/IsRequest.php +++ b/packages/http/src/IsRequest.php @@ -158,8 +158,9 @@ public function accepts(ContentType ...$contentTypes): bool ]; } - /** @var array */ - $supported = []; + if (empty($mediaTypes)) { + return true; + } foreach ($contentTypes as $contentType) { [$mediaType, $subType] = explode('/', $contentType->value); @@ -169,15 +170,12 @@ public function accepts(ContentType ...$contentTypes): bool ($acceptedType['mediaType'] === '*' || $acceptedType['mediaType'] === $mediaType) && ($acceptedType['subType'] === '*' || $acceptedType['subType'] === $subType) ) { - $supported[$contentType->value] = true; - break; + return true; } - - $supported[$contentType->value] = false; } } - return every($supported, static fn (bool $isSupported) => $isSupported); + return false; } public function withMethod(Method $method): self diff --git a/packages/http/src/Request.php b/packages/http/src/Request.php index f9901709b9..f2eb0ef31e 100644 --- a/packages/http/src/Request.php +++ b/packages/http/src/Request.php @@ -60,8 +60,7 @@ 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 all are matched. + * 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 3d3eee8f70..c9c7ccef1b 100644 --- a/packages/http/tests/GenericRequestTest.php +++ b/packages/http/tests/GenericRequestTest.php @@ -150,7 +150,7 @@ public function test_accepts_with_wildcard_subtype(): void $this->assertTrue($request->accepts(ContentType::XML)); } - public function test_accepts_evaluates_all_content_types(): void + public function test_accepts_can_take_multiple_params(): void { $request = new GenericRequest( method: Method::GET, @@ -160,9 +160,10 @@ public function test_accepts_evaluates_all_content_types(): void ], ); - $this->assertFalse($request->accepts(ContentType::JSON, ContentType::HTML)); - $this->assertTrue($request->accepts(ContentType::JSON, ContentType::XML)); + $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->assertFalse($request->accepts(ContentType::AVIF, ContentType::PNG)); + $this->assertTrue($request->accepts(ContentType::AVIF, ContentType::PNG)); + $this->assertFalse($request->accepts(ContentType::HTML, ContentType::PNG)); } } From 1cf26243933f2baabcf70f18641de22e2e0bc5e6 Mon Sep 17 00:00:00 2001 From: NeoIsRecursive Date: Sun, 12 Oct 2025 19:36:12 +0200 Subject: [PATCH 4/5] fix --- packages/http/src/IsRequest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/http/src/IsRequest.php b/packages/http/src/IsRequest.php index 7ee01b6a56..1a61f1415d 100644 --- a/packages/http/src/IsRequest.php +++ b/packages/http/src/IsRequest.php @@ -158,7 +158,7 @@ public function accepts(ContentType ...$contentTypes): bool ]; } - if (empty($mediaTypes)) { + if (count($mediaTypes) === 0) { return true; } From 95a793fbc48afe4dfd2afd0f125c23eca8261576 Mon Sep 17 00:00:00 2001 From: NeoIsRecursive Date: Sun, 12 Oct 2025 19:43:48 +0200 Subject: [PATCH 5/5] fix include support for priority/weight --- packages/http/src/IsRequest.php | 2 +- packages/http/tests/GenericRequestTest.php | 16 +++++++++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/packages/http/src/IsRequest.php b/packages/http/src/IsRequest.php index 1a61f1415d..73f8f9310c 100644 --- a/packages/http/src/IsRequest.php +++ b/packages/http/src/IsRequest.php @@ -154,7 +154,7 @@ public function accepts(ContentType ...$contentTypes): bool $mediaTypes[] = [ 'mediaType' => $acceptedType->before('/')->toString(), - 'subType' => $acceptedType->afterFirst('/')->toString(), + 'subType' => $acceptedType->afterFirst('/')->beforeLast(';q')->toString(), ]; } diff --git a/packages/http/tests/GenericRequestTest.php b/packages/http/tests/GenericRequestTest.php index c9c7ccef1b..f4a2e9f61a 100644 --- a/packages/http/tests/GenericRequestTest.php +++ b/packages/http/tests/GenericRequestTest.php @@ -150,7 +150,21 @@ public function test_accepts_with_wildcard_subtype(): void $this->assertTrue($request->accepts(ContentType::XML)); } - public function test_accepts_can_take_multiple_params(): void + 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,