Skip to content

Commit 2713821

Browse files
committed
🛀 extract header normalize
1 parent f9fd033 commit 2713821

File tree

5 files changed

+206
-153
lines changed

5 files changed

+206
-153
lines changed

src/Psr7/Header.php

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
<?php
2+
/**
3+
* Class Header
4+
*
5+
* @created 28.03.2021
6+
* @author smiley <smiley@chillerlan.net>
7+
* @copyright 2021 smiley
8+
* @license MIT
9+
*/
10+
11+
namespace chillerlan\HTTP\Psr7;
12+
13+
use function array_keys;
14+
use function array_map;
15+
use function array_values;
16+
use function count;
17+
use function explode;
18+
use function implode;
19+
use function is_array;
20+
use function is_numeric;
21+
use function is_string;
22+
use function strtolower;
23+
use function trim;
24+
use function ucfirst;
25+
26+
/**
27+
*
28+
*/
29+
class Header{
30+
31+
/**
32+
* Normalizes an array of header lines to format ["Name" => "Value (, Value2, Value3, ...)", ...]
33+
* An exception is being made for Set-Cookie, which holds an array of values for each cookie.
34+
* For multiple cookies with the same name, only the last value will be kept.
35+
*
36+
* @param array $headers
37+
*
38+
* @return array
39+
*/
40+
public static function normalize(array $headers):array{
41+
$normalized = [];
42+
43+
foreach($headers as $key => $val){
44+
45+
// the key is numeric, so $val is either a string or an array
46+
if(is_numeric($key)){
47+
48+
// "key: val"
49+
if(is_string($val)){
50+
$header = explode(':', $val, 2);
51+
52+
if(count($header) !== 2){
53+
continue;
54+
}
55+
56+
$key = $header[0];
57+
$val = $header[1];
58+
}
59+
// [$key, $val], ["key" => $key, "val" => $val]
60+
elseif(is_array($val)){
61+
$key = array_keys($val)[0];
62+
$val = array_values($val)[0];
63+
}
64+
else{
65+
continue;
66+
}
67+
}
68+
// the key is named, so we assume $val holds the header values only, either as string or array
69+
else{
70+
if(is_array($val)){
71+
$val = implode(', ', array_values($val));
72+
}
73+
}
74+
75+
$key = implode('-', array_map(fn(string $v):string => ucfirst(strtolower(trim($v))), explode('-', $key)));
76+
$val = trim($val);
77+
78+
// skip if the header already exists but the current value is empty
79+
if(isset($normalized[$key]) && empty($val)){
80+
continue;
81+
}
82+
83+
// cookie headers may appear multiple times
84+
// https://tools.ietf.org/html/rfc6265#section-4.1.2
85+
if($key === 'Set-Cookie'){
86+
// i'll just collect the last value here and leave parsing up to you :P
87+
$normalized[$key][strtolower(explode('=', $val, 2)[0])] = $val;
88+
}
89+
// combine header fields with the same name
90+
// https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2
91+
else{
92+
isset($normalized[$key]) && !empty($normalized[$key])
93+
? $normalized[$key] .= ', '.$val
94+
: $normalized[$key] = $val;
95+
}
96+
}
97+
98+
return $normalized;
99+
}
100+
101+
102+
}

