ReactPHP

André Roaldseth - @androa

Why ReactPHP?

Event-driven non-blocking I/O in PHP. 

Event-driven?

A programming paradigm where code 
execution is triggered by events.

Non-blocking?

Program can work on other task while
 waiting for input/output operations.

Web Scraping

Generates lots of HTTP requests.
Often little or none processing.

What takes time?

andrer@dev:~/http-analyzer (master)$ ./http-analyzer analyze github.com
GET: http://github.com
IP: 192.30.252.129
Status Code: 200
Time spent in 1 redirection(s): 302 ms
Response size:        12.3 KB

  DNS Lookup:           60.9 ms
  Connecting:          188.7 ms
  Sending:               0.1 ms
  Waiting (TTFB):      637.7 ms
  Recieving:             0.6 ms
  Total time:          940.3 ms 

Actual transfer is 0.6 ms. The rest is waiting on network.

Non-blocking I/O to the rescue!

Non-blocking I/O enables you to 
work on other requests while waiting.

But how do you know when it's done?

Through Events!

When an asynchronously non-blocking operation 
has progressed, it will tell you through events.

Typical events for I/O operations are:
start
progress/update
end/complete/success
failure/error

Back to ReactPHP

Basically a port of Node.js to PHP.

Well, not really.

Provides a userland implementation
of non-blocking event-driven I/O.


Igor Wiedler - @igorwhiletrue

Some Numbers

The following graphs comes from Phil Sturgeons 
blog post about benchmarking ReactPHP vs Node.js

The Initial Test Case


The linearity of the red (and blue) line is 
caused by consistent network latency.

Wait, What? PHP being faster than JS?


Node's maxConnection setting has been tuned, 
getting it up to speed with PHP.

A simple HTTP Server with ReactPHP

<?php
require 'vendor/autoload.php';

$loop = React\EventLoop\Factory::create();
$socket = new React\Socket\Server($loop);
$http = new React\Http\Server($socket, $loop);

$app = function ($request, $response) {
    $response->writeHead(200, array('Content-Type' => 'text/plain'));
    $response->end("Hello World\n");
};

$http->on('request', $app);

$socket->listen(1337);
echo "Server running at http://127.0.0.1:1337\n";

$loop->run(); 

Streams

 In its simplest definition, a stream is a resource object which exhibits streamable behavior. 

That is, it can be read from or written to in a linear fashion, and may be able to fseek() to an arbitrary locations within the stream.
- PHP Manual

Stream Wrappers

Files
HTTP
FTP
stdin/stdout (CLI)

Memory
Compression
Process interaction
Filters

Readable Streams

Emits these events:

data
end
error
close

Provides these methods:

isReadable()
pause()
resume()
pipe(WritableStreamInterface $dest, array $options = [])

Writeable Streams

Emits these events:

drain
end
error
close

Provides these methods:

isWritable()
write($data)
end($data = null)

The Most Basic Example

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

$source = new React\Stream\Stream(fopen('omg.txt', 'r'), $loop);
$dest = new React\Stream\Stream(fopen('wtf.txt', 'w'), $loop);

$source->pipe($dest);

$loop->run(); 

Reading a Huge File

$redis = new Predis\Client();
$loop = React\EventLoop\Factory::create();

$buffer = '';

$dest = new React\Stream\Stream(fopen('local-copy.txt', 'w'), $loop);
$source = new React\Stream\Stream(fopen('http://internet/4GB-file.txt', 'r'), $loop);
$source->on('data', function($data) use (&$buffer, $redis) {
    $buffer .= $data;

    if (strpos($buffer, PHP_EOL) !== false) {
        foreach (explode(PHP_EOL, $buffer) as $line) {
            $redis->rpush('queue', $line);
        }

        $buffer = '';
    }
});

$dest->pipe($source);
$loop->run();
Reduced run time from 30 minutes to 7-8 minutes.
Yes, there is a bug in the code :)

Promises

Based on CommonJS Promises/A

Consists of three concepts:
  • Deferred
  • Promise
  • Resolver

Deferred

A Deferred represents a computation or unit of work that may not have completed yet.

Promise

While a Deferred represents the computation itself, a Promise represents the result of that computation.

Resolver

A Resolver can resolve, reject or trigger progress notifications on behalf of a Deferred without knowing any details about consumers.

Create a Promise

<?php
function doAsyncHttpRequest($url) {
    $deferred = new React\Promise\Deferred();

    // Pass only the Resolver
    fetchHttpAsynchronously($deferred->resolver(), $url);

    // Return only the Promise, so that the caller cannot
    // resolve, reject, or otherwise work with the original Deferred.
    return $deferred->promise();
} 

Act On The Promise

$promise = doAsyncHttpRequest('http://api.awesome.com/sauce/21');

