Skip to content

Commit b61d352

Browse files
feat(http): add accepts helper method (#1638)
1 parent bf476c0 commit b61d352

File tree

3 files changed

+167
-0
lines changed

3 files changed

+167
-0
lines changed

packages/http/src/IsRequest.php

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@
99
use Tempest\Validation\SkipValidation;
1010

1111
use function Tempest\get;
12+
use function Tempest\Support\Arr\every;
1213
use function Tempest\Support\Arr\get_by_key;
1314
use function Tempest\Support\Arr\has_key;
15+
use function Tempest\Support\str;
1416

1517
/** @phpstan-require-implements \Tempest\Http\Request */
1618
trait IsRequest
@@ -135,6 +137,46 @@ public function hasQuery(string $key): bool
135137
return has_key($this->query, $key);
136138
}
137139

140+
public function accepts(ContentType ...$contentTypes): bool
141+
{
142+
$header = $this->headers->get(name: 'accept') ?? '';
143+
144+
/** @var array{mediaType:string,subType:string} */
145+
$mediaTypes = [];
146+
147+
foreach (str($header)->explode(separator: ',') as $acceptedType) {
148+
$acceptedType = str($acceptedType)->trim();
149+
150+
if ($acceptedType->isEmpty()) {
151+
continue;
152+
}
153+
154+
$mediaTypes[] = [
155+
'mediaType' => $acceptedType->before('/')->toString(),
156+
'subType' => $acceptedType->afterFirst('/')->beforeLast(';q')->toString(),
157+
];
158+
}
159+
160+
if (count($mediaTypes) === 0) {
161+
return true;
162+
}
163+
164+
foreach ($contentTypes as $contentType) {
165+
[$mediaType, $subType] = explode('/', $contentType->value);
166+
167+
foreach ($mediaTypes as $acceptedType) {
168+
if (
169+
($acceptedType['mediaType'] === '*' || $acceptedType['mediaType'] === $mediaType)
170+
&& ($acceptedType['subType'] === '*' || $acceptedType['subType'] === $subType)
171+
) {
172+
return true;
173+
}
174+
}
175+
}
176+
177+
return false;
178+
}
179+
138180
public function withMethod(Method $method): self
139181
{
140182
$clone = clone $this;

packages/http/src/Request.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,4 +57,10 @@ public function get(string $key, mixed $default = null): mixed;
5757
public function getSessionValue(string $name): mixed;
5858

5959
public function getCookie(string $name): ?Cookie;
60+
61+
/**
62+
* Determines if the request's "Content-Type" header matches the given content type.
63+
* If multiple content types are provided, the method returns true if any of them matches.
64+
*/
65+
public function accepts(ContentType ...$contentType): bool;
6066
}

packages/http/tests/GenericRequestTest.php

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
use LogicException;
88
use PHPUnit\Framework\TestCase;
9+
use Tempest\Http\ContentType;
910
use Tempest\Http\GenericRequest;
1011
use Tempest\Http\Header;
1112
use Tempest\Http\Method;
@@ -61,4 +62,122 @@ public function test_throws_on_unset(): void
6162
$this->expectException(LogicException::class);
6263
unset($headers['x']);
6364
}
65+
66+
public function test_accepts_with_accept_header(): void
67+
{
68+
$request = new GenericRequest(
69+
method: Method::GET,
70+
uri: '/',
71+
headers: [
72+
'Accept' => 'application/json',
73+
],
74+
);
75+
76+
$this->assertTrue($request->accepts(ContentType::JSON));
77+
$this->assertFalse($request->accepts(ContentType::HTML));
78+
$this->assertFalse($request->accepts(ContentType::XML));
79+
}
80+
81+
public function test_accepts_with_no_accept_header(): void
82+
{
83+
$request = new GenericRequest(
84+
method: Method::GET,
85+
uri: '/',
86+
);
87+
88+
$this->assertTrue($request->accepts(ContentType::JSON));
89+
$this->assertTrue($request->accepts(ContentType::HTML));
90+
$this->assertTrue($request->accepts(ContentType::XML));
91+
}
92+
93+
public function test_accepts_with_empty_accept_header(): void
94+
{
95+
$request = new GenericRequest(
96+
method: Method::GET,
97+
uri: '/',
98+
headers: [
99+
'Accept' => '',
100+
],
101+
);
102+
103+
$this->assertTrue($request->accepts(ContentType::JSON));
104+
$this->assertTrue($request->accepts(ContentType::HTML));
105+
$this->assertTrue($request->accepts(ContentType::XML));
106+
}
107+
108+
public function test_accepts_with_wildcard(): void
109+
{
110+
$request = new GenericRequest(
111+
method: Method::GET,
112+
uri: '/',
113+
headers: [
114+
'Accept' => '*/*',
115+
],
116+
);
117+
118+
$this->assertTrue($request->accepts(ContentType::JSON));
119+
$this->assertTrue($request->accepts(ContentType::HTML));
120+
$this->assertTrue($request->accepts(ContentType::XML));
121+
}
122+
123+
public function test_accepts_with_multiple_values(): void
124+
{
125+
$request = new GenericRequest(
126+
method: Method::GET,
127+
uri: '/',
128+
headers: [
129+
'Accept' => 'application/json, text/html',
130+
],
131+
);
132+
133+
$this->assertTrue($request->accepts(ContentType::JSON));
134+
$this->assertTrue($request->accepts(ContentType::HTML));
135+
$this->assertFalse($request->accepts(ContentType::XML));
136+
}
137+
138+
public function test_accepts_with_wildcard_subtype(): void
139+
{
140+
$request = new GenericRequest(
141+
method: Method::GET,
142+
uri: '/',
143+
headers: [
144+
'Accept' => 'application/*',
145+
],
146+
);
147+
148+
$this->assertTrue($request->accepts(ContentType::JSON));
149+
$this->assertFalse($request->accepts(ContentType::HTML));
150+
$this->assertTrue($request->accepts(ContentType::XML));
151+
}
152+
153+
public function test_accepts_can_handle_priorities(): void
154+
{
155+
$request = new GenericRequest(
156+
method: Method::GET,
157+
uri: '/',
158+
headers: [
159+
'Accept' => 'text/html, application/xhtml+xml;q=0.8, application/xml;q=0.8, image/webp',
160+
],
161+
);
162+
163+
$this->assertTrue($request->accepts(ContentType::XHTML));
164+
$this->assertTrue($request->accepts(ContentType::XML));
165+
}
166+
167+
public function test_accepts_returns_true_on_first_match(): void
168+
{
169+
$request = new GenericRequest(
170+
method: Method::GET,
171+
uri: '/',
172+
headers: [
173+
'Accept' => 'application/*, image/avif',
174+
],
175+
);
176+
177+
$this->assertTrue($request->accepts(ContentType::HTML, ContentType::JSON));
178+
$this->assertTrue($request->accepts(ContentType::XML, ContentType::JSON));
179+
$this->assertTrue($request->accepts(ContentType::JSON, ContentType::AVIF));
180+
$this->assertTrue($request->accepts(ContentType::AVIF, ContentType::PNG));
181+
$this->assertFalse($request->accepts(ContentType::HTML, ContentType::PNG));
182+
}
64183
}

0 commit comments

Comments
 (0)