Getting started with ReactPHP

    Cees-Jan Kiewiet

  • @WyriHaximus
  • ReactPHP Team member
  • Sculpin Team member
  • Lead Software Engineer at 

 

 

Why ReactPHP

Building a sample app:

  • Analyzes hostnames
  • Looks up IP address
  • Fetches page title
  • Fetches GeoIP

Demo time!

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

ReactJS

(Just a little bit of it)

var React = require('react');

var state = {};
state['title'] = '';
state['ip'] = '';
state['geo'] = '';

var getState = function () {
    return state;
};

var AppComponent = React.createClass({
    getInitialState: function () {
        return getState();
    },
    onChange: function(e) {
        if (e.target.value == '') {
            return;
        }

        var request = new XMLHttpRequest();
        request.onreadystatechange = function () {};
        request.open('GET', '/lookup.json?host=' + e.target.value, true);
        request.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
        request.send(null);
    },
    render: function () {
        return <div className="grid-container">
            <input onChange={this.onChange} />
            <div>
                <div className="grid-50">
                    <h1>IP</h1>
                    <h2>{this.state.dns}</h2>
                </div>
                <div className="grid-50">
                    <h1>Title</h1>
                    <h2>{this.state.title}</h2>
                </div>
            </div>
            <div>
                <div className="grid-100">
                    <h1>Location</h1>
                    <h2>{this.state.geo}</h2>
                </div>
            </div>
        </div>;
    },
    componentDidMount: function () {
        var es = new EventSource('sse');
        es.addEventListener('message', function (event) {
            var message = JSON.parse(event.data);
            console.log(message);

            state[message.type] = message.payload;
            this.setState(getState());
        }.bind(this));
    }
});

module.exports = AppComponent;

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

Any questions about this bit?

  • Event loop 
  • Event listeners
  • Filesystem
  • HTTP server
  • DNS lookups
  • HTTP Client

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)
);

Any questions about this bit?

  • Event loop 
  • Event listeners 
  • Filesystem
  • HTTP server
  • DNS lookups
  • HTTP Client

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

Any questions about this bit?

  • Event loop 
  • Event listeners 
  • Filesystem 
  • HTTP server
  • DNS lookups
  • HTTP Client

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);
    $response->writeHead(200);
    $response->end('{}');
}

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);
        });
    }
}

Any questions about this bit?

  • Event loop 
  • Event listeners 
  • Filesystem 
  • HTTP server 
  • DNS lookups
  • HTTP Client

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,
                ]);
            });
        });
    }
}

Any questions about this bit?

  • Event loop 
  • Event listeners 
  • Filesystem 
  • HTTP server 
  • DNS lookups 
  • HTTP Client

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(),
                ]);
            });
        });
    }
}

Any questions about this bit?

  • Event loop 
  • Event listeners 
  • Filesystem 
  • HTTP server 
  • DNS lookups 
  • HTTP Client 

Questions?

Please rate this talk at:

https://joind.in/15007

  • Slides: https://slides.com/wyrihaximus/getting-started-with-reactphp-010php
  • Demo source: https://github.com/WyriHaximus/reactphp-reactjs-hostname-analyzer-example
  • Joind.in: https://joind.in/15007
  • Blog posts: http://blog.wyrihaximus.net/categories/reactphp-series/
  • ReactPHP: http://reactphp.org/

Getting started with ReactPHP (010PHP)

By wyrihaximus

Getting started with ReactPHP (010PHP)

  • 2,092