Skip to content

Commit 8b2fd6f

Browse files
committed
1 parent 1b0492e commit 8b2fd6f

15 files changed

+968
-0
lines changed

src/CurlClient.php

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
<?php
2+
/**
3+
* Class CurlClient
4+
*
5+
* @filesource CurlClient.php
6+
* @created 21.10.2017
7+
* @package chillerlan\HTTP
8+
* @author Smiley <smiley@chillerlan.net>
9+
* @copyright 2017 Smiley
10+
* @license MIT
11+
*/
12+
13+
namespace chillerlan\HTTP;
14+
15+
use chillerlan\Traits\ContainerInterface;
16+
17+
/**
18+
* @property resource $http
19+
*/
20+
class CurlClient extends HTTPClientAbstract{
21+
22+
/**
23+
* @var \stdClass
24+
*/
25+
protected $responseHeaders;
26+
27+
/** @inheritdoc */
28+
public function __construct(ContainerInterface $options){
29+
parent::__construct($options);
30+
31+
if(!isset($this->options->ca_info) || !is_file($this->options->ca_info)){
32+
throw new HTTPClientException('invalid CA file');
33+
}
34+
35+
$this->http = curl_init();
36+
37+
curl_setopt_array($this->http, $this->options->curl_options);
38+
39+
curl_setopt_array($this->http, [
40+
CURLOPT_HEADER => false,
41+
CURLOPT_RETURNTRANSFER => true,
42+
CURLOPT_PROTOCOLS => CURLPROTO_HTTP|CURLPROTO_HTTPS,
43+
CURLOPT_CAINFO => $this->options->ca_info,
44+
CURLOPT_SSL_VERIFYPEER => true,
45+
CURLOPT_SSL_VERIFYHOST => 2,
46+
CURLOPT_TIMEOUT => 5,
47+
CURLOPT_USERAGENT => $this->options->user_agent,
48+
]);
49+
50+
}
51+
52+
/** @inheritdoc */
53+
public function request(string $url, array $params = null, string $method = null, $body = null, array $headers = null):HTTPResponse{
54+
$this->responseHeaders = new \stdClass;
55+
56+
try{
57+
$parsedURL = parse_url($url);
58+
59+
if(!isset($parsedURL['host']) || $parsedURL['scheme'] !== 'https'){
60+
trigger_error('invalid URL');
61+
}
62+
63+
$method = strtoupper($method ?? 'POST');
64+
$headers = $this->normalizeRequestHeaders($headers ?? []);
65+
$options = [CURLOPT_CUSTOMREQUEST => $method];
66+
67+
if(in_array($method, ['PATCH', 'POST', 'PUT', 'DELETE'], true)){
68+
69+
if($method === 'POST'){
70+
$options = [CURLOPT_POST => true];
71+
72+
if(!isset($headers['Content-type']) && is_array($body)){
73+
$headers += ['Content-type: application/x-www-form-urlencoded'];
74+
$body = http_build_query($body, '', '&', PHP_QUERY_RFC1738);
75+
}
76+
77+
}
78+
79+
$options += [CURLOPT_POSTFIELDS => $body];
80+
}
81+
82+
$headers += [
83+
'Host: '.$parsedURL['host'],
84+
'Connection: close',
85+
];
86+
87+
$params = $params ?? [];
88+
$url = $url.(!empty($params) ? '?'.http_build_query($params) : '');
89+
90+
$options += [
91+
CURLOPT_URL => $url,
92+
CURLOPT_HTTPHEADER => $headers,
93+
CURLOPT_HEADERFUNCTION => [$this, 'headerLine'],
94+
];
95+
96+
curl_setopt_array($this->http, $options);
97+
98+
$response = curl_exec($this->http);
99+
100+
return new HTTPResponse([
101+
'headers' => $this->responseHeaders,
102+
'body' => $response,
103+
]);
104+
105+
}
106+
catch(\Exception $e){
107+
throw new HTTPClientException($e->getMessage());
108+
}
109+
110+
}
111+
112+
/**
113+
* @param resource $curl
114+
* @param string $header_line
115+
*
116+
* @return int
117+
*
118+
* @link http://php.net/manual/function.curl-setopt.php CURLOPT_HEADERFUNCTION
119+
*/
120+
protected function headerLine(/** @noinspection PhpUnusedParameterInspection */$curl, $header_line){
121+
$header = explode(':', $header_line, 2);
122+
123+
if(count($header) === 2){
124+
$this->responseHeaders->{trim(strtolower($header[0]))} = trim($header[1]);
125+
}
126+
elseif(substr($header_line, 0, 4) === 'HTTP'){
127+
$status = explode(' ', $header_line, 3);
128+
129+
$this->responseHeaders->httpversion = explode('/', $status[0], 2)[1];
130+
$this->responseHeaders->statuscode = intval($status[1]);
131+
$this->responseHeaders->statustext = trim($status[2]);
132+
}
133+
134+
return strlen($header_line);
135+
}
136+
137+
}

