Initial commit

This commit is contained in:
Joop Schilder 2019-12-07 15:03:02 +01:00
commit e02e43217c
9 changed files with 375 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
/var/
/vendor/
/.idea/

47
README.md Normal file
View File

@ -0,0 +1,47 @@
# `curl-http2`
## What is this?
A `curl` wrapper for PHP that uses `HTTP/2` to send requests over a single TCP connection.
Provided are:
- The main class `JoopSchilder\Http2\Http2`
- A simple class for requests supporting `curl` options
- A simple class for responses
## What is this not?
A production-ready library.
I mean, you _are_ of course allowed to use it if you think it's useful.
## Example
There is some example code in `bin/app.php`.
In it's most basic form, usage of this library might look like this:
```php
use JoopSchilder\Http2\Http2;
use JoopSchilder\Http2\Response;
$http2 = new Http2();
// Might also be an implementation of the ResponseHandler interface
$http2->onResponse(function(Response $response) {
var_dump($response);
});
// This creates a request with sensible defaults
$request = $http2->createRequest('https://www.twitter.com/');
$request->setOptions([CURLOPT_USERAGENT => 'AppleTV6,2/11.1']);
$http2->addRequest($request);
// Requests are not executed until this method is called
$http2->execute();
```
It's a good idea to use a different instance of `Http2` for every host you plan to make a request to.
## What's next?
- Add more control to dynamically add requests to an `Http2` instance
- Make an `Http2` instance coupled to a domain (as this is its intended use)
- Use [`PSR-7`](https://www.php-fig.org/psr/psr-7/) HTTP message interfaces
- Add a factory for requests

37
bin/app.php Executable file
View File

@ -0,0 +1,37 @@
<?php
use JoopSchilder\Http2\Http2;
use JoopSchilder\Http2\Response;
require_once __DIR__ . '/../vendor/autoload.php';
$http2 = new Http2();
$http2->onResponse(function (Response $response) {
$statusLine = substr($response->getHeader(), 0, strpos($response->getHeader(), "\r"));
$statusLine = str_pad($statusLine, 14, ' ', STR_PAD_RIGHT);
print("$statusLine {$response->getOriginalUrl()}\n");
});
// Add urls
$urls = [
'https://twitter.com/survivetheark',
'https://twitter.com/missingpeople',
'https://twitter.com/elainecrowley',
'https://twitter.com/2020comms',
'https://twitter.com/goal',
'https://twitter.com/cydarmedical',
'https://twitter.com/cloakzy',
'https://twitter.com/cllrandrewkelly',
'https://twitter.com/youranonnews',
'https://twitter.com/trickyjabs',
];
foreach ($urls as $url) {
$http2->addRequest($http2->createRequest($url));
}
// Execute the requests
print("Note: it might take some time to establish the TCP connection.\n");
print("The benefits of HTTP/2 become more clear if you send more requests to the same server.\n\n");
$startTime = microtime(true);
$http2->execute();
printf("Requests: %d, time(s): %.3f\n", count($urls), microtime(true) - $startTime);

19
composer.json Normal file
View File

@ -0,0 +1,19 @@
{
"name": "joopschilder/php-http2",
"authors": [
{
"name": "Joop Schilder",
"email": "jnmschilder@protonmail.com"
}
],
"autoload": {
"psr-4": {
"JoopSchilder\\Http2\\": [
"src"
]
}
},
"require": {
"ext-curl": "*"
}
}

19
composer.lock generated Normal file
View File

@ -0,0 +1,19 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "5b1e133742e667bcab8f73ed237d3adc",
"packages": [],
"packages-dev": [],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
"prefer-stable": false,
"prefer-lowest": false,
"platform": {
"ext-curl": "*"
},
"platform-dev": []
}

117
src/Http2.php Normal file
View File

