Skip to content

Commit

Permalink
Merge pull request #1 from InitPHP/2.0
Browse files Browse the repository at this point in the history
v2.0
  • Loading branch information
muhammetsafak committed Mar 16, 2023
2 parents 8ad1763 + 1cc565e commit 1fc689b
Show file tree
Hide file tree
Showing 32 changed files with 3,459 additions and 1,962 deletions.
72 changes: 68 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ This library provides HTTP Message and HTTP Factory solution following PSR-7 and
- PHP 7.4 or higher
- PSR-7 HTTP Message Interfaces
- PSR-17 HTTP Factories Interfaces
- PSR-18 HTTP Client Interfaces

## Installation

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

## Usage

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

### Emitter Usage
### PSR-7 Emitter Usage

```php
use \InitPHP\HTTP\{Response, Emitter, Stream};
use \InitPHP\HTTP\Message\{Response, Stream};
use \InitPHP\HTTP\Emitter\Emitter;


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

$emitter = new Emitter;
$emitter = new Emitter();
$emitter->emit($response);
```

or

```php
use \InitPHP\HTTP\Facade\Factory;
use \InitPHP\HTTP\Facade\Emitter;

$response = Factory::createResponse(200);
$response->getBody()->write('Hello World');

Emitter::emit($response);
```

### PSR-17 Factory Usage

```php
use \InitPHP\HTTP\Factory\Factory;

$httpFactory = new Factory();

/** @var \Psr\Http\Message\RequestInterface $request */
$request = $httpFactory->createRequest('GET', 'http://example.com');

// ...
```

or

```php
use InitPHP\HTTP\Facade\Factory;

/** @var \Psr\Http\Message\RequestInterface $request */
$request = Factory::createRequest('GET', 'http://example.com');
```

### PSR-18 Client Usage

```php
use \InitPHP\HTTP\Message\Request;
use \InitPHP\HTTP\Client\Client;

$request = new Request('GET', 'http://example.com');

$client = new Client();

/** @var \Psr\Http\Message\ResponseInterface $response */
$response = $client->sendRequest($request);
```

or

```php
use \InitPHP\HTTP\Facade\Factory;
use \InitPHP\HTTP\Facade\Client;

$request = Factory::createRequest('GET', 'http://example.com');

/** @var \Psr\Http\Message\ResponseInterface $response */
$response = Client::sendRequest($request);
```



#### A Small Difference For PSR-7 Stream

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.
Expand Down
6 changes: 4 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "initphp/http",
"description": "InitPHP PSR-7 HTTP Message Library",
"description": "HTTP",
"type": "library",
"license": "MIT",
"autoload": {
Expand All @@ -19,7 +19,9 @@
"minimum-stability": "stable",
"require": {
"php": ">=7.4",
"ext-json": "*",
"psr/http-message": "^1.0",
"psr/http-factory": "^1.0"
"psr/http-factory": "^1.0",
"psr/http-client": "^1.0"
}
}
262 changes: 262 additions & 0 deletions src/Client/Client.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
<?php
/**
* Client.php
*
* This file is part of InitPHP HTTP.
*
* @author Muhammet ŞAFAK <info@muhammetsafak.com.tr>
* @copyright Copyright © 2022 Muhammet ŞAFAK
* @license ./LICENSE MIT
* @version 2.0
* @link https://www.muhammetsafak.com.tr
*/

declare(strict_types=1);

namespace InitPHP\HTTP\Client;

use InitPHP\HTTP\Message\Interfaces\StreamInterface;
use \InitPHP\HTTP\Message\{Request, Stream, Response};
use \Psr\Http\Message\{RequestInterface, ResponseInterface};
use \InitPHP\HTTP\Client\Exceptions\{ClientException, NetworkException, RequestException};

use const CASE_LOWER;
use const FILTER_VALIDATE_URL;