src/GuzzleClient.php

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
<?php
2+
/**
3+
* Class GuzzleClient
4+
*
5+
* @filesource GuzzleClient.php
6+
* @created 23.10.2017
7+
* @package chillerlan\HTTP
8+
* @author Smiley <smiley@chillerlan.net>
9+
* @copyright 2017 Smiley
10+
* @license MIT
11+
*/
12+
13+
namespace chillerlan\HTTP;
14+
15+
use chillerlan\Traits\ContainerInterface;
16+
use GuzzleHttp\Client;
17+
use Psr\Http\Message\StreamInterface;
18+
19+
/**
20+
* @property \GuzzleHttp\Client $http
21+
*/
22+
class GuzzleClient extends HTTPClientAbstract{
23+
24+
/**
25+
* GuzzleClient constructor.
26+
*
27+
* @param \chillerlan\Traits\ContainerInterface|null $options
28+
* @param \GuzzleHttp\Client|null $http
29+
*/
30+
public function __construct(ContainerInterface $options = null, Client $http = null){
31+
parent::__construct($options);
32+
33+
$this->setClient($http);
34+
}
35+
36+
/** @inheritdoc */
37+
public function setClient(Client $http):HTTPClientInterface{
38+
$this->http = $http;
39+
40+
return $this;
41+
}
42+
43+
/** @inheritdoc */
44+
public function request(string $url, array $params = null, string $method = null, $body = null, array $headers = null):HTTPResponse{
45+
46+
try{
47+
$parsedURL = parse_url($url);
48+
$method = strtoupper($method ?? 'POST');
49+
50+
if(!isset($parsedURL['host']) || $parsedURL['scheme'] !== 'https'){
51+
trigger_error('invalid URL');
52+
}
53+
54+
// @link http://docs.guzzlephp.org/en/stable/request-options.html
55+
$options = [
56+
'query' => $params ?? [],
57+
'headers' => $headers ?? [],
58+
'http_errors' => false, // no exceptions on HTTP errors plz
59+
];
60+
61+
if(in_array($method, ['PATCH', 'POST', 'PUT', 'DELETE'], true)){
62+
63+
if(is_scalar($body) || $body instanceof StreamInterface){
64+
$options['body'] = $body; // @codeCoverageIgnore
65+
}
66+
elseif(in_array($method, ['PATCH', 'POST', 'PUT'], true) && is_array($body)){
67+
$options['form_params'] = $body;
68+
}
69+
70+
}
71+
72+
$response = $this->http->request($method, explode('?', $url)[0], $options);
73+
74+
$responseHeaders = $this->parseResponseHeaders($response->getHeaders());
75+
$responseHeaders->statuscode = $response->getStatusCode();
76+
$responseHeaders->statustext = $response->getReasonPhrase();
77+
$responseHeaders->httpversion = $response->getProtocolVersion();
78+
79+
return new HTTPResponse([
80+
'headers' => $responseHeaders,
81+
'body' => $response->getBody(),
82+
]);
83+
84+
}
85+
catch(\Exception $e){
86+
throw new HTTPClientException('fetch error: '.$e->getMessage());
87+
}
88+
89+
}
90+
91+
/**
92+
* @param array $headers
93+
*
94+
* @return \stdClass
95+
*/
96+
protected function parseResponseHeaders(array $headers):\stdClass {
97+
$h = new \stdClass;
98+
99+
foreach($headers as $k => $v){
100+
$h->{strtolower($k)} = $v[0] ?? null;
101+
}
102+
103+
return $h;
104+
}
105+
106+
}

