clue/buzz-react

Simple, async HTTP client for concurrently processing any number of HTTP requests, built on top of React PHP

Installs: 2 174

Dependents: 5

Stars: 20

Watchers: 2

Forks: 7

Open Issues: 11

Language: PHP

v0.4.0 2015-08-09 22:28 UTC

README

Simple, async HTTP client for concurrently processing any number of HTTP requests, built on top of React PHP.

This library is heavily inspired by the great kriswallsmith/Buzz project. However, instead of blocking on each request, it relies on React PHP's EventLoop to process multiple requests in parallel. This allows you to interact with multiple HTTP servers (fetch URLs, talk to RESTful APIs, follow redirects etc.) at the same time. Unlike the underlying react/http-client, this project aims at providing a higher-level API that is easy to use in order to process multiple HTTP requests concurrently without having to mess with most of the low-level details.

  • Async execution of HTTP requests - Send any number of HTTP requests to any number of HTTP servers in parallel and process their responses as soon as results come in. The Promise-based design provides a sane interface to working with out of bound responses.
  • Lightweight, SOLID design - Provides a thin abstraction that is just good enough and does not get in your way. Builds on top of well-tested components and well-established concepts instead of reinventing the wheel.
  • Good test coverage - Comes with an automated tests suite and is regularly tested in the real world

Table of contents

Quickstart example

Once installed, you can use the following code to access a HTTP webserver and send some simple HTTP GET requests:

$loop = React\EventLoop\Factory::create();
$client = new Browser($loop);

$client->get('http://www.google.com/')->then(function (Response $result) {
    var_dump($result->getHeaders(), $result->getBody());
});

$loop->run();

See also the examples.

Usage

Browser

The Browser is responsible for sending HTTP requests to your HTTP server and keeps track of pending incoming HTTP responses. It also registers everything with the main EventLoop.

$loop = React\EventLoop\Factory::create();
$browser = new Browser($loop);

If you need custom DNS or proxy settings, you can explicitly pass a custom Sender instance. This is considered advanced usage.

Methods

The Browser offers several methods that resemble the HTTP protocol methods:

$browser->get($url, array $headers = array());
$browser->head($url, array $headers = array());
$browser->post($url, array $headers = array(), $content = '');
$browser->delete($url, array $headers = array(), $content = '');
$browser->put($url, array $headers = array(), $content = '');
$browser->patch($url, array $headers = array(), $content = '');

If you need a custom HTTP protocol method, you can use the send() method.

Each of the above methods supports async operation and either resolves with a Response or rejects with an Exception. Please see the following chapter about promises for more details.

Promises

Sending requests is async (non-blocking), so you can actually send multiple requests in parallel. The Browser will respond to each request with a Response message, the order is not guaranteed. Sending requests uses a Promise-based interface that makes it easy to react to when a transaction is fulfilled (i.e. either successfully resolved or rejected with an error):

$browser->get($url)->then(
    function ($response) {
        var_dump('Response received', $response);
    },
    function (Exception $error) {
        var_dump('There was an error', $error->getMessage());
    }
});

If this looks strange to you, you can also use the more traditional blocking API.

Blocking

As stated above, this library provides you a powerful, async API by default.

If, however, you want to integrate this into your traditional, blocking environment, you should look into also using clue/block-react.

The resulting blocking code could look something like this:

use Clue\React\Block;

$loop = React\EventLoop\Factory::create();
$browser = new Clue\React\Buzz\Browser($loop);

$promise = $browser->get('http://example.com/');

try {
    $response = Block\await($promise, $loop);
    // response successfully received
} catch (Exception $e) {
    // an error occured while performing the request
}

Similarly, you can also process multiple requests concurrently and await an array of Response objects:

$promises = array(
    $browser->get('http://example.com/'),
    $browser->get('http://www.example.org/'),
);

$responses = Block\awaitAll($promises, $loop);

Please refer to clue/block-react for more details.

submit()

The submit($url, array $fields, $headers = array(), $method = 'POST') method can be used to submit an array of field values similar to submitting a form (application/x-www-form-urlencoded).

send()

The send(Request $request) method can be used to send an arbitrary Request object.

withOptions()

The withOptions(array $options) method can be used to change the options to use:

$newBrowser = $browser->withOptions($options);

Notice that the Browser is an immutable object, i.e. the withOptions() method actually returns a new Browser instance with the options applied.

See options for more details.

withSender()

The withSender(Sender $sender) method can be used to change the Sender instance to use:

$newBrowser = $browser->withSender($sender);

Notice that the Browser is an immutable object, i.e. the withSender() method actually returns a new Browser instance with the given Sender applied.

See Sender for more details.

withBase()

The withBase($baseUri) method can be used to change the base URI used to resolve relative URIs to.

$newBrowser = $browser->withBase('http://api.example.com/v3');

Notice that the Browser is an immutable object, i.e. the withBase() method actually returns a new Browser instance with the given base URI applied.

Any requests to relative URIs will then be processed by first prepending the base URI. Please note that this merely prepends the base URI and does not resolve any relative path references (like ../ etc.). This is mostly useful for API calls where all endpoints (URIs) are located under a common base URI scheme.

// will request http://api.example.com/v3/example
$newBrowser->get('/example')->then(…);

See also resolve().

withoutBase()

The withoutBase() method can be used to remove the base URI.

$newBrowser = $browser->withoutBase();

Notice that the Browser is an immutable object, i.e. the withoutBase() method actually returns a new Browser instance without any base URI applied.

See also withBase().

resolve()

