Skip to content

Commit c1995d5

Browse files
committed
:octocat: extract Message realted functions into class MessageUtil
1 parent 729a95e commit c1995d5

File tree

4 files changed

+289
-189
lines changed

4 files changed

+289
-189
lines changed

src/MessageUtil.php

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
<?php
2+
/**
3+
* Class MessageUtil
4+
*
5+
* @created 22.10.2022
6+
* @author smiley <smiley@chillerlan.net>
7+
* @copyright 2022 smiley
8+
* @license MIT
9+
*/
10+
11+
namespace chillerlan\HTTP\Utils;
12+
13+
use Psr\Http\Message\{MessageInterface, RequestInterface, ResponseInterface};
14+
use RuntimeException;
15+
use function extension_loaded, function_exists, gzdecode, gzinflate, gzuncompress, implode, json_decode, json_encode,
16+
simplexml_load_string, strtolower, trim;
17+
18+
/**
19+
*
20+
*/
21+
class MessageUtil{
22+
23+
/**
24+
* @return \stdClass|array|bool
25+
*/
26+
public static function decodeJSON(MessageInterface $message, bool $assoc = null){
27+
$data = json_decode((string)$message->getBody(), $assoc ?? false);
28+
29+
$message->getBody()->rewind();
30+
31+
return $data;
32+
}
33+
34+
/**
35+
* @return \SimpleXMLElement|array|bool
36+
*/
37+
public static function decodeXML(MessageInterface $message, bool $assoc = null){
38+
$data = simplexml_load_string((string)$message->getBody());
39+
40+
$message->getBody()->rewind();
41+
42+
return $assoc === true
43+
? json_decode(json_encode($data), true) // cruel
44+
: $data;
45+
}
46+
47+
/**
48+
* Returns the string representation of an HTTP message. (from Guzzle)
49+
*/
50+
public static function toString(MessageInterface $message, bool $appendBody = true):string{
51+
$msg = '';
52+
53+
if($message instanceof RequestInterface){
54+
$msg = trim($message->getMethod().' '.$message->getRequestTarget()).' HTTP/'.$message->getProtocolVersion();
55+
56+
if(!$message->hasHeader('host')){
57+
$msg .= "\r\nHost: ".$message->getUri()->getHost();
58+
}
59+
60+
}
61+
elseif($message instanceof ResponseInterface){
62+
$msg = 'HTTP/'.$message->getProtocolVersion().' '.$message->getStatusCode().' '.$message->getReasonPhrase();
63+
}
64+
65+
foreach($message->getHeaders() as $name => $values){
66+
$msg .= "\r\n".$name.': '.implode(', ', $values);
67+
}
68+
69+
// appending the body might cause issues in some cases, e.g. with large responses or file streams
70+
if($appendBody){
71+
$data = $message->getBody()->getContents();
72+
$message->getBody()->rewind();
73+
74+
$msg .= "\r\n\r\n".$data;
75+
}
76+
77+
return $msg;
78+
}
79+
80+
/**
81+
* Decompresses the message content according to the Content-Encoding header and returns the decompressed data
82+
*
83+
* @see https://github.com/kjdev/php-ext-brotli
84+
* @see https://github.com/kjdev/php-ext-zstd
85+
* @see https://en.wikipedia.org/wiki/HTTP_compression#Content-Encoding_tokens
86+
*
87+
* @throws \RuntimeException
88+
*/
89+
public static function decompress(MessageInterface $message):string{
90+
$data = $message->getBody()->getContents();
91+
$encoding = strtolower($message->getHeaderLine('content-encoding'));
92+
$message->getBody()->rewind();
93+
94+
if($encoding === '' || $encoding === 'identity'){
95+
return $data;
96+
}
97+
98+
if($encoding === 'gzip' || $encoding === 'x-gzip'){
99+
return gzdecode($data);
100+
}
101+
102+
if($encoding === 'compress'){
103+
return gzuncompress($data);
104+
}
105+
106+
if($encoding === 'deflate'){
107+
return gzinflate($data);
108+
}
109+
110+
if($encoding === 'br'){
111+
112+
if(extension_loaded('brotli') && function_exists('brotli_uncompress')){
113+
/** @phan-suppress-next-line PhanUndeclaredFunction */
114+
return \brotli_uncompress($data); // @codeCoverageIgnore
115+
}
116+
117+
throw new RuntimeException('cannot decompress brotli compressed message body');
118+
}
119+
120+
if($encoding === 'zstd'){
121+
122+
if(extension_loaded('zstd') && function_exists('zstd_uncompress')){
123+
/** @phan-suppress-next-line PhanUndeclaredFunction */
124+
return \zstd_uncompress($data); // @codeCoverageIgnore
125+
}
126+
127+
throw new RuntimeException('cannot decompress zstd compressed message body');
128+
}
129+
130+
throw new RuntimeException('unknown content-encoding value: '.$encoding);
131+
}
132+
133+
}