use function extension_loaded;
use function trim;
use function strtolower;
use function preg_match;
use function explode;
use function ltrim;
use function rtrim;
use function strlen;
use function filter_var;
use function array_change_key_case;
use function json_encode;
use function is_string;
use function is_array;
use function is_object;
use function method_exists;
use function class_exists;
use function get_object_vars;

class Client implements \Psr\Http\Client\ClientInterface
{

protected string $userAgent;

public function __construct()
{
if (!extension_loaded('curl')) {
throw new ClientException('The CURL extension must be installed.');
}
}

public function getUserAgent(): string
{
return $this->userAgent ?? 'InitPHP HTTP PSR-18 Client cURL';
}

public function setUserAgent(?string $userAgent = null): self
{
!empty($userAgent) && $this->userAgent = $userAgent;

return $this;
}

public function withUserAgent(?string $userAgent = null): self
{
return (clone $this)->setUserAgent($userAgent);
}

public function fetch(string $url, array $details = []): ResponseInterface
{
$details = array_change_key_case($details, CASE_LOWER);

$request = $this->prepareRequest(
$details['method'] ?? 'GET',
$url,
$details['data'] ?? $details['body'] ?? null,
$details['headers'] ?? [],
$details['version'] ?? '1.1'
);

return $this->sendRequest($request);
}

public function get(string $url, $body = null, array $headers = [], string $version = '1.1'): ResponseInterface
{
return $this->sendRequest($this->prepareRequest('GET', $url, $body, $headers, $version));
}

public function post(string $url, $body = null, array $headers = [], string $version = '1.1'): ResponseInterface
{
return $this->sendRequest($this->prepareRequest('POST', $url, $body, $headers, $version));
}

public function patch(string $url, $body = null, array $headers = [], string $version = '1.1'): ResponseInterface
{
return $this->sendRequest($this->prepareRequest('PATCH', $url, $body, $headers, $version));
}

public function put(string $url, $body = null, array $headers = [], string $version = '1.1'): ResponseInterface
{
return $this->sendRequest($this->prepareRequest('PUT', $url, $body, $headers, $version));
}

public function delete(string $url, $body = null, array $headers = [], string $version = '1.1'): ResponseInterface
{
return $this->sendRequest($this->prepareRequest('DELETE', $url, $body, $headers, $version));
}

public function head(string $url, $body = null, array $headers = [], string $version = '1.1'): ResponseInterface
{
return $this->sendRequest($this->prepareRequest('HEAD', $url, $body, $headers, $version));
}

/**
* @inheritDoc
*/
public function sendRequest(RequestInterface $request): ResponseInterface
{
if ($request instanceof \InitPHP\HTTP\Message\Request) {
$requestParameters = $request->all();
if (!empty($requestParameters) && empty(trim($request->getBody()->getContents()))) {
$bodyContent = json_encode($requestParameters);
$request->getBody()->isWritable()
? $request->getBody()->write($bodyContent)
: $request->setBody(new Stream($bodyContent, null));
}
}

$options = $this->prepareCurlOptions($request);
try {
$curl = \curl_init();
\curl_setopt_array($curl, $options);
if (!\curl_errno($curl)) {
$response['body'] = \curl_exec($curl);
} else {
throw new ClientException(\curl_error($curl), (int)\curl_errno($curl));
}
} catch (\Throwable $e) {
throw new NetworkException($request, $e->getMessage(), (int)$e->getCode(), $e->getPrevious());
} finally {
\curl_reset($curl);
\curl_close($curl);
}

return new Response($response['status'], $response['headers'], new Stream($response['body'], null), $response['version']);
}


private function prepareCurlOptions(RequestInterface $request): array
{
try {
$url = $request->getUri()->__toString();
if (filter_var($url, FILTER_VALIDATE_URL)) {
throw new ClientException('URL address is not valid.');
}
$version = $request->getProtocolVersion();
$method = $request->getMethod();
$headers = $request->getHeaders();
$body = $request->getBody()->getContents();
} catch (\Throwable $e) {
throw new RequestException($request, $e->getMessage(), (int)$e->getCode(), $e->getPrevious());
}

try {
$options = [
\CURLOPT_URL => $url,
\CURLOPT_RETURNTRANSFER => true,
\CURLOPT_ENCODING => '',
\CURLOPT_MAXREDIRS => 10,
\CURLOPT_TIMEOUT => 0,
\CURLOPT_FOLLOWLOCATION => true,
\CURLOPT_CUSTOMREQUEST => $method,
\CURLOPT_USERAGENT => $this->getUserAgent(),
];
switch ($version) {
case '1.0':
$options[\CURLOPT_HTTP_VERSION] = \CURL_HTTP_VERSION_1_0;
break;
case '2.0':
$options[\CURLOPT_HTTP_VERSION] = \CURL_HTTP_VERSION_2_0;
break;
default:
$options[\CURLOPT_HTTP_VERSION] = \CURL_HTTP_VERSION_1_1;
}

if ($method === 'HEAD') {
$options[\CURLOPT_NOBODY] = true;
} else {
if (!empty($body)) {
$options[\CURLOPT_POSTFIELDS] = $body;
}
}
if (!empty($headers)) {
$options[\CURLOPT_HTTPHEADER] = [];
foreach ($headers as $name => $value) {
$options[\CURLOPT_HTTPHEADER][] = $name . ': ' . $value;
}
}

$response = [
'body' => '',
'version' => $version,
'status' => 200,
'headers' => [],
];

$options[\CURLOPT_HEADERFUNCTION] = function ($ch, $data) use (&$response) {
$str = trim($data);
if (!empty($str)) {
$lowercase = strtolower($str);
if (preg_match("/http\/([\.0-2]+) ([\d]+).?/i", $lowercase, $matches)) {
$response['version'] = $matches[1];
$response['status'] = (int)$matches[2];
} else {
$split = explode(':', $str, 2);
$response['headers'][trim($split[0], ' ')] = ltrim(rtrim($split[1], ';'), ' ');
}
}

return strlen($data);
};

} catch (\Throwable $e) {
throw new ClientException($e->getMessage(), (int)$e->getCode(), $e->getPrevious());
}

return $options;
}

private function prepareRequest(string $method, string $url, $body = null, array $headers = [], string $version = '1.1'): RequestInterface
{
if ($body === null) {
$body = new Stream('', null);
} else if (is_string($body)) {
$body = new Stream($body, null);
} else if (is_array($body)) {
$body = new Stream(json_encode($body), null);
} else if ((class_exists('DOMDocument')) && ($body instanceof \DOMDocument)) {
$body = new Stream($body->saveHTML(), null);
} else if ((class_exists('SimpleXMLElement')) && ($body instanceof \SimpleXMLElement)) {
$body = new Stream($body->asXML(), null);
} else if (is_object($body)) {
if (method_exists($body, '__toString')) {
$body = $body->__toString();
} else if (method_exists($body, 'toArray')) {
$body = json_encode($body->toArray());
} else {
$body = json_encode(get_object_vars($body));
}
$body = new Stream($body, null);
}
if ($body instanceof StreamInterface) {
throw new \InvalidArgumentException("\$body is not supported.");
}
return new Request($method, $url, $body, $headers, $version);
}

}
20 changes: 20 additions & 0 deletions src/Client/Exceptions/ClientException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php
/**
* ClientException.php
*
* This file is part of InitPHP HTTP.
*
* @author Muhammet ŞAFAK <info@muhammetsafak.com.tr>
* @copyright Copyright © 2022 Muhammet ŞAFAK
* @license ./LICENSE MIT
* @version 2.0
* @link https://www.muhammetsafak.com.tr
*/

declare(strict_types=1);

namespace InitPHP\HTTP\Client\Exceptions;

class ClientException extends \Exception implements \Psr\Http\Client\ClientExceptionInterface
{
}
Loading

0 comments on commit 1fc689b

Please sign in to comment.