@ -0,0 +1,117 @@
<?php
namespace JoopSchilder\Http2;
use RuntimeException;
/**
* Class Http2
*/
class Http2
{
/** @var resource */
private $multihandle;
/** @var callable */
private $responseHandler;
/**
* Http2 constructor.
*/
public function __construct()
{
$this->multihandle = curl_multi_init();
curl_multi_setopt($this->multihandle, CURLMOPT_PIPELINING, CURLPIPE_MULTIPLEX);
curl_multi_setopt($this->multihandle, CURLMOPT_MAX_HOST_CONNECTIONS, 1);
curl_multi_setopt($this->multihandle, CURLMOPT_MAX_TOTAL_CONNECTIONS, 1);
}
/**
* Http2 destructor.
*/
public function __destruct()
{
if (is_resource($this->multihandle)) {
curl_multi_close($this->multihandle);
}
}
/**
* @param string $url
* @param bool $priorKnowledge
* @return Request
*/
public function createRequest(string $url, bool $priorKnowledge = true): Request
{
return (new Request($url))->setOptions([
CURLOPT_AUTOREFERER => 1,
CURLOPT_FOLLOWLOCATION => 1,
CURLOPT_HEADER => 1,
CURLOPT_HTTP_VERSION => ($priorKnowledge ? CURL_HTTP_VERSION_2_PRIOR_KNOWLEDGE : CURL_HTTP_VERSION_2),
CURLOPT_MAXREDIRS => 5,
CURLOPT_PIPEWAIT => 1,
CURLOPT_RETURNTRANSFER => 1,
CURLOPT_SSL_VERIFYPEER => 0,
CURLOPT_TIMEOUT => 10,
]);
}
/**
* @param Request $request
*/
public function addRequest(Request $request): void
{
$handle = curl_init($request->getUri());
curl_setopt_array($handle, $request->getOptions());
curl_multi_add_handle($this->multihandle, $handle);
}
/**
* @param callable $responseHandler
*/
public function onResponse(callable $responseHandler): void
{
$this->responseHandler = $responseHandler;
}
/**
*
*/
public function execute(): void
{
$this->guardNoResponseHandler();
do {
curl_multi_exec($this->multihandle, $stillRunning);
while (false !== ($message = curl_multi_info_read($this->multihandle))) {
$handle = $message['handle'];
$content = curl_multi_getcontent($handle);
$header = substr($content, 0, curl_getinfo($handle, CURLINFO_HEADER_SIZE));
$content = str_replace($header, '', $content);
$url = curl_getinfo($handle, CURLINFO_EFFECTIVE_URL);
call_user_func($this->responseHandler, new Response($url, $header, $content));
curl_multi_remove_handle($this->multihandle, $handle);
curl_close($handle);
}
usleep(5);
} while ($stillRunning > 0);
}
/**
*
*/
private function guardNoResponseHandler(): void
{
if (!is_callable($this->responseHandler)) {
throw new RuntimeException('No valid response handler found');
}
}
}

57
src/Request.php Normal file
View File

@ -0,0 +1,57 @@
<?php
namespace JoopSchilder\Http2;
/**
* Class Request
*/
class Request
{
/** @var string */
private $uri;
/** @var array */
private $options;
/**
* Request constructor.
* @param string $uri
*/
public function __construct(string $uri)
{
$this->uri = $uri;
$this->options = [];
}
/**
* @return string
*/
public function getUri(): string
{
return $this->uri;
}
/**
* @return array
*/
public function getOptions(): array
{
return $this->options;
}
/**
* @param array $options
* @return Request
*/
public function setOptions(array $options)
{
$this->options = array_replace($this->options, $options);
return $this;
}
}

60
src/Response.php Normal file
View File

@ -0,0 +1,60 @@
<?php
namespace JoopSchilder\Http2;
/**
* Class Response
*/
class Response
{
/** @var string */
private $originalUrl;
/** @var string */
private $header;
/** @var string */
private $content;
/**
* Response constructor.
* @param string $originalUrl
* @param string $header
* @param string $content
*/
public function __construct(string $originalUrl, string $header, string $content)
{
$this->originalUrl = trim($originalUrl);
$this->header = trim($header);
$this->content = trim($content);
}
/**
* @return string
*/
public function getOriginalUrl(): string
{
return $this->originalUrl;
}
/**
* @return string
*/
public function getHeader(): string
{
return $this->header;
}
/**
* @return string
*/
public function getContent(): string
{
return $this->content;
}
}

16
src/ResponseHandler.php Normal file
View File

@ -0,0 +1,16 @@
<?php
namespace JoopSchilder\Http2;
/**
* Interface ResponseHandler
*/
interface ResponseHandler
{
/**
* @param Response $response
* @return mixed
*/
function __invoke(Response $response): void;
}