symplely / hyper
An simple advance asynchronous PSR-7 and PSR-18 Http client using coroutines.
Requires
- php: >7.1
- ext-json: *
- symplely/coroutine: ^1.7.0
- symplely/http: ^1.1.0
- symplely/logger: ^1.0.9
Requires (Dev)
- phpunit/phpunit: ^6 | ^7
Provides
This package is auto-updated.
Last update: 2024-10-07 23:25:09 UTC
README
An simple advance PSR-7 implementation and asynchronous PSR-18 HTTP client using coroutines.
Table of Contents
Introduction/Usage
This package is based on coroutines using yield
an generator
, it requires our other repo package Coroutine. It's construction is inspired by and an overhaul of shuttle, an PSR-18 client.
There is a lot to be said about coroutines, but for an quick overview, checkout this video, if you have no formulary with the concept or construction. Only one thing to keep in mind when viewing the video, is that it's an overview of callbacks vs promises vs generators, an object given an async/await construction in other languages. And the Promise
reference there, is referred here has as an Task
, that returns a plain Integer
.
This library and the whole Coroutine concept here, is base around NEVER having the user/developer directly accessing the Task, the Promise like object. For conceptually usage example see Advanced Asyncio: Solving Real World Production Problems.
From example's folder:
/** * @see https://github.com/amphp/artax/blob/master/examples/6-parallel-requests.php */ include 'vendor/autoload.php'; use Async\Coroutine\Exceptions\Panicking; // An infinite loop example task, this will output on all task executions, pauses, stalls, or whatever. function lapse() { $i = 0; while(true) { $i++; print $i.'.lapse '; yield; } } // An initial function is required, this gives all other tasks an execution point to begin. function main() { // start an background job/task yield \await(lapse()); $uris = [ "https://github.com/", "https://google.com/", "https://stackoverflow.com/", 'http://creativecommons.org/' ]; try { $uriId = []; // Make an asynchronous HTTP requests // this `array` will collect `int`, represents an `http task id` // each `request()` starts an new background task, does not block or wait. // the `array` will be passed to `fetch();` for resolution. foreach ($uris as $uri) { $uriId[] = yield \request(\http_get($uri)); } // Executions here will pause and wait for each request to receive an response, // all previous tasks started will continue to run, so you will see output only // from the `lapse` task here until the foreach loop starts. // Doing `\fetchOptions(1);` before this statement will wait for only 1 response. $bodies = yield \fetch($uriId); foreach ($bodies as $id => $result) { $uri = \response_meta($result, 'uri'); // will pause execution of loop until full body is received, only `lapse` task will still output $body = yield \response_body($result); print \EOL."HTTP Task $id: ". $uri. " - " . \strlen($body) . " bytes" . \EOL.\EOL; } } catch (Panicking $error) { echo 'There was a problem: '.$error->getMessage(); } // display the collected stats logs \print_defaultLog(); yield \http_closeLog(); // if you don't the infinite loop will continue it's output. yield \shutdown(); } // Coroutine code needs to be bootstrapped. // The function called `MUST` have at least one `yield` statement. \coroutine_run(\main());
Functions
The functions listed here and in Core.php file is the recommended way to use this package. An functional programming approach is being used, the actual OOP class library used is constantly changing, but these calling functions will not.
const SYMPLELY_USER_AGENT = 'Symplely Hyper PHP/' . \PHP_VERSION; // Content types for header data. const HTML_TYPE = 'text/html'; const OCTET_TYPE = 'application/octet-stream'; const XML_TYPE = 'application/xml'; const PLAIN_TYPE = 'text/plain'; const MULTI_TYPE = 'multipart/form-data'; const JSON_TYPE = 'application/json'; const FORM_TYPE = 'application/x-www-form-urlencoded'; /** * This function works similar to coroutine `await()` * * Takes an `request` instance or `yield`ed coroutine of an request. * Will immediately return an `int` HTTP task id, and continue to the next instruction. * Will resolve to an Response instance when `fetch()` * * - This function needs to be prefixed with `yield` */ yield \request(); /** * Run awaitable HTTP tasks in the requests set concurrently and block * until the condition specified by count. * * This function works similar to `gatherWait()`. * * Controls how the `wait/fetch` functions operates. * `await()` will behave like **Promise** functions `All`, `Some`, `Any` in JavaScript. * * - This function needs to be prefixed with `yield` */ yield \fetch_await($requests, $count, $exception, $clearAborted); /** * This function works similar to coroutine `gather()` * * Takes an array of request HTTP task id's. * Will pause current task and continue other tasks until * the supplied request HTTP task id's resolve to an response instance. * * - This function needs to be prefixed with `yield` */ yield \fetch(...$requests); /** * This function works similar to `cancel_task()` * * Will abort the supplied request HTTP task id and close stream. * * - This function needs to be prefixed with `yield` */ yield \request_abort($httpId); /** * This function is automatically called by the http_* functions. * * Creates and returns an `Hyper` instance for the global HTTP functions by. */ \http_instance($tag); /** * Clear & Close the global `Hyper`, and `Stream` Instances by. */ \http_clear($tag); /** * Close and Clear `ALL` global Hyper function, Stream instances. */ \http_nuke(); /** * Make a GET request, will pause current task, and * continue other tasks until an response is received. * * - This function needs to be prefixed with `yield` */ yield \http_get($tagUri, ...$authorizeHeaderOptions); /** * Make a PUT request, will pause current task, and * continue other tasks until an response is received. * * - This function needs to be prefixed with `yield` */ yield \http_put($tagUri, ...$authorizeHeaderOptions); /** * Make a POST request, will pause current task, and * continue other tasks until an response is received. * * - This function needs to be prefixed with `yield` */ yield \http_post($tagUri, ...$authorizeHeaderOptions); /** * Make a PATCH request, will pause current task, and * continue other tasks until an response is received. * * - This function needs to be prefixed with `yield` */ yield \http_patch($tagUri, ...$authorizeHeaderOptions); /** * Make a DELETE request, will pause current task, and * continue other tasks until an response is received. * * - This function needs to be prefixed with `yield` */ yield \http_delete($tagUri, ...$authorizeHeaderOptions); /** * Make a OPTIONS request, will pause current task, and * continue other tasks until an response is received. * * - This function needs to be prefixed with `yield` */ yield \http_options($tagUri, ...$authorizeHeaderOptions); /** * Make a HEAD request, will pause current task, and * continue other tasks until an response is received. * * - This function needs to be prefixed with `yield` */ yield \http_head($tagUri, ...$authorizeHeaderOptions); /** * This function is automatically called by the http_* functions. * * Set global functions response instance by. */ \response_set($response, $tag); /** * This function is automatically called by the response_* functions. * * Return current global functions response instance by. */ \response_instance($tag); /** * Clear and close global functions response/stream instance by. */ \response_clear($tag); /** * Close and Clear `ALL` global functions response instances. */ \response_nuke(); /** * Is response from an successful request? * Returns an `bool` or NULL, if not ready. * * This function can be used in an loop control statement, * which you will `yield` on `NULL`. */ \response_ok($tag); /** * Returns response reason phrase `string` or NULL, if not ready. * * This function can be used in an loop control statement, * which you will `yield` on `NULL`. */ \response_phrase($tag); /** * Returns response status code `int` or NULL, if not ready. * * This function can be used in an loop control statement, * which you will `yield` on `NULL`. */ \response_code($tag); /** * Check if response has header key by. * Returns `bool` or NULL, if not ready. * * This function can be used in an loop control statement, * which you will `yield` on `NULL`. */ \response_has($tag, $header); /** * Retrieve a response value for header key by. * Returns `string` or NULL, if not ready. * * This function can be used in an loop control statement, * which you will `yield` on `NULL`. */ \response_header($tag, $header); /** * returns response FULL body. * * - This function needs to be prefixed with `yield` */ yield \response_body($tag); /** * Returns `string` of response metadata by key. */ \response_meta($tag, $key); /** * Check if response body been read completely by. * Returns `bool` or NULL, if not ready. * * This function can be used in an loop control statement, * which you will `yield` on `NULL`. */ \response_eof($tag); /** * returns response STREAM body. * * - This function needs to be prefixed with `yield` */ yield \response_stream($tag, $size); /** * returns response JSON body. * * - This function needs to be prefixed with `yield` */ yield \response_json($tag, $assoc); /** * returns response XML body. * * - This function needs to be prefixed with `yield` */ yield \response_xml($tag, $assoc);
Installation
composer require symplely/hyper
Usage/Historical
Making requests: The easy old fashion way, with one caveat, need to be prefix with yield
The quickest and easiest way to begin making requests is to use the HTTP method name:
use Async\Request\Hyper; function main() { $http = new Hyper; $response = yield $http->get("https://www.google.com"); $response = yield $http->post("https://example.com/search", ["Form data"]));
This library has built-in methods to support the major HTTP verbs: GET
, POST
, PUT
, PATCH
, DELETE
, HEAD
, and OPTIONS
. However, you can make any HTTP verb request using the request method directly, that returns an PSR-7 RequestInterface
.
$request = $http->request("connect", "https://api.example.com/v1/books"); $response = yield $http->sendRequest($request);
Handling responses
Responses in Hyper implement PSR-7 ResponseInterface
and as such are streamable resources.
$response = $http->get("https://api.example.com/v1/books"); echo $response->getStatusCode(); // 200 echo $response->getReasonPhrase(); // OK // The body is return asynchronous in an non-blocking mode, // and as such needs to be prefixed with `yield` $body = yield $response->getBody()->getContents(); } // All coroutines/async/await needs to be enclosed/bootstrapped // in one `main` entrance routine function to properly execute. // The function `MUST` have at least one `yield` statement. \coroutine_run(\main());
Handling failed requests
This library will throw a RequestException
by default if the request failed. This includes things like host name not found, connection timeouts, etc.
Responses with HTTP 4xx or 5xx status codes will not throw an exception and must be handled properly within your business logic.
Making requests: The PSR-7 way, with one caveat, need to be prefix with yield
If code reusability and portability is your thing, future proof your code by making requests the PSR-7 way. Remember, PSR-7 stipulates that Request and Response messages be immutable.
use Async\Request\Uri; use Async\Request\Request; use Async\Request\Hyper; function main() { // Build Request message. $request = new Request; $request = $request ->withMethod("get") ->withUri(Uri::create("https://www.google.com")) ->withHeader("Accept-Language", "en_US"); $http = new Hyper; // Send the Request. // Pauses current/task and send request, // will continue next to instruction once response is received, // other tasks/code continues to run. $response = yield $http->sendRequest($request); } // All coroutines/async/await needs to be enclosed/bootstrapped // in one `main` entrance routine function to properly execute. // The function `MUST` have at least one `yield` statement. \coroutine_run(\main());
Options
The following options can be pass on each request.
$http->request($method, $url, $body = null, array ...$authorizeHeaderOptions);
Authorization
An array with key as either:auth_basic
,auth_bearer
,auth_digest
, and value aspassword
ortoken
.Headers
An array of key & value pairs to pass in with each request.Options
An array of key & value pairs to pass in with each request.
Request bodies
An easy way to submit data with your request is to use the Body
class. This class will automatically
transform the data, convert to a BufferStream, and set a default Content-Type header on the request.
Pass one of the following CONSTANTS, onto the class constructor will:
Body::JSON
Convert an associative array into JSON, setsContent-Type
header toapplication/json
.Body::FORM
Convert an associative array into a query string, setsContent-Type
header toapplication/x-www-form-urlencoded
.Body::XML
Does no conversion of data, setsContent-Type
header toapplication/xml
.Body::FILE
Does no conversion of data, will detect and setContent-Type
header.Body::MULTI
Does no conversion of data, setsContent-Type
header tomultipart/form-data
.
To submit a JSON payload with a request:
use Async\Request\Body; use Async\Request\Hyper; function main() { $book = [ "title" => "Breakfast Of Champions", "author" => "Kurt Vonnegut", ]; $http = new Hyper; yield $http->post("https://api.example.com/v1/books", [Body::JSON, $book]); // Or yield $http->post("https://api.example.com/v1/books", new Body(Body::JSON, $book)); // Or yield $http->post("https://api.example.com/v1/books", Body::create(Body::JSON, $book)); // Otherwise the default, will be submitted in FORM format of `application/x-www-form-urlencoded` yield $http->post("https://api.example.com/v1/books", $book); } // All coroutines/async/await needs to be enclosed/bootstrapped // in one `main` entrance routine function to properly execute. // The function `MUST` have at least one `yield` statement. \coroutine_run(\main());
Contributing
Contributions are encouraged and welcome; I am always happy to get feedback or pull requests on Github :) Create Github Issues for bugs and new features and comment on the ones you are interested in.
License
The MIT License (MIT). Please see License File for more information.