Skip to content

Commit 1fc689b

Browse files
Merge pull request #1 from InitPHP/2.0
v2.0
2 parents 8ad1763 + 1cc565e commit 1fc689b

32 files changed

+3459
-1962
lines changed

README.md

Lines changed: 68 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ This library provides HTTP Message and HTTP Factory solution following PSR-7 and
99
- PHP 7.4 or higher
1010
- PSR-7 HTTP Message Interfaces
1111
- PSR-17 HTTP Factories Interfaces
12+
- PSR-18 HTTP Client Interfaces
1213

1314
## Installation
1415

@@ -18,20 +19,83 @@ composer require initphp/http
1819

1920
## Usage
2021

21-
It adheres to the PSR-7 and PSR-17 standards and strictly implements these interfaces to a large extent.
22+
It adheres to the PSR-7, PSR-17, PSR-18 standards and strictly implements these interfaces to a large extent.
2223

23-
### Emitter Usage
24+
### PSR-7 Emitter Usage
2425

2526
```php
26-
use \InitPHP\HTTP\{Response, Emitter, Stream};
27+
use \InitPHP\HTTP\Message\{Response, Stream};
28+
use \InitPHP\HTTP\Emitter\Emitter;
2729

2830

2931
$response = new Response(200, [], new Stream('Hello World', null), '1.1');
3032

31-
$emitter = new Emitter;
33+
$emitter = new Emitter();
3234
$emitter->emit($response);
3335
```
3436

37+
or
38+
39+
```php
40+
use \InitPHP\HTTP\Facade\Factory;
41+
use \InitPHP\HTTP\Facade\Emitter;
42+
43+
$response = Factory::createResponse(200);
44+
$response->getBody()->write('Hello World');
45+
46+
Emitter::emit($response);
47+
```
48+
49+
### PSR-17 Factory Usage
50+
51+
```php
52+
use \InitPHP\HTTP\Factory\Factory;
53+
54+
$httpFactory = new Factory();
55+
56+
/** @var \Psr\Http\Message\RequestInterface $request */
57+
$request = $httpFactory->createRequest('GET', 'http://example.com');
58+
59+
// ...
60+
```
61+
62+
or
63+
64+
```php
65+
use InitPHP\HTTP\Facade\Factory;
66+
67+
/** @var \Psr\Http\Message\RequestInterface $request */
68+
$request = Factory::createRequest('GET', 'http://example.com');
69+
```
70+
71+
### PSR-18 Client Usage
72+
73+
```php
74+
use \InitPHP\HTTP\Message\Request;
75+
use \InitPHP\HTTP\Client\Client;
76+
77+
$request = new Request('GET', 'http://example.com');
78+
79+
$client = new Client();
80+
81+
/** @var \Psr\Http\Message\ResponseInterface $response */
82+
$response = $client->sendRequest($request);
83+
```
84+
85+
or
86+
87+
```php
88+
use \InitPHP\HTTP\Facade\Factory;
89+
use \InitPHP\HTTP\Facade\Client;
90+
91+
$request = Factory::createRequest('GET', 'http://example.com');
92+
93+
/** @var \Psr\Http\Message\ResponseInterface $response */
94+
$response = Client::sendRequest($request);
95+
```
96+
97+
98+
3599
#### A Small Difference For PSR-7 Stream
36100

37101
If you are working with small content; The PSR-7 Stream interface may be cumbersome for you. This is because the PSR-7 stream interface writes the content "`php://temp`" or "`php://memory`". By default this library will also overwrite `php://temp` with your content. To change this behavior, this must be declared as the second parameter to the constructor method when creating the Stream object.

composer.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "initphp/http",
3-
"description": "InitPHP PSR-7 HTTP Message Library",
3+
"description": "HTTP",
44
"type": "library",
55
"license": "MIT",
66
"autoload": {
@@ -19,7 +19,9 @@
1919
"minimum-stability": "stable",
2020
"require": {
2121
"php": ">=7.4",
22+
"ext-json": "*",
2223
"psr/http-message": "^1.0",
23-
"psr/http-factory": "^1.0"
24+
"psr/http-factory": "^1.0",
25+
"psr/http-client": "^1.0"
2426
}
2527
}

