Skip to content

Commit 9954ccf

Browse files
authored
Merge pull request #8194 from kenjis/feat-Message-addHeader
feat: add Message::addHeader() to add header with the same name
2 parents 0153a66 + 5d55cd0 commit 9954ccf

File tree

8 files changed

+168
-11
lines changed

8 files changed

+168
-11
lines changed

system/HTTP/Header.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ public function getName(): string
6464

6565
/**
6666
* Gets the raw value of the header. This may return either a string
67-
* of an array, depending on whether the header has multiple values or not.
67+
* or an array, depending on whether the header has multiple values or not.
6868
*
6969
* @return array<int|string, array<string, string>|string>|string
7070
*/

system/HTTP/Message.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111

1212
namespace CodeIgniter\HTTP;
1313

14+
use InvalidArgumentException;
15+
1416
/**
1517
* An HTTP message
1618
*
@@ -112,6 +114,13 @@ public function hasHeader(string $name): bool
112114
*/
113115
public function getHeaderLine(string $name): string
114116
{
117+
if ($this->hasMultipleHeaders($name)) {
118+
throw new InvalidArgumentException(
119+
'The header "' . $name . '" already has multiple headers.'
120+
. ' You cannot use getHeaderLine().'
121+
);
122+
}
123+
115124
$origName = $this->getHeaderName($name);
116125

117126
if (! array_key_exists($origName, $this->headers)) {

system/HTTP/MessageInterface.php

Lines changed: 2 additions & 2 deletions
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

Lines changed: 63 additions & 5 deletions
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

@@ -93,7 +98,7 @@ public function populateHeaders(): void
9398

9499
$this->setHeader($header, $_SERVER[$key]);
95100

96-
// Add us to the header map so we can find them case-insensitively
101+
// Add us to the header map, so we can find them case-insensitively
97102
$this->headerMap[strtolower($header)] = $header;
98103
}
99104
}
@@ -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,33 @@ 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) && isset($this->headers[$origName])) {
238+
$this->headers[$origName] = [$this->headers[$origName]];
239+
}
240+
241+
// Add the header.
242+
$this->headers[$origName][] = new Header($origName, $value);
243+
244+
return $this;
245+
}
246+
191247
/**
192248
* Adds an additional header value to any headers that accept
193249
* multiple values (i.e. are an array or implement ArrayAccess)
@@ -196,6 +252,8 @@ public function appendHeader(string $name, ?string $value): self
196252
*/
197253
public function prependHeader(string $name, string $value): self
198254
{
255+
$this->checkMultipleHeaders($name);
256+
199257
$origName = $this->getHeaderName($name);
200258

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

tests/system/HTTP/MessageTest.php

Lines changed: 72 additions & 2 deletions
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
@@ -207,7 +208,7 @@ public static function provideArrayHeaderValue(): iterable
207208
/**
208209
* @dataProvider provideArrayHeaderValue
209210
*
210-
* @param mixed $arrayHeaderValue
211+
* @param array $arrayHeaderValue
211212
*/
212213
public function testSetHeaderWithExistingArrayValuesAppendStringValue($arrayHeaderValue): void
213214
{
@@ -220,7 +221,7 @@ public function testSetHeaderWithExistingArrayValuesAppendStringValue($arrayHead
220221
/**
221222
* @dataProvider provideArrayHeaderValue
222223
*
223-
* @param mixed $arrayHeaderValue
224+
* @param array $arrayHeaderValue
224225
*/
225226
public function testSetHeaderWithExistingArrayValuesAppendArrayValue($arrayHeaderValue): void
226227
{
@@ -304,4 +305,73 @@ 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+
}
358+
359+
public function testGetHeaderLineWithMultipleHeaders(): void
360+
{
361+
$this->expectException(InvalidArgumentException::class);
362+
$this->expectExceptionMessage(
363+
'The header "Set-Cookie" already has multiple headers. You cannot use getHeaderLine().'
364+
);
365+
366+
$this->message->addHeader(
367+
'Set-Cookie',
368+
'logged_in=no; Path=/'
369+
);
370+
$this->message->addHeader(
371+
'Set-Cookie',
372+
'sessid=123456; Path=/'
373+
);
374+
375+
$this->message->getHeaderLine('Set-Cookie');
376+
}
307377
}

user_guide_src/source/changelogs/v4.5.0.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,8 @@ Others
299299
usage in your view files, which was supported by CodeIgniter 3.
300300
- **CSP:** Added ``ContentSecurityPolicy::clearDirective()`` method to clear
301301
existing CSP directives. See :ref:`csp-clear-directives`.
302+
- **HTTP:** Added ``Message::addHeader()`` method to add another header with
303+
the same name. See :php:meth:`CodeIgniter\\HTTP\\Message::addHeader()`.
302304

303305
Message Changes
304306
***************

user_guide_src/source/incoming/message.rst

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ requests and responses, including the message body, protocol version, utilities
77
the headers, and methods for handling content negotiation.
88

99
This class is the parent class that both the :doc:`Request Class <../incoming/request>` and the
10-
:doc:`Response Class <../outgoing/response>` extend from.
10+
:doc:`Response Class <../outgoing/response>` extend from, and it is not used directly.
1111

1212
***************
1313
Class Reference
@@ -146,6 +146,20 @@ Class Reference
146146

147147
.. literalinclude:: message/009.php
148148

149+
.. php:method:: addHeader($name, $value)
150+
151+
.. versionadded:: 4.5.0
152+
153+
:param string $name: The name of the header to add.
154+
:param string $value: The value of the header.
155+
:returns: The current message instance
156+
:rtype: CodeIgniter\\HTTP\\Message
157+
158+
Adds a header (not a header value) with the same name.
159+
Use this only when you set multiple headers with the same name,
160+
161+
.. literalinclude:: message/011.php
162+
149163
.. php:method:: getProtocolVersion()
150164
151165
:returns: The current HTTP protocol version
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<?php
2+
3+
$message->addHeader('Set-Cookie', 'logged_in=no; Path=/');
4+
$message->addHeader('Set-Cookie', 'sessid=123456; Path=/');

0 commit comments

Comments
 (0)