$promise->then(function($apiResponse) {
    echo 'API responded with ' . $apiResponse;
});

Resolve The Promise

<?php
function doAsyncHttpRequest($resolver, $url) {
    // Perform the HTTP request asynchronously and 
    // resolve the promise using the API response.

    $resolver->resolve($response);
}

Promise API

interface PromiseInterface {
    public function then(
        callable $fulfilledHandler = null, 
        callable $errorHandler = null, 
        callable $progressHandler = null
    );
}
Returns a new Promise, making it chainable.

Resolver API

interface ResolverInterface {
    public function resolve(mixed $result = null);
    public function reject(mixed $reason = null);
    public function progress(mixed $update = null);}

When?

Used for creation, joining, mapping 
and reducing collections of Promises.

When API

$promise = React\Promise\When::all(
    array|React\Promise\PromiseInterface $promisesOrValues,
    callable $fulfilledHandler = null,
    callable $errorHandler = null,
    callable $progressHandler = null
);
$promise = React\Promise\When::any(
    array|React\Promise\PromiseInterface $promisesOrValues,
    callable $fulfilledHandler = null,
    callable $errorHandler = null,
    callable $progressHandler = null
);
$promise = React\Promise\When::some(
    array|React\Promise\PromiseInterface $promisesOrValues,
    integer $howMany,
    callable $fulfilledHandler = null,
    callable $errorHandler = null,
    callable $progressHandler = null
);

When API

$promise = React\Promise\When::map(
    array|React\Promise\PromiseInterface $promisesOrValues,
    callable $mapFunc
);
$promise = React\Promise\When::reduce(
    array|React\Promise\PromiseInterface $promisesOrValues,
    callable $reduceFunc,
    $initialValue = null
);

Partial Function Application


Pre-fills argument(s) to a given function 
and returns a new function.
function multiplyNumbers($a, $b) {
    return $a * $b;
}
$vat = 1.24;

$priceA = 100;
$priceAWithVat = multiplyNumbers($price, $vat);

$priceB = 300;
$priceBWithVat = multiplyNumbers($price, $vat);
$addVat = React\Partial\bind('multiplyNumbers', 1.24);

$priceA = 100;
$priceAWithVat = $addVat($price);

$priceB = 100;
$priceBWithVat = $addVat($price);

Partial Function Application

Useful for adding arguments you have now,
but not when the function will be used in the future.
$url = 'http://www.vg.no';

$promise = performAsyncGet($url);

function responseHandler($url, $response) {
    // Your handler wants both the $url and $response
}

$promise->then('responseHandler'); // But.. we have no URL?
$promise->then(function($data) use ($url) {
    responseHandler($url, $data);
});
$promise->then(React\Partial\bind('responseHandler', $url));

Putting it all together

A rather silly example on how to use 
the different components together.

We want to GET several URLs in parallel and
show how many bytes each where and
create a grand total.

Set up the DNS and HTTP client

<?php
require 'vendor/autoload.php';

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

$dnsResolverFactory = new React\Dns\Resolver\Factory();
$dnsResolver = $dnsResolverFactory->createCached('8.8.8.8', $loop);

$factory = new React\HttpClient\Factory();
$client = $factory->create($loop, $dnsResolver);

Create a function for
handling responses

function handleResponse($resolver, $response) {
    $body = '';

    $response->on('data', function ($data) use (&$body) {
        $body .= $data;
    });

    $response->on('end', function() use (&$body, $resolver) {
        $resolver->resolve($body);
    });
}

Prepare some HTTP requests

$promises = [];

foreach (['http://www.vg.no', 'http://www.google.com'] as $url) {
    $deferred = new React\Promise\Deferred();
    $promise = $deferred->promise();

    $promise->then(function($data) use ($url) {
        // Executed when the $url is loaded and does not know about any other requests.
        printf('%s responded with %d bytes' . PHP_EOL, $url, strlen($data));
    });

    $request = $client->request('GET', $url);
    $request->on('response', React\Partial\bind('handleResponse', $deferred->resolver()));
    $request->end();
    
    $promises[] = $promise;
}

Create a total when all
promises are fulfilled

React\Promise\When::reduce($promises,
    function ($value, $data) {
        return strlen($data) + $value;
    }
)->then(function($data) {
    printf('Downloaded %d bytes in total.' . PHP_EOL, $data);
});

Start the event loop

$loop->run();
Output should now be:
http://www.google.com responded with 258 bytes
http://www.vg.no responded with 255766 bytes
Downloaded 256024 bytes in total.
Notice how vg.no was bigger and
completed later than google.com,
even though it was started first.

Thank you!

André Roaldseth ― @androagithub/androa


Questions?

ReactPHP

By André Roaldseth

ReactPHP

  • 5,455