src/Psr7/Message.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ abstract class Message implements MessageInterface{
3939
* @param string|null $version
4040
*/
4141
public function __construct(array $headers = null, $body = null, string $version = null){
42-
$this->setHeaders(normalize_message_headers($headers ?? []));
42+
$this->setHeaders(Header::normalize($headers ?? []));
4343

4444
$this->version = $version ?? '1.1';
4545
$this->streamFactory = new StreamFactory;

src/Psr7/message_helpers.php

Lines changed: 3 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,9 @@
1111
use InvalidArgumentException, TypeError;
1212
use Psr\Http\Message\{MessageInterface, RequestInterface, ResponseInterface, UploadedFileInterface, UriInterface};
1313

14-
use function array_filter, array_keys, array_map, array_values, count, explode,
15-
gzdecode, gzinflate, gzuncompress, implode, is_array, is_numeric, is_scalar, is_string,
16-
json_decode, json_encode, parse_url, preg_match, preg_replace_callback, rawurldecode, rawurlencode,
17-
simplexml_load_string, strtolower, trim, ucfirst, urlencode;
14+
use function array_filter, array_keys, array_map, explode, gzdecode, gzinflate, gzuncompress, implode,
15+
is_array, is_scalar, json_decode, json_encode, parse_url, preg_match, preg_replace_callback, rawurldecode,
16+
rawurlencode, simplexml_load_string, trim, urlencode;
1817

1918
const PSR7_INCLUDES = true;
2019

@@ -124,76 +123,6 @@
124123
'zip' => 'application/zip',
125124
];
126125

127-
/**
128-
* Normalizes an array of header lines to format ["Name" => "Value (, Value2, Value3, ...)", ...]
129-
* An exception is being made for Set-Cookie, which holds an array of values for each cookie.
130-
* For multiple cookies with the same name, only the last value will be kept.
131-
*
132-
* @param array $headers
133-
*
134-
* @return array
135-
*/
136-
function normalize_message_headers(array $headers):array{
137-
$normalized_headers = [];
138-
139-
foreach($headers as $key => $val){
140-
141-
// the key is numeric, so $val is either a string or an array
142-
if(is_numeric($key)){
143-
144-
// "key: val"
145-
if(is_string($val)){
146-
$header = explode(':', $val, 2);
147-
148-
if(count($header) !== 2){
149-
continue;
150-
}
151-
152-
$key = $header[0];
153-
$val = $header[1];
154-
}
155-
// [$key, $val], ["key" => $key, "val" => $val]
156-
elseif(is_array($val)){
157-
$key = array_keys($val)[0];
158-
$val = array_values($val)[0];
159-
}
160-
else{
161-
continue;
162-
}
163-
}
164-
// the key is named, so we assume $val holds the header values only, either as string or array
165-
else{
166-
if(is_array($val)){
167-
$val = implode(', ', array_values($val));
168-
}
169-
}
170-
171-
$key = implode('-', array_map(fn(string $v):string => ucfirst(strtolower(trim($v))), explode('-', $key)));
172-
$val = trim($val);
173-
174-
// skip if the header already exists but the current value is empty
175-
if(isset($normalized_headers[$key]) && empty($val)){
176-
continue;
177-
}
178-
179-
// cookie headers may appear multiple times
180-
// https://tools.ietf.org/html/rfc6265#section-4.1.2
181-
if($key === 'Set-Cookie'){
182-
// i'll just collect the last value here and leave parsing up to you :P
183-
$normalized_headers[$key][strtolower(explode('=', $val, 2)[0])] = $val;
184-
}
185-
// combine header fields with the same name
186-
// https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2
187-
else{
188-
isset($normalized_headers[$key]) && !empty($normalized_headers[$key])
189-
? $normalized_headers[$key] .= ', '.$val
190-
: $normalized_headers[$key] = $val;
191-
}
192-
}
193-
194-
return $normalized_headers;
195-
}
196-
197126
/**
198127
* @param string|string[] $data
199128
*

tests/Psr7/HeaderTest.php

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
<?php
2+
/**
3+
* Class HeaderTest
4+
*
5+
* @created 28.03.2021
6+
* @author smiley <smiley@chillerlan.net>
7+
* @copyright 2021 smiley
8+
* @license MIT
9+
*/
10+
11+
namespace chillerlan\HTTPTest\Psr7;
12+
13+
use chillerlan\HTTP\Psr7\{Header, Response};
14+
use PHPUnit\Framework\TestCase;
15+
16+
/**
17+
*
18+
*/
19+
class HeaderTest extends TestCase{
20+
21+
public function headerDataProvider():array{
22+
return [
23+
'content-Type' => [['Content-Type' => 'application/x-www-form-urlencoded'], ['Content-Type' => 'application/x-www-form-urlencoded']],
24+
'lowercasekey' => [['lowercasekey' => 'lowercasevalue'], ['Lowercasekey' => 'lowercasevalue']],
25+
'UPPERCASEKEY' => [['UPPERCASEKEY' => 'UPPERCASEVALUE'], ['Uppercasekey' => 'UPPERCASEVALUE']],
26+
'mIxEdCaSeKey' => [['mIxEdCaSeKey' => 'MiXeDcAsEvAlUe'], ['Mixedcasekey' => 'MiXeDcAsEvAlUe']],
27+
'31i71casekey' => [['31i71casekey' => '31i71casevalue'], ['31i71casekey' => '31i71casevalue']],
28+
'numericvalue' => [['numericvalue:1'], ['Numericvalue' => '1']],
29+
'numericvalue2' => [['numericvalue' => 2], ['Numericvalue' => '2']],
30+
'keyvaluearray' => [[['foo' => 'bar']], ['Foo' => 'bar']],
31+
'arrayvalue' => [['foo' => ['bar', 'baz']], ['Foo' => 'bar, baz']],
32+
'invalid: 2' => [[2 => 2], []],
33+
'invalid: what' => [['what'], []],
34+
];
35+
}
36+
37+
/**
38+
* @dataProvider headerDataProvider
39+
*
40+
* @param array $headers
41+
* @param array $normalized
42+
*/
43+
public function testNormalizeHeaders(array $headers, array $normalized):void{
44+
$this::assertSame($normalized, Header::normalize($headers));
45+
}
46+
47+
public function testCombineHeaderFields():void{
48+
49+
$headers = [
50+
'accept:',
51+
'Accept: foo',
52+
'accept' => 'bar',
53+
'x-Whatever :nope',
54+
'X-whatever' => '',
55+
'x-foo' => 'bar',
56+
'x - fOO: baz ',
57+
' x-foo ' => ['what', 'nope'],
58+
];
59+
60+
$this::assertSame([
61+
'Accept' => 'foo, bar',
62+
'X-Whatever' => 'nope',
63+
'X-Foo' => 'bar, baz, what, nope'
64+
], Header::normalize($headers));
65+
66+
$r = new Response;
67+
68+
foreach(Header::normalize($headers) as $k => $v){
69+
$r = $r->withAddedHeader($k, $v);
70+
}
71+
72+
$this::assertSame( [
73+
'Accept' => ['foo, bar'],
74+
'X-Whatever' => ['nope'],
75+
'X-Foo' => ['bar, baz, what, nope']
76+
], $r->getHeaders());
77+
78+
}
79+
80+
public function testCombinedCookieHeaders():void{
81+
82+
$headers = [
83+
'Set-Cookie: foo=bar',
84+
'Set-Cookie: foo=baz',
85+
'Set-Cookie: whatever=nope; HttpOnly',
86+
];
87+
88+
$this::assertSame([
89+
'Set-Cookie' => [
90+
'foo' => 'foo=baz',
91+
'whatever' => 'whatever=nope; HttpOnly'
92+
]
93+
], Header::normalize($headers));
94+
}
95+
96+
97+
}

0 commit comments

Comments
 (0)