Skip to content

v2.0 #1

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Mar 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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