The resolve($uri, array $parameters = array()) method can be used to resolve the given relative URI to an absolute URI by appending it behind the configured base URI. It also replaces URI template placeholders with the given $parameters according to RFC 6570. It returns a new Uri instance which can then be passed to the HTTP methods.

URI template placeholders in the given URI string will be replaced according to RFC 6570:

echo $browser->resolve('http://example.com/{?first,second,third}', array(
    'first' => 'a',
    'third' => 'c'
));
// http://example.com/?first=a&third=c

If you pass in a relative URI string, then it will be resolved relative to the configured base URI. Please note that this merely prepends the base URI and does not resolve any relative path references (like ../ etc.). This is mostly useful for API calls where all endpoints (URIs) are located under a common base URI:

$newBrowser = $browser->withBase('http://api.example.com/v3');

echo $newBrowser->resolve('/example');
// http://api.example.com/v3/example

The URI template placeholders can also be combined with a base URI like this:

echo $newBrowser->resolve('/fetch{/file}{?version,tag}', array(
    'file' => 'example',
    'version' => 1.0,
    'tag' => 'just testing'
));
// http://api.example.com/v3/fetch/example?version=1.0&tag=just%20testing

This uses the excellent rize/uri-template library under the hood. Please refer to its documentation or RFC 6570 for more details.

Trying to resolve anything that does not live under the same base URI will result in an UnexpectedValueException:

$newBrowser->resolve('http://www.example.com/');
// throws UnexpectedValueException

Similarily, if you do not have a base URI configured, passing a relative URI will result in an InvalidArgumentException:

$browser->resolve('/example');
// throws InvalidArgumentException

Message

The Message is an abstract base class for the Response and Request. It provides a common interface for these message types.

See its class outline for more details.

Response

The Response value object represents the incoming response received from the Browser. It shares all properties of the Message parent class.

See its class outline for more details.

Request

The Request value object represents the outgoing request to be sent via the Browser. It shares all properties of the Message parent class.

See its class outline for more details.

getUri()

The getUri() method can be used to get its Uri instance.

Uri

An Uri represents an absolute URI (aka URL).

By definition of this library, an Uri instance is always absolute and can not contain any placeholders. As such, any incomplete/relative URI will be rejected with an InvalidArgumentException.

Each Request contains a (full) absolute request URI.

$request = new Request('GET', 'http://www.google.com/');
$uri = $request->getUri();

assert('http' == $uri->getScheme());
assert('www.google.com' == $uri->getHost());
assert('/' == $uri->getPath());

See its class outline for more details.

Internally, this class uses the excellent ml/iri library under the hood.

ResponseException

The ResponseException is an Exception sub-class that will be used to reject a request promise if the remote server returns a non-success status code (anything but 2xx or 3xx). You can control this behavior via the "obeySuccessCode" option.

The getCode() method can be used to return the HTTP response status code.

The getResponse() method can be used to access its underlying Response object.

Advanced

Sender

The Sender is responsible for passing the Request objects to the underlying HttpClient library and keeps track of its transmission and converts its reponses back to Response objects.

It also registers everything with the main EventLoop and the default Connector and DNS Resolver.

See also Browser::withSender() for changing the Sender instance during runtime.

DNS

The Sender is also responsible for creating the underlying TCP/IP connection to the remote HTTP server and hence has to orchestrate DNS lookups. By default, it uses a Connector instance which uses Google's public DNS servers (8.8.8.8).

If you need custom DNS settings, you explicitly create a Sender instance with your DNS server address (or React\Dns\Resolver instance) like this:

$dns = '127.0.0.1';
$sender = Sender::createFromLoopDns($loop, $dns);
$browser = $browser->withSender($sender);

See also Browser::withSender() for more details.

SOCKS proxy

You can also establish your outgoing connections through a SOCKS proxy server by adding a dependency to clue/socks-react.

The SOCKS protocol operates at the TCP/IP layer and thus requires minimal effort at the HTTP application layer. This works for both plain HTTP and SSL encrypted HTTPS requests.

See also the SOCKS example.

UNIX domain sockets

This library also supports connecting to a local UNIX domain socket path. You have to explicitly create a Sender that passes every request through the given UNIX domain socket. For consistency reasons you still have to pass full HTTP URLs for every request, but the host and port will be ignored when establishing a connection.

$path = 'unix:///tmp/daemon.sock';
$sender = Sender::createFromLoopUnix($loop, $path);
$client = new Browser($loop, $sender);

$client->get('http://localhost/demo');

Options

Note: This API is subject to change.

The Browser class exposes several options for the handling of HTTP transactions. These options resemble some of PHP's HTTP context options and can be controlled via the following API (and their defaults):

$newBrowser = $browser->withOptions(array(
    'followRedirects' => true,
    'maxRedirects' => 10,
    'obeySuccessCode' => true
));

Notice that the Browser is an immutable object, i.e. the withOptions() method actually returns a new Browser instance with the options applied.

Streaming

Note: This API is subject to change.

The Sender emits a progress event array on its Promise that can be used to intercept the underlying outgoing request stream (React\HttpClient\Request in the requestStream key) and the incoming response stream (React\HttpClient\Response in the responseStream key).

$client->get('http://www.google.com/')->then($handler, null, function ($event) {
    if (isset($event['responseStream'])) {
        /* @var $stream React\HttpClient\Response */
        $stream = $event['responseStream'];
        $stream->on('data', function ($data) { });
    }
});

Install

The recommended way to install this library is through composer. New to composer?

{
    "require": {
        "clue/buzz-react": "~0.4.0"
    }
}

License

MIT