symplely/hyper

An simple advance asynchronous PSR-7 and PSR-18 Http client using coroutines.

1.0.0 2020-03-09 14:36 UTC

This package is auto-updated.

Last update: 2020-05-07 13:30:25 UTC


README

Build StatusBuild statuscodecovCodacy BadgeMaintainability

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 as password or token.
  • 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, sets Content-Type header to application/json.
  • Body::FORM Convert an associative array into a query string, sets Content-Type header to application/x-www-form-urlencoded.
  • Body::XML Does no conversion of data, sets Content-Type header to application/xml.
  • Body::FILE Does no conversion of data, will detect and set Content-Type header.
  • Body::MULTI Does no conversion of data, sets Content-Type header to multipart/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.