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,214