src/message_helpers.php

Lines changed: 3 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,10 @@
88

99
namespace chillerlan\HTTP\Utils;
1010

11-
use RuntimeException, TypeError;
12-
use Psr\Http\Message\{MessageInterface, RequestInterface, ResponseInterface, UriInterface};
11+
use TypeError;
12+
use Psr\Http\Message\UriInterface;
1313

14-
use function array_filter, array_map, explode, extension_loaded, function_exists, gzdecode, gzinflate, gzuncompress, implode,
15-
is_array, is_scalar, json_decode, json_encode, parse_url, pathinfo, preg_match, preg_replace_callback, rawurldecode,
16-
rawurlencode, simplexml_load_string, strtolower, trim, urlencode;
14+
use function array_filter, array_map, explode, implode, is_array, is_scalar, pathinfo, rawurldecode, rawurlencode, strtolower;
1715

1816
use const PATHINFO_EXTENSION;
1917

@@ -160,90 +158,6 @@ function r_rawurlencode($data){
160158
return rawurlencode((string)$data);
161159
}
162160

163-
/**
164-
* @return \stdClass|array|bool
165-
*/
166-
function get_json(MessageInterface $message, bool $assoc = null){
167-
$data = json_decode((string)$message->getBody(), $assoc ?? false);
168-
169-
$message->getBody()->rewind();
170-
171-
return $data;
172-
}
173-
174-
/**
175-
* @return \SimpleXMLElement|array|bool
176-
*/
177-
function get_xml(MessageInterface $message, bool $assoc = null){
178-
$data = simplexml_load_string((string)$message->getBody());
179-
180-
$message->getBody()->rewind();
181-
182-
return $assoc === true
183-
? json_decode(json_encode($data), true) // cruel
184-
: $data;
185-
}
186-
187-
/**
188-
* Returns the string representation of an HTTP message. (from Guzzle)
189-
*/
190-
function message_to_string(MessageInterface $message):string{
191-
$msg = '';
192-
193-
if($message instanceof RequestInterface){
194-
$msg = trim($message->getMethod().' '.$message->getRequestTarget()).' HTTP/'.$message->getProtocolVersion();
195-
196-
if(!$message->hasHeader('host')){
197-
$msg .= "\r\nHost: ".$message->getUri()->getHost();
198-
}
199-
200-
}
201-
elseif($message instanceof ResponseInterface){
202-
$msg = 'HTTP/'.$message->getProtocolVersion().' '.$message->getStatusCode().' '.$message->getReasonPhrase();
203-
}
204-
205-
foreach($message->getHeaders() as $name => $values){
206-
$msg .= "\r\n".$name.': '.implode(', ', $values);
207-
}
208-
209-
$data = (string)$message->getBody();
210-
$message->getBody()->rewind();
211-
212-
return $msg."\r\n\r\n".$data;
213-
}
214-
215-
/**
216-
* Decompresses the message content according to the Content-Encoding header and returns the decompressed data
217-
*
218-
* @throws \RuntimeException
219-
*/
220-
function decompress_content(MessageInterface $message):string{
221-
$data = (string)$message->getBody();
222-
$encoding = strtolower($message->getHeaderLine('content-encoding'));
223-
$message->getBody()->rewind();
224-
225-
if($encoding === 'br'){
226-
// https://github.com/kjdev/php-ext-brotli
227-
if(extension_loaded('brotli') && function_exists('brotli_uncompress')){
228-
/** @phan-suppress-next-line PhanUndeclaredFunction */
229-
return brotli_uncompress($data); // @codeCoverageIgnore
230-
}
231-
232-
throw new RuntimeException('cannot decompress brotli compressed message body');
233-
}
234-
elseif($encoding === 'compress'){
235-
return gzuncompress($data);
236-
}
237-
elseif($encoding === 'deflate'){
238-
return gzinflate($data);
239-
}
240-
elseif($encoding === 'gzip' || $encoding === 'x-gzip'){
241-
return gzdecode($data);
242-
}
243-
244-
return $data;
245-
}
246-
247161
const URI_DEFAULT_PORTS = [
248162
'http' => 80,
249163
'https' => 443,

tests/MessageHelpersTest.php

Lines changed: 0 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -54,106 +54,6 @@ public function testRawurlencodeTypeErrorException():void{
5454
r_rawurlencode((object)[]);
5555
}
5656

57-
public function testGetJSON():void{
58-
59-
$r = $this->responseFactory->createResponse()->withBody($this->streamFactory->createStream('{"foo":"bar"}'));
60-
61-
$this::assertSame('bar', get_json($r)->foo);
62-
63-
$r->getBody()->rewind();
64-
65-
$this::assertSame('bar', get_json($r, true)['foo']);
66-
}
67-
68-
public function testGetXML():void{
69-
70-
$r = $this->responseFactory
71-
->createResponse()
72-
->withBody($this->streamFactory->createStream('<?xml version="1.0" encoding="UTF-8"?><root><foo>bar</foo></root>'));
73-
74-
$this::assertSame('bar', get_xml($r)->foo->__toString());
75-
76-
$r->getBody()->rewind();
77-
78-
$this::assertSame('bar', get_xml($r, true)['foo']);
79-
}
80-
81-
public function testMessageToString():void{
82-
$body = $this->streamFactory->createStream('testbody');
83-
84-
$request = $this->requestFactory
85-
->createRequest('GET', 'https://localhost/foo')
86-
->withAddedHeader('foo', 'bar')
87-
->withBody($body)
88-
;
89-
90-
$this::assertSame(
91-
'GET /foo HTTP/1.1'."\r\n".'Host: localhost'."\r\n".'foo: bar'."\r\n\r\n".'testbody',
92-
message_to_string($request)
93-
);
94-
95-
$response = $this->responseFactory
96-
->createResponse()
97-
->withAddedHeader('foo', 'bar')
98-
->withBody($body)
99-
;
100-
101-
$this::assertSame(
102-
'HTTP/1.1 200 OK'."\r\n".'foo: bar'."\r\n\r\n".'testbody',
103-
message_to_string($response)
104-
);
105-
}
106-
107-
public function decompressDataProvider():array{
108-
return [
109-
'br' => ['brotli_compress', 'br'],
110-
'compress' => ['gzcompress', 'compress'],
111-
'deflate' => ['gzdeflate', 'deflate'],
112-
'gzip' => ['gzencode', 'gzip'],
113-
'none' => ['', ''],
114-
];
115-
}
116-
117-
/**
118-
* @dataProvider decompressDataProvider
119-
*/
120-
public function testDecompressContent(string $fn, string $encoding):void{
121-
122-
// https://github.com/kjdev/php-ext-brotli
123-
if($encoding === 'br' && (!extension_loaded('brotli') || !function_exists('brotli_compress'))){
124-
$this::markTestSkipped('N/A (ext-brotli not installed)');
125-
}
126-
127-
$data = str_repeat('compressed string ', 100);
128-
$expected = $data;
129-
$response = $this->responseFactory->createResponse();
130-
131-
if($fn){
132-
$data = $fn($data);
133-
$response = $response->withHeader('Content-Encoding', $encoding);
134-
}
135-
136-
$response = $response->withBody($this->streamFactory->createStream($data));
137-
138-
$this::assertSame($expected, decompress_content($response));
139-
}
140-
141-
public function testDecompressContentUnableToDecompressBrotliException():void{
142-
143-
if(extension_loaded('brotli') && function_exists('brotli_uncompress')){
144-
$this::markTestSkipped('N/A (ext-brotli isntalled)');
145-
}
146-
147-
$this->expectException(RuntimeException::class);
148-
$this->expectExceptionMessage('cannot decompress brotli compressed message body');
149-
150-
$response = $this->responseFactory
151-
->createResponse()
152-
->withHeader('Content-Encoding', 'br');
153-
154-
decompress_content($response);
155-
}
156-
15757
public function testUriIsAbsolute():void{
15858
$this::assertTrue(uriIsAbsolute($this->uriFactory->createUri('http://example.org')));
15959
$this::assertFalse(uriIsAbsolute($this->uriFactory->createUri('//example.org')));

0 commit comments

Comments
 (0)