src/HTTPClientAbstract.php

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<?php
2+
/**
3+
* Class HTTPClientAbstract
4+
*
5+
* @filesource HTTPClientAbstract.php
6+
* @created 09.07.2017
7+
* @package chillerlan\HTTP
8+
* @author Smiley <smiley@chillerlan.net>
9+
* @copyright 2017 Smiley
10+
* @license MIT
11+
*/
12+
13+
namespace chillerlan\HTTP;
14+
15+
use chillerlan\Traits\ContainerInterface;
16+
17+
abstract class HTTPClientAbstract implements HTTPClientInterface{
18+
19+
/**
20+
* @var mixed
21+
*/
22+
protected $http;
23+
24+
/**
25+
* @var \chillerlan\Traits\ContainerInterface|mixed
26+
*/
27+
protected $options;
28+
29+
/** @inheritdoc */
30+
public function __construct(ContainerInterface $options){
31+
$this->options = $options;
32+
}
33+
34+
/** @inheritdoc */
35+
public function normalizeRequestHeaders(array $headers):array {
36+
$normalized_headers = [];
37+
38+
foreach($headers as $key => $val){
39+
40+
if(is_numeric($key)){
41+
$header = explode(':', $val, 2);
42+
43+
if(count($header) !== 2){
44+
continue;
45+
}
46+
47+
$key = $header[0];
48+
$val = $header[1];
49+
}
50+
51+
$key = ucfirst(strtolower($key));
52+
53+
$normalized_headers[$key] = trim($key).': '.trim($val);
54+
}
55+
56+
return $normalized_headers;
57+
}
58+
59+
60+
}

src/HTTPClientException.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
/**
3+
* Class HTTPClientException
4+
*
5+
* @filesource HTTPClientException.php
6+
* @created 30.12.2017
7+
* @package chillerlan\HTTP
8+
* @author Smiley <smiley@chillerlan.net>
9+
* @copyright 2017 Smiley
10+
* @license MIT
11+
*/
12+
13+
namespace chillerlan\HTTP;
14+
15+
class HTTPClientException extends \Exception{}

src/HTTPClientInterface.php

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
/**
3+
* Interface HTTPClientInterface
4+
*
5+
* @filesource HTTPClientInterface.php
6+
* @created 09.07.2017
7+
* @package chillerlan\HTTP
8+
* @author Smiley <smiley@chillerlan.net>
9+
* @copyright 2017 Smiley
10+
* @license MIT
11+
*/
12+
13+
namespace chillerlan\HTTP;
14+
15+
use chillerlan\Traits\ContainerInterface;
16+
17+
interface HTTPClientInterface{
18+
19+
/**
20+
* HTTPClientInterface constructor.
21+
*
22+
* @param \chillerlan\Traits\ContainerInterface $options
23+
*/
24+
public function __construct(ContainerInterface $options);
25+
26+
/**
27+
* @param string $url
28+
* @param array $params
29+
* @param string $method
30+
* @param mixed $body
31+
* @param array $headers
32+
*
33+
* @return \chillerlan\HTTP\HTTPResponse
34+
* @throws \chillerlan\HTTP\HTTPClientException
35+
*/
36+
public function request(string $url, array $params = null, string $method = null, $body = null, array $headers = null):HTTPResponse;
37+
38+
/**
39+
* @param array $headers
40+
*
41+
* @return array
42+
*/
43+
public function normalizeRequestHeaders(array $headers):array;
44+
45+
}

0 commit comments

Comments
 (0)