Merge pull request #8194 from kenjis/feat-Message-addHeader

feat: add Message::addHeader() to add header with the same name
This commit is contained in:
kenjis 2023-11-21 06:22:50 +09:00 committed by GitHub
commit 9954ccf67a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 168 additions and 11 deletions

View File

@ -64,7 +64,7 @@ class Header
/** /**
* Gets the raw value of the header. This may return either a string * Gets the raw value of the header. This may return either a string
* of an array, depending on whether the header has multiple values or not. * or an array, depending on whether the header has multiple values or not.
* *
* @return array<int|string, array<string, string>|string>|string * @return array<int|string, array<string, string>|string>|string
*/ */

View File

@ -11,6 +11,8 @@
namespace CodeIgniter\HTTP; namespace CodeIgniter\HTTP;
use InvalidArgumentException;
/** /**
* An HTTP message * An HTTP message
* *
@ -112,6 +114,13 @@ class Message implements MessageInterface
*/ */
public function getHeaderLine(string $name): string public function getHeaderLine(string $name): string
{ {
if ($this->hasMultipleHeaders($name)) {
throw new InvalidArgumentException(
'The header "' . $name . '" already has multiple headers.'
. ' You cannot use getHeaderLine().'
);
}
$origName = $this->getHeaderName($name); $origName = $this->getHeaderName($name);
if (! array_key_exists($origName, $this->headers)) { if (! array_key_exists($origName, $this->headers)) {

View File

@ -62,7 +62,7 @@ interface MessageInterface
/** /**
* Returns an array containing all Headers. * Returns an array containing all Headers.
* *
* @return array<string, Header> An array of the Header objects * @return array<string, Header|list<Header>> An array of the Header objects
*/ */
public function headers(): array; public function headers(): array;
@ -83,7 +83,7 @@ interface MessageInterface
* *
* @param string $name * @param string $name
* *
* @return array|Header|null * @return Header|list<Header>|null
*/ */
public function header($name); public function header($name);

View File

@ -12,6 +12,7 @@
namespace CodeIgniter\HTTP; namespace CodeIgniter\HTTP;
use CodeIgniter\HTTP\Exceptions\HTTPException; use CodeIgniter\HTTP\Exceptions\HTTPException;
use InvalidArgumentException;
/** /**
* Message Trait * Message Trait
@ -25,7 +26,11 @@ trait MessageTrait
/** /**
* List of all HTTP request headers. * List of all HTTP request headers.
* *
* @var array<string, Header> * [name => Header]
* or
* [name => [Header1, Header2]]
*
* @var array<string, Header|list<Header>>
*/ */
protected $headers = []; protected $headers = [];
@ -93,7 +98,7 @@ trait MessageTrait
$this->setHeader($header, $_SERVER[$key]); $this->setHeader($header, $_SERVER[$key]);
// Add us to the header map so we can find them case-insensitively // Add us to the header map, so we can find them case-insensitively
$this->headerMap[strtolower($header)] = $header; $this->headerMap[strtolower($header)] = $header;
} }
} }
@ -102,7 +107,7 @@ trait MessageTrait
/** /**
* Returns an array containing all Headers. * Returns an array containing all Headers.
* *
* @return array<string, Header> An array of the Header objects * @return array<string, Header|list<Header>> An array of the Header objects
*/ */
public function headers(): array public function headers(): array
{ {
@ -122,7 +127,7 @@ trait MessageTrait
* *
* @param string $name * @param string $name
* *
* @return array|Header|null * @return Header|list<Header>|null
*/ */
public function header($name) public function header($name)
{ {
@ -140,9 +145,14 @@ trait MessageTrait
*/ */
public function setHeader(string $name, $value): self public function setHeader(string $name, $value): self
{ {
$this->checkMultipleHeaders($name);
$origName = $this->getHeaderName($name); $origName = $this->getHeaderName($name);
if (isset($this->headers[$origName]) && is_array($this->headers[$origName]->getValue())) { if (
isset($this->headers[$origName])
&& is_array($this->headers[$origName]->getValue())
) {
if (! is_array($value)) { if (! is_array($value)) {
$value = [$value]; $value = [$value];
} }
@ -158,6 +168,23 @@ trait MessageTrait
return $this; return $this;
} }
private function hasMultipleHeaders(string $name): bool
{
$origName = $this->getHeaderName($name);
return isset($this->headers[$origName]) && is_array($this->headers[$origName]);
}
private function checkMultipleHeaders(string $name): void
{
if ($this->hasMultipleHeaders($name)) {
throw new InvalidArgumentException(
'The header "' . $name . '" already has multiple headers.'
. ' You cannot change them. If you really need to change, remove the header first.'
);
}
}
/** /**
* Removes a header from the list of headers we track. * Removes a header from the list of headers we track.
* *
@ -179,6 +206,8 @@ trait MessageTrait
*/ */
public function appendHeader(string $name, ?string $value): self public function appendHeader(string $name, ?string $value): self
{ {
$this->checkMultipleHeaders($name);
$origName = $this->getHeaderName($name); $origName = $this->getHeaderName($name);
array_key_exists($origName, $this->headers) array_key_exists($origName, $this->headers)
@ -188,6 +217,33 @@ trait MessageTrait
return $this; return $this;
} }
/**
* Adds a header (not a header value) with the same name.
* Use this only when you set multiple headers with the same name,
* typically, for `Set-Cookie`.
*
* @return $this
*/
public function addHeader(string $name, string $value): static
{
$origName = $this->getHeaderName($name);
if (! isset($this->headers[$origName])) {
$this->setHeader($name, $value);
return $this;
}
if (! $this->hasMultipleHeaders($name) && isset($this->headers[$origName])) {
$this->headers[$origName] = [$this->headers[$origName]];
}
// Add the header.
$this->headers[$origName][] = new Header($origName, $value);
return $this;
}
/** /**
* Adds an additional header value to any headers that accept * Adds an additional header value to any headers that accept
* multiple values (i.e. are an array or implement ArrayAccess) * multiple values (i.e. are an array or implement ArrayAccess)
@ -196,6 +252,8 @@ trait MessageTrait
*/ */
public function prependHeader(string $name, string $value): self public function prependHeader(string $name, string $value): self
{ {
$this->checkMultipleHeaders($name);
$origName = $this->getHeaderName($name); $origName = $this->getHeaderName($name);
$this->headers[$origName]->prependValue($value); $this->headers[$origName]->prependValue($value);

View File

@ -13,6 +13,7 @@ namespace CodeIgniter\HTTP;
use CodeIgniter\HTTP\Exceptions\HTTPException; use CodeIgniter\HTTP\Exceptions\HTTPException;
use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\CIUnitTestCase;
use InvalidArgumentException;
/** /**
* @internal * @internal
@ -207,7 +208,7 @@ final class MessageTest extends CIUnitTestCase
/** /**
* @dataProvider provideArrayHeaderValue * @dataProvider provideArrayHeaderValue
* *
* @param mixed $arrayHeaderValue * @param array $arrayHeaderValue
*/ */
public function testSetHeaderWithExistingArrayValuesAppendStringValue($arrayHeaderValue): void public function testSetHeaderWithExistingArrayValuesAppendStringValue($arrayHeaderValue): void
{ {
@ -220,7 +221,7 @@ final class MessageTest extends CIUnitTestCase
/** /**
* @dataProvider provideArrayHeaderValue * @dataProvider provideArrayHeaderValue
* *
* @param mixed $arrayHeaderValue * @param array $arrayHeaderValue
*/ */
public function testSetHeaderWithExistingArrayValuesAppendArrayValue($arrayHeaderValue): void public function testSetHeaderWithExistingArrayValuesAppendArrayValue($arrayHeaderValue): void
{ {
@ -304,4 +305,73 @@ final class MessageTest extends CIUnitTestCase
$_SERVER = $original; // restore so code coverage doesn't break $_SERVER = $original; // restore so code coverage doesn't break
} }
public function testAddHeaderAddsFirstHeader(): void
{
$this->message->addHeader(
'Set-Cookie',
'logged_in=no; Path=/'
);
$header = $this->message->header('Set-Cookie');
$this->assertInstanceOf(Header::class, $header);
$this->assertSame('logged_in=no; Path=/', $header->getValue());
}
public function testAddHeaderAddsTwoHeaders(): void
{
$this->message->addHeader(
'Set-Cookie',
'logged_in=no; Path=/'
);
$this->message->addHeader(
'Set-Cookie',
'sessid=123456; Path=/'
);
$headers = $this->message->header('Set-Cookie');
$this->assertCount(2, $headers);
$this->assertSame('logged_in=no; Path=/', $headers[0]->getValue());
$this->assertSame('sessid=123456; Path=/', $headers[1]->getValue());
}
public function testAppendHeaderWithMultipleHeaders(): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage(
'The header "Set-Cookie" already has multiple headers. You cannot change them. If you really need to change, remove the header first.'
);
$this->message->addHeader(
'Set-Cookie',
'logged_in=no; Path=/'
);
$this->message->addHeader(
'Set-Cookie',
'sessid=123456; Path=/'
);
$this->message->appendHeader('Set-Cookie', 'HttpOnly');
}
public function testGetHeaderLineWithMultipleHeaders(): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage(
'The header "Set-Cookie" already has multiple headers. You cannot use getHeaderLine().'
);
$this->message->addHeader(
'Set-Cookie',
'logged_in=no; Path=/'
);
$this->message->addHeader(
'Set-Cookie',
'sessid=123456; Path=/'
);
$this->message->getHeaderLine('Set-Cookie');
}
} }

View File

@ -299,6 +299,8 @@ Others
usage in your view files, which was supported by CodeIgniter 3. usage in your view files, which was supported by CodeIgniter 3.
- **CSP:** Added ``ContentSecurityPolicy::clearDirective()`` method to clear - **CSP:** Added ``ContentSecurityPolicy::clearDirective()`` method to clear
existing CSP directives. See :ref:`csp-clear-directives`. existing CSP directives. See :ref:`csp-clear-directives`.
- **HTTP:** Added ``Message::addHeader()`` method to add another header with
the same name. See :php:meth:`CodeIgniter\\HTTP\\Message::addHeader()`.
Message Changes Message Changes
*************** ***************

View File

@ -7,7 +7,7 @@ requests and responses, including the message body, protocol version, utilities
the headers, and methods for handling content negotiation. the headers, and methods for handling content negotiation.
This class is the parent class that both the :doc:`Request Class <../incoming/request>` and the This class is the parent class that both the :doc:`Request Class <../incoming/request>` and the
:doc:`Response Class <../outgoing/response>` extend from. :doc:`Response Class <../outgoing/response>` extend from, and it is not used directly.
*************** ***************
Class Reference Class Reference
@ -146,6 +146,20 @@ Class Reference
.. literalinclude:: message/009.php .. literalinclude:: message/009.php
.. php:method:: addHeader($name, $value)
.. versionadded:: 4.5.0
:param string $name: The name of the header to add.
:param string $value: The value of the header.
:returns: The current message instance
:rtype: CodeIgniter\\HTTP\\Message
Adds a header (not a header value) with the same name.
Use this only when you set multiple headers with the same name,
.. literalinclude:: message/011.php
.. php:method:: getProtocolVersion() .. php:method:: getProtocolVersion()
:returns: The current HTTP protocol version :returns: The current HTTP protocol version

View File

@ -0,0 +1,4 @@
<?php
$message->addHeader('Set-Cookie', 'logged_in=no; Path=/');
$message->addHeader('Set-Cookie', 'sessid=123456; Path=/');