Getting started with ReactPHP
Cees-Jan Kiewiet
- @WyriHaximus
- ReactPHP Team member
- Sculpin Team member
- Lead Software Engineer at Vastgoeddata
Building a sample app:
- Analizes hostnames
- Looks up IP address
- Fetches page title
- Fetches GeoIP
Requirements:
- react/event-loop
- league/event
- react/http
- react/filesystem
- react/dns
- wyrihaximus/react-guzzle-ring
- clue/sse-react
- reactjs
All available at: https://github.com/WyriHaximus/reactphp-reactjs-hostname-analyzer-example
composer.json
{
"require": {
"react/dns": "^0.4.1",
"wyrihaximus/react-guzzle-ring": "^1.0",
"react/http": "^0.4.0",
"react/filesystem": "dev-master",
"league/event": "^2.1",
"clue/sse-react": "dev-master"
},
"autoload": {
"psr-4": {
"WyriHaximus\\React\\Examples\\HostnameAnalyzer\\": "src"
}
}
}
react/event-loop
- read/write streams
- timers
- future/next ticks
<?php
use React\EventLoop\Factory;
require 'vendor/autoload.php';
$loop = Factory::create();
$loop->run();
Setting up the event loop
league/event
Adding listeners
$emitter = new Emitter();
$emitter->useListenerProvider(
new TitleListener($emitter, $loop, $dns)
);
$emitter->useListenerProvider(
new DnsListener($emitter, $dns)
);
$emitter->useListenerProvider(
new GeoListener($emitter, $guzzle, $dns)
);
$emitter->useListenerProvider(
new ChannelListener($emitter, $channel)
);
react/filesystem
- currently using EIO (ext-eio)
- utilizing threads to perform operations
- simple yet very powerful API
Warning!
Work in Progress!!!
define('WEBROOT', __DIR__ . DIRECTORY_SEPARATOR . 'webroot');
$filesystem = Filesystem::create($loop);
$files = $filesystem->dir(WEBROOT)->ls();
Listing all files to serv
react/http
- simple HTTP server
- build on streams
$socket = new SocketServer($loop);
$http = new HttpServer($socket, $loop);
$http->on('request', new ResponseHandler(
$files,
$filesystem,
$emitter,
$channel
));
$socket->listen(1337);
Setting up the HTTP server
public function __invoke(Request $request, Response $response) {
if ($request->getPath() == self::SSE_PATH) {
$this->handleSse($request, $response);
return;
}
if ($request->getPath() == self::LOOKED_PATH) {
$this->handleLookup($response, $request->getQuery()['host']);
return;
}
$this->files->then(function (\SplObjectStorage $files) use ($request, $response) {
foreach ($files as $file) {
if ($file->getPath() == WEBROOT . $request->getPath()) {
$this->handleFile($file, $response);
return;
}
}
$this->handleFile(
$this->filesystem->file(WEBROOT . DIRECTORY_SEPARATOR . '404.txt'),
$response
);
return;
});
}
Setting up request handling
protected function handleFile(File $file, Response $response)
{
if (isset($this->filesContents[$file->getPath()])) {
return $this->filesContents[$file->getPath()];
}
$file->getContents()->then(function ($contents) use ($file) {
$this->filesContents[$file->getPath()] = $contents;
return $file->close()->then(function () use ($contents) {
return $contents;
});
})->then(function ($fileContents) use ($response) {
$response->writeHead(200);
$response->end($fileContents);
});
}
Handling file fetch
protected function handleLookup(Response $response, $hostName)
{
$this->emitter->emit('lookup', $hostName);
$this->handleFile(
$this->filesystem->file(WEBROOT . DIRECTORY_SEPARATOR . 'lookup.json'),
$response
);
}
Emitting the hookup event
protected function handleSse(Request $request, Response $response)
{
$headers = $request->getHeaders();
$id = isset($headers['Last-Event-ID']) ? $headers['Last-Event-ID'] : null;
$response->writeHead(200, array('Content-Type' => 'text/event-stream'));
$this->channel->connect($response, $id);
$response->on('close', function () use ($response) {
$this->channel->disconnect($response);
});
}
Handling SSE
<?php
namespace WyriHaximus\React\Examples\HostnameAnalyzer;
use Clue\React\Sse\BufferedChannel;
use League\Event\Emitter;
use React\Filesystem\Filesystem;
use React\Filesystem\Node\File;
use React\Http\Request;
use React\Http\Response;
use React\Promise\RejectedPromise;
class ResponseHandler
{
const LOOKED_PATH = '/lookup';
const SSE_PATH = '/sse';
protected $files;
/**
* @var Filesystem
*/
protected $filesystem;
/**
* @var Emitter
*/
protected $emitter;
/**
* @var BufferedChannel
*/
protected $channel;
protected $filesContents = [];
public function __construct($files, Filesystem $filesystem, Emitter $emitter, BufferedChannel $channel)
{
$this->files = $files;
$this->filesystem = $filesystem;
$this->emitter = $emitter;
$this->channel = $channel;
}
public function __invoke(Request $request, Response $response) {
if ($request->getPath() == self::SSE_PATH) {
$this->handleSse($request, $request);
return;
}
if ($request->getPath() == self::LOOKED_PATH) {
$this->handleLookup($response, $request->getQuery()['host']);
return;
}
$this->files->then(function (\SplObjectStorage $files) use ($request, $response) {
foreach ($files as $file) {
if ($file->getPath() == WEBROOT . $request->getPath()) {
$this->handleFile($file, $response);
return;
}
}
$this->handleFile($this->filesystem->file(WEBROOT . DIRECTORY_SEPARATOR . '404.txt'), $response);
return;
});
}
protected function handleFile(File $file, Response $response)
{
if (isset($this->filesContents[$file->getPath()])) {
return $this->filesContents[$file->getPath()];
}
$file->getContents()->then(function ($contents) use ($file) {
$this->filesContents[$file->getPath()] = $contents;
return $file->close()->then(function () use ($contents) {
return $contents;
});
})->then(function ($fileContents) use ($response) {
$response->writeHead(200);
$response->end($fileContents);
});
}
protected function handleLookup(Response $response, $hostName)
{
$this->emitter->emit('lookup', $hostName);
$this->handleFile($this->filesystem->file(WEBROOT . DIRECTORY_SEPARATOR . 'lookup.json'), $response);
}
protected function handleSse(Request $request, Response $response)
{
$headers = $request->getHeaders();
$id = isset($headers['Last-Event-ID']) ? $headers['Last-Event-ID'] : null;
$response->writeHead(200, array('Content-Type' => 'text/event-stream'));
$this->channel->connect($response, $id);
$response->on('close', function () use ($response) {
$this->channel->disconnect($response);
});
}
}
react/dns
- Resolves hostnames to IP addresses
- Picks a random IP address from Round-robin
$dns = (new \React\Dns\Resolver\Factory())->createCached('8.8.8.8', $loop);
Setting up the resolver
$this->resolver->resolve($hostname)->then(function ($ip) {
$this->emitter->emit('ip', $ip);
$this->emitter->emit('sse', [
'type' => 'dns',
'payload' => $ip,
]);
});
Resolving an IP address
<?php
namespace WyriHaximus\React\Examples\HostnameAnalyzer\Listeners;
use League\Event\Emitter;
use League\Event\ListenerAcceptorInterface;
use League\Event\ListenerProviderInterface;
use React\Dns\Resolver\Resolver;
class DnsListener implements ListenerProviderInterface
{
/**
* @var Emitter
*/
protected $emitter;
/**
* @var Resolver
*/
protected $resolver;
public function __construct(Emitter $emitter, Resolver $resolver)
{
$this->emitter = $emitter;
$this->resolver = $resolver;
}
public function provideListeners(ListenerAcceptorInterface $acceptor)
{
$acceptor->addListener('lookup', function ($event, $hostname) {
$this->resolver->resolve($hostname)->then(function ($ip) {
$this->emitter->emit('ip', $ip);
$this->emitter->emit('sse', [
'type' => 'dns',
'payload' => $ip,
]);
});
});
}
}
wyrihaximus/react-guzzle(|-ring|-psr7)
Text
- Bridging react/http-client into Guzzle
- Resulting in a clean highlevel API
$guzzle = new Client([
'handler' => new HttpClientAdapter($loop, null, $dns),
]);
Setting up Guzzle
$this->client->get('http://' . $hostname . '/', [
'future' => true,
])->then(function (Response $response) {
if (preg_match(
'/<title>(.+)<\/title>/',
$response->getBody()->getContents(),
$matches
) && isset($matches[1])) {
$title = $matches[1];
$this->emitter->emit('sse', [
'type' => 'title',
'payload' => $title,
]);
}
});
Fetching the title of the page
<?php
namespace WyriHaximus\React\Examples\HostnameAnalyzer\Listeners;
use GuzzleHttp\Client;
use GuzzleHttp\Message\Response;
use League\Event\Emitter;
use League\Event\ListenerAcceptorInterface;
use League\Event\ListenerProviderInterface;
class TitleListener implements ListenerProviderInterface
{
/**
* @var Emitter
*/
protected $emitter;
/**
* @var Client
*/
protected $client;
/**
* @param Client $client
*/
public function __construct(Emitter $emitter, CLient $client)
{
$this->emitter = $emitter;
$this->client = $client;
}
public function provideListeners(ListenerAcceptorInterface $acceptor)
{
$acceptor->addListener('lookup', function ($event, $hostname) {
$this->client->get('http://' . $hostname . '/', [
'future' => true,
])->then(function (Response $response) {
if (preg_match('/<title>(.+)<\/title>/', $response->getBody()->getContents(), $matches) && isset($matches[1])) {
$title = $matches[1];
$this->emitter->emit('sse', [
'type' => 'title',
'payload' => $title,
]);
}
});
});
}
}
$this->client->get('https://freegeoip.net/json/' . $ip, [
'future' => true,
])->then(function (Response $response) {
$this->emitter->emit('sse', [
'type' => 'geo',
'payload' => $response->json(),
]);
});
Fetching GeoIP
<?php
namespace WyriHaximus\React\Examples\HostnameAnalyzer\Listeners;
use GuzzleHttp\Client;
use GuzzleHttp\Message\Response;
use League\Event\Emitter;
use League\Event\ListenerAcceptorInterface;
use League\Event\ListenerProviderInterface;
class GeoListener implements ListenerProviderInterface
{
/**
* @var Emitter
*/
protected $emitter;
/**
* @var Client
*/
protected $client;
/**
* @param Client $client
*/
public function __construct(Emitter $emitter, CLient $client)
{
$this->emitter = $emitter;
$this->client = $client;
}
public function provideListeners(ListenerAcceptorInterface $acceptor)
{
$acceptor->addListener('ip', function ($event, $ip) {
$this->client->get('https://freegeoip.net/json/' . $ip, [
'future' => true,
])->then(function (Response $response) {
$this->emitter->emit('sse', [
'type' => 'geo',
'payload' => $response->json(),
]);
});
});
}
}
Demo time!
Questions?
Please rate this talk at:
https://joind.in/event/view/3942
Getting started with ReactPHP (PHP-FRL)
By wyrihaximus
Getting started with ReactPHP (PHP-FRL)
- 2,202