Skip to content

Commit c60bb05

Browse files
committed
feat: add Message::addHeader()
1 parent b5c1f2b commit c60bb05

File tree

3 files changed

+117
-6
lines changed

3 files changed

+117
-6
lines changed

system/HTTP/MessageInterface.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ public function populateHeaders(): void;
6262
/**
6363
* Returns an array containing all Headers.
6464
*
65-
* @return array<string, Header> An array of the Header objects
65+
* @return array<string, Header|list<Header>> An array of the Header objects
6666
*/
6767
public function headers(): array;
6868

@@ -83,7 +83,7 @@ public function hasHeader(string $name): bool;
8383
*
8484
* @param string $name
8585
*
86-
* @return array|Header|null
86+
* @return Header|list<Header>|null
8787
*/
8888
public function header($name);
8989

system/HTTP/MessageTrait.php

+64-4
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
namespace CodeIgniter\HTTP;
1313

1414
use CodeIgniter\HTTP\Exceptions\HTTPException;
15+
use InvalidArgumentException;
1516

1617
/**
1718
* Message Trait
@@ -25,7 +26,11 @@ trait MessageTrait
2526
/**
2627
* List of all HTTP request headers.
2728
*
28-
* @var array<string, Header>
29+
* [name => Header]
30+
* or
31+
* [name => [Header1, Header2]]
32+
*
33+
* @var array<string, Header|list<Header>>
2934
*/
3035
protected $headers = [];
3136

@@ -102,7 +107,7 @@ public function populateHeaders(): void
102107
/**
103108
* Returns an array containing all Headers.
104109
*
105-
* @return array<string, Header> An array of the Header objects
110+
* @return array<string, Header|list<Header>> An array of the Header objects
106111
*/
107112
public function headers(): array
108113
{
@@ -122,7 +127,7 @@ public function headers(): array
122127
*
123128
* @param string $name
124129
*
125-
* @return array|Header|null
130+
* @return Header|list<Header>|null
126131
*/
127132
public function header($name)
128133
{
@@ -140,9 +145,14 @@ public function header($name)
140145
*/
141146
public function setHeader(string $name, $value): self
142147
{
148+
$this->checkMultipleHeaders($name);
149+
143150
$origName = $this->getHeaderName($name);
144151

145-
if (isset($this->headers[$origName]) && is_array($this->headers[$origName]->getValue())) {
152+
if (
153+
isset($this->headers[$origName])
154+
&& is_array($this->headers[$origName]->getValue())
155+
) {
146156
if (! is_array($value)) {
147157
$value = [$value];
148158
}
@@ -158,6 +168,23 @@ public function setHeader(string $name, $value): self
158168
return $this;
159169
}
160170

171+
private function hasMultipleHeaders(string $name): bool
172+
{
173+
$origName = $this->getHeaderName($name);
174+
175+
return isset($this->headers[$origName]) && is_array($this->headers[$origName]);
176+
}
177+
178+
private function checkMultipleHeaders(string $name): void
179+
{
180+
if ($this->hasMultipleHeaders($name)) {
181+
throw new InvalidArgumentException(
182+
'The header "' . $name . '" already has multiple headers.'
183+
. ' You cannot change them. If you really need to change, remove the header first.'
184+
);
185+
}
186+
}
187+
161188
/**
162189
* Removes a header from the list of headers we track.
163190
*
@@ -179,6 +206,8 @@ public function removeHeader(string $name): self
179206
*/
180207
public function appendHeader(string $name, ?string $value): self
181208
{
209+
$this->checkMultipleHeaders($name);
210+
182211
$origName = $this->getHeaderName($name);
183212

184213
array_key_exists($origName, $this->headers)
@@ -188,6 +217,35 @@ public function appendHeader(string $name, ?string $value): self
188217
return $this;
189218
}
190219

220+
/**
221+
* Adds a header (not a header value) with the same name.
222+
* Use this only when you set multiple headers with the same name,
223+
* typically, for `Set-Cookie`.
224+
*
225+
* @return $this
226+
*/
227+
public function addHeader(string $name, string $value): static
228+
{
229+
$origName = $this->getHeaderName($name);
230+
231+
if (! isset($this->headers[$origName])) {
232+
$this->setHeader($name, $value);
233+
234+
return $this;
235+
}
236+
237+
if (! $this->hasMultipleHeaders($name)) {
238+
if (isset($this->headers[$origName])) {
239+
$this->headers[$origName] = [$this->headers[$origName]];
240+
}
241+
}
242+
243+
// Add the header.
244+
$this->headers[$origName][] = new Header($origName, $value);
245+
246+
return $this;
247+
}
248+
191249
/**
192250
* Adds an additional header value to any headers that accept
193251
* multiple values (i.e. are an array or implement ArrayAccess)
@@ -196,6 +254,8 @@ public function appendHeader(string $name, ?string $value): self
196254
*/
197255
public function prependHeader(string $name, string $value): self
198256
{
257+
$this->checkMultipleHeaders($name);
258+
199259
$origName = $this->getHeaderName($name);
200260

201261
$this->headers[$origName]->prependValue($value);

tests/system/HTTP/MessageTest.php

+51
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
use CodeIgniter\HTTP\Exceptions\HTTPException;
1515
use CodeIgniter\Test\CIUnitTestCase;
16+
use InvalidArgumentException;
1617

1718
/**
1819
* @internal
@@ -304,4 +305,54 @@ public function testPopulateHeaders(): void
304305

305306
$_SERVER = $original; // restore so code coverage doesn't break
306307
}
308+
309+
public function testAddHeaderAddsFirstHeader(): void
310+
{
311+
$this->message->addHeader(
312+
'Set-Cookie',
313+
'logged_in=no; Path=/'
314+
);
315+
316+
$header = $this->message->header('Set-Cookie');
317+
318+
$this->assertInstanceOf(Header::class, $header);
319+
$this->assertSame('logged_in=no; Path=/', $header->getValue());
320+
}
321+
322+
public function testAddHeaderAddsTwoHeaders(): void
323+
{
324+
$this->message->addHeader(
325+
'Set-Cookie',
326+
'logged_in=no; Path=/'
327+
);
328+
$this->message->addHeader(
329+
'Set-Cookie',
330+
'sessid=123456; Path=/'
331+
);
332+
333+
$headers = $this->message->header('Set-Cookie');
334+
335+
$this->assertCount(2, $headers);
336+
$this->assertSame('logged_in=no; Path=/', $headers[0]->getValue());
337+
$this->assertSame('sessid=123456; Path=/', $headers[1]->getValue());
338+
}
339+
340+
public function testAppendHeaderWithMultipleHeaders(): void
341+
{
342+
$this->expectException(InvalidArgumentException::class);
343+
$this->expectExceptionMessage(
344+
'The header "Set-Cookie" already has multiple headers. You cannot change them. If you really need to change, remove the header first.'
345+
);
346+
347+
$this->message->addHeader(
348+
'Set-Cookie',
349+
'logged_in=no; Path=/'
350+
);
351+
$this->message->addHeader(
352+
'Set-Cookie',
353+
'sessid=123456; Path=/'
354+
);
355+
356+
$this->message->appendHeader('Set-Cookie', 'HttpOnly');
357+
}
307358
}

0 commit comments

Comments
 (0)