src/Client/Client.php

Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
<?php
2+
/**
3+
* Client.php
4+
*
5+
* This file is part of InitPHP HTTP.
6+
*
7+
* @author Muhammet ŞAFAK <[email protected]>
8+
* @copyright Copyright © 2022 Muhammet ŞAFAK
9+
* @license ./LICENSE MIT
10+
* @version 2.0
11+
* @link https://www.muhammetsafak.com.tr
12+
*/
13+
14+
declare(strict_types=1);
15+
16+
namespace InitPHP\HTTP\Client;
17+
18+
use InitPHP\HTTP\Message\Interfaces\StreamInterface;
19+
use \InitPHP\HTTP\Message\{Request, Stream, Response};
20+
use \Psr\Http\Message\{RequestInterface, ResponseInterface};
21+
use \InitPHP\HTTP\Client\Exceptions\{ClientException, NetworkException, RequestException};
22+
23+
use const CASE_LOWER;
24+
use const FILTER_VALIDATE_URL;
25+
26+
use function extension_loaded;
27+
use function trim;
28+
use function strtolower;
29+
use function preg_match;
30+
use function explode;
31+
use function ltrim;
32+
use function rtrim;
33+
use function strlen;
34+
use function filter_var;
35+
use function array_change_key_case;
36+
use function json_encode;
37+
use function is_string;
38+
use function is_array;
39+
use function is_object;
40+
use function method_exists;
41+
use function class_exists;
42+
use function get_object_vars;
43+
44+
class Client implements \Psr\Http\Client\ClientInterface
45+
{
46+
47+
protected string $userAgent;
48+
49+
public function __construct()
50+
{
51+
if (!extension_loaded('curl')) {
52+
throw new ClientException('The CURL extension must be installed.');
53+
}
54+
}
55+
56+
public function getUserAgent(): string
57+
{
58+
return $this->userAgent ?? 'InitPHP HTTP PSR-18 Client cURL';
59+
}
60+
61+
public function setUserAgent(?string $userAgent = null): self
62+
{
63+
!empty($userAgent) && $this->userAgent = $userAgent;
64+
65+
return $this;
66+
}
67+
68+
public function withUserAgent(?string $userAgent = null): self
69+
{
70+
return (clone $this)->setUserAgent($userAgent);
71+
}
72+
73+
public function fetch(string $url, array $details = []): ResponseInterface
74+
{
75+
$details = array_change_key_case($details, CASE_LOWER);
76+
77+
$request = $this->prepareRequest(
78+
$details['method'] ?? 'GET',
79+
$url,
80+
$details['data'] ?? $details['body'] ?? null,
81+
$details['headers'] ?? [],
82+
$details['version'] ?? '1.1'
83+
);
84+
85+
return $this->sendRequest($request);
86+
}
87+
88+
public function get(string $url, $body = null, array $headers = [], string $version = '1.1'): ResponseInterface
89+
{
90+
return $this->sendRequest($this->prepareRequest('GET', $url, $body, $headers, $version));
91+
}
92+
93+
public function post(string $url, $body = null, array $headers = [], string $version = '1.1'): ResponseInterface
94+
{
95+
return $this->sendRequest($this->prepareRequest('POST', $url, $body, $headers, $version));
96+
}
97+
98+
public function patch(string $url, $body = null, array $headers = [], string $version = '1.1'): ResponseInterface
99+
{
100+
return $this->sendRequest($this->prepareRequest('PATCH', $url, $body, $headers, $version));
101+
}
102+
103+
public function put(string $url, $body = null, array $headers = [], string $version = '1.1'): ResponseInterface
104+
{
105+
return $this->sendRequest($this->prepareRequest('PUT', $url, $body, $headers, $version));
106+
}
107+
108+
public function delete(string $url, $body = null, array $headers = [], string $version = '1.1'): ResponseInterface
109+
{
110+
return $this->sendRequest($this->prepareRequest('DELETE', $url, $body, $headers, $version));
111+
}
112+
113+
public function head(string $url, $body = null, array $headers = [], string $version = '1.1'): ResponseInterface
114+
{
115+
return $this->sendRequest($this->prepareRequest('HEAD', $url, $body, $headers, $version));
116+
}
117+
118+
/**
119+
* @inheritDoc
120+
*/
121+
public function sendRequest(RequestInterface $request): ResponseInterface
122+
{
123+
if ($request instanceof \InitPHP\HTTP\Message\Request) {
124+
$requestParameters = $request->all();
125+
if (!empty($requestParameters) && empty(trim($request->getBody()->getContents()))) {
126+
$bodyContent = json_encode($requestParameters);
127+
$request->getBody()->isWritable()
128+
? $request->getBody()->write($bodyContent)
129+
: $request->setBody(new Stream($bodyContent, null));
130+
}
131+
}
132+
133+
$options = $this->prepareCurlOptions($request);
134+
try {
135+
$curl = \curl_init();
136+
\curl_setopt_array($curl, $options);
137+
if (!\curl_errno($curl)) {
138+
$response['body'] = \curl_exec($curl);
139+
} else {
140+
throw new ClientException(\curl_error($curl), (int)\curl_errno($curl));
141+
}
142+
} catch (\Throwable $e) {
143+
throw new NetworkException($request, $e->getMessage(), (int)$e->getCode(), $e->getPrevious());
144+
} finally {
145+
\curl_reset($curl);
146+
\curl_close($curl);
147+
}
148+
149+
return new Response($response['status'], $response['headers'], new Stream($response['body'], null), $response['version']);
150+
}
151+
152+
153+
private function prepareCurlOptions(RequestInterface $request): array
154+
{
155+
try {
156+
$url = $request->getUri()->__toString();
157+
if (filter_var($url, FILTER_VALIDATE_URL)) {
158+
throw new ClientException('URL address is not valid.');
159+
}
160+
$version = $request->getProtocolVersion();
161+
$method = $request->getMethod();
162+
$headers = $request->getHeaders();
163+
$body = $request->getBody()->getContents();
164+
} catch (\Throwable $e) {
165+
throw new RequestException($request, $e->getMessage(), (int)$e->getCode(), $e->getPrevious());
166+
}
167+
168+
try {
169+
$options = [
170+
\CURLOPT_URL => $url,
171+
\CURLOPT_RETURNTRANSFER => true,
172+
\CURLOPT_ENCODING => '',
173+
\CURLOPT_MAXREDIRS => 10,
174+
\CURLOPT_TIMEOUT => 0,
175+
\CURLOPT_FOLLOWLOCATION => true,
176+
\CURLOPT_CUSTOMREQUEST => $method,
177+
\CURLOPT_USERAGENT => $this->getUserAgent(),
178+
];
179+
switch ($version) {
180+
case '1.0':
181+
$options[\CURLOPT_HTTP_VERSION] = \CURL_HTTP_VERSION_1_0;
182+
break;
183+
case '2.0':
184+
$options[\CURLOPT_HTTP_VERSION] = \CURL_HTTP_VERSION_2_0;
185+
break;
186+
default:
187+
$options[\CURLOPT_HTTP_VERSION] = \CURL_HTTP_VERSION_1_1;
188+
}
189+
190+
if ($method === 'HEAD') {
191+
$options[\CURLOPT_NOBODY] = true;
192+
} else {
193+
if (!empty($body)) {
194+
$options[\CURLOPT_POSTFIELDS] = $body;
195+
}
196+
}
197+
if (!empty($headers)) {
198+
$options[\CURLOPT_HTTPHEADER] = [];
199+
foreach ($headers as $name => $value) {
200+
$options[\CURLOPT_HTTPHEADER][] = $name . ': ' . $value;
201+
}
202+
}
203+
204+
$response = [
205+
'body' => '',
206+
'version' => $version,
207+
'status' => 200,
208+
'headers' => [],
209+
];
210+
211+
$options[\CURLOPT_HEADERFUNCTION] = function ($ch, $data) use (&$response) {
212+
$str = trim($data);
213+
if (!empty($str)) {
214+
$lowercase = strtolower($str);
215+
if (preg_match("/http\/([\.0-2]+) ([\d]+).?/i", $lowercase, $matches)) {
216+
$response['version'] = $matches[1];
217+
$response['status'] = (int)$matches[2];
218+
} else {
219+
$split = explode(':', $str, 2);
220+
$response['headers'][trim($split[0], ' ')] = ltrim(rtrim($split[1], ';'), ' ');
221+
}
222+
}
223+
224+
return strlen($data);
225+
};
226+
227+
} catch (\Throwable $e) {
228+
throw new ClientException($e->getMessage(), (int)$e->getCode(), $e->getPrevious());
229+
}
230+
231+
return $options;
232+
}
233+
234+
private function prepareRequest(string $method, string $url, $body = null, array $headers = [], string $version = '1.1'): RequestInterface
235+
{
236+
if ($body === null) {
237+
$body = new Stream('', null);
238+
} else if (is_string($body)) {
239+
$body = new Stream($body, null);
240+
} else if (is_array($body)) {
241+
$body = new Stream(json_encode($body), null);
242+
} else if ((class_exists('DOMDocument')) && ($body instanceof \DOMDocument)) {
243+
$body = new Stream($body->saveHTML(), null);
244+
} else if ((class_exists('SimpleXMLElement')) && ($body instanceof \SimpleXMLElement)) {
245+
$body = new Stream($body->asXML(), null);
246+
} else if (is_object($body)) {
247+
if (method_exists($body, '__toString')) {
248+
$body = $body->__toString();
249+
} else if (method_exists($body, 'toArray')) {
250+
$body = json_encode($body->toArray());
251+
} else {
252+
$body = json_encode(get_object_vars($body));
253+
}
254+
$body = new Stream($body, null);
255+
}
256+
if ($body instanceof StreamInterface) {
257+
throw new \InvalidArgumentException("\$body is not supported.");
258+
}
259+
return new Request($method, $url, $body, $headers, $version);
260+
}
261+
262+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
/**
3+
* ClientException.php
4+
*
5+
* This file is part of InitPHP HTTP.
6+
*
7+
* @author Muhammet ŞAFAK <[email protected]>
8+
* @copyright Copyright © 2022 Muhammet ŞAFAK
9+
* @license ./LICENSE MIT
10+
* @version 2.0
11+
* @link https://www.muhammetsafak.com.tr
12+
*/
13+
14+
declare(strict_types=1);
15+
16+
namespace InitPHP\HTTP\Client\Exceptions;
17+
18+
class ClientException extends \Exception implements \Psr\Http\Client\ClientExceptionInterface
19+
{
20+
}

0 commit comments

Comments
 (0)