Introduction à ReactPHP

Event-driven, non-blocking I/O avec PHP

@JamesHemery

Hello 👋

@JamesHemery

🫣   James Hemery

👨‍💻  CTO @ Yield Studio

🫶  Amoureux des chats, de PHP,

     Laravel et Javascript, ReactJS

De l'asynchrone en PHP ?

@JamesHemery

  • Langage synchrone
  • Principalement connu pour un cycle requête / réponse
  • PHP est lent ?

 

Mais... Aujourd'hui on peut utiliser PHP pour bien plus qu'un cycle req/res :

  • API
  • Live streaming
  • Console application

 

Sync. vs Async.

@JamesHemery

Synchronous

Asynchronous

Query 1

Traitement

Query 2

Traitement

Query 1

Traitement

Query 2

Traitement

Temps

L'idée derrière ReactPHP

@JamesHemery

  • Les calculs CPU sont rapides
  • Les opérations I/O sont lentes
  • Travailler avec des opérations I/O non bloquantes

 

Opérations bloquantes:

  • Filesystem
  • Base de données (MySQL, Redis..)
  • API tiers (REST, SOAP, Graphql...)
  • Librairies tierces
  • Beaucoup des fonctions natives

ReactPHP, kesako ?

@JamesHemery

  • PHP Asynchrone & I/O non bloquante
  • Ensemble de composants
  • Reactor Pattern (comme Node.js)
  • Pure PHP
  • Production ready & LTS

Notre programme

@JamesHemery

Event Loop

La boucle d'événement de ReactPHP basé sur le pattern Reactor, que les bibliothèques peuvent utiliser pour les I/O.

Stream

Lecture et écriture de flux pilotés par des évènements pour des I/O non bloquantes.

Promise

Implémentation de la spécification Promise/A en PHP

Socket

Client et serveur socket TCP/IP et TLS, plaintext et asynchrones pour ReactPHP

Event Loop

@JamesHemery

  • Cœur d'une application asynchrone
  • Composant bas niveau
  • Ne pas confondre asynchrone et parallèle

Event Loop

@JamesHemery

Timers

  • unique
  • périodique
  • ticks (unité de temps sur laquelle les timers se basent)

 

Streams event

  • lecture
  • écriture

 

Signals

Timers

@JamesHemery

<?php

use React\EventLoop\Loop;

// Ne s'executera qu'une fois
Loop::addTimer(1, function () {
    echo 'Hello AFUP Tours' . PHP_EOL;
});

// S'execute toutes les x secondes
$i = 0;
Loop::addPeriodicTimer(2, function ($timer) use (&$i) {
    echo 'Alors, comment ça va ?' . PHP_EOL;

    if ($i++ === 2) {
        Loop::cancelTimer($timer);
    }
});

// Basé sur les ticks
Loop::futureTick(function(){
    echo '...';
});

Récupérer l'event loop

@JamesHemery

<?php

use React\EventLoop\Loop;

$loop = Loop::get(); // Factory::create();

$loop->addTimer(1, function () {
    echo 'Hello AFUP Tours' . PHP_EOL;
});

$i = 0;
$loop->addPeriodicTimer(2, function ($timer) use ($loop, &$i) {
    echo 'Alors, comment ça va ?' . PHP_EOL;

    if ($i++ === 2) {
        $loop->cancelTimer($timer);
    }
});

Stream

@JamesHemery

<?php

use React\EventLoop\Loop;

Loop::addReadStream(STDIN, function ($stream) {
    echo 'Vous avez dit: ' . fread($stream, 1024);
});

// Vous pouvez aussi utiliser Loop::addWriteStream

Signals

@JamesHemery

<?php

use React\EventLoop\Loop;

Loop::addSignal(SIGINT, $func = function () use (&$func) {
    echo 'Au revoir !';
    Loop::removeSignal(SIGINT, $func);
});

Opérations bloquantes

@JamesHemery

⚠️ Attention : l'event loop est démarré en permanence cependant, les longues opérations bloquantes bloquent la loop.

<?php

use React\EventLoop\Loop;

Loop::addTimer(1, function ()  {
    echo 'Après 1 seconde...';
});

while (true) {
    //
}

Opérations bloquantes

@JamesHemery

Streams

@JamesHemery

Permets de traiter un grand nombre de données au travers de chunks

 

Il existe trois types :

  • Readable (Ex: STDIN)
  • Writable (Ex: STDOUT)
  • Duplex (Ex: Socket)

Lecture / Écriture

@JamesHemery

<?php

use React\EventLoop\Loop;
use React\Stream\ReadableResourceStream;
use React\Stream\WritableResourceStream;

$loop = Loop::get();

$out = new WritableResourceStream(STDOUT, $loop);
$in = new ReadableResourceStream(STDIN, $loop);

$in->on('data', function ($data) use ($out) {
    $out->write('Vous avez dit : ' . $data);
});

Composite

@JamesHemery

<?php

use React\EventLoop\Loop;
use React\Stream\CompositeStream;
use React\Stream\ReadableResourceStream;
use React\Stream\WritableResourceStream;

$loop = Loop::get();

$out = new WritableResourceStream(STDOUT, $loop);
$in = new ReadableResourceStream(STDIN, $loop);

$composite = new CompositeStream($in, $out);

$composite->on('data', function($data) use ($composite) {
   $composite->write('Vous avez dit : ' . $data);
});

Pipe

@JamesHemery

<?php

use React\EventLoop\Loop;
use React\Stream\ReadableResourceStream;
use React\Stream\ThroughStream;
use React\Stream\WritableResourceStream;

$loop = Loop::get();

$out = new WritableResourceStream(STDOUT, $loop);
$in = new ReadableResourceStream(STDIN, $loop);

$uppercase = new ThroughStream(function($data){
    return strtoupper($data);
});

$in->pipe($uppercase)->pipe($out);

Exemple : Lecture de fichier

@JamesHemery

<?php

use React\EventLoop\Loop;
use React\Stream\ReadableResourceStream;
use React\Stream\WritableResourceStream;

$loop = Loop::get();

$readable = new ReadableResourceStream(fopen('file.txt', 'r'), $loop, 100);
$output = new WritableResourceStream(STDOUT, $loop);

$readable->on('data', function($data) use ($output){
    $output->write('Chunk : ' . $data . PHP_EOL);
});

$readable->on('end', function() use ($output) {
    $output->write('Fin');
    $output->end();
});

Promises

@JamesHemery

Permets de réagir à une opération asynchrone.

Implémenter selon la spécification Promise/A.

 

Une promise à trois états possible:

  • Pending
  • Fulfilled
  • Rejected

Promise

@JamesHemery

<?php

use React\Promise\Deferred;
use function React\Promise\Timer\sleep;

asynchronousFn()->then(
    function ($value) {
        echo $value . PHP_EOL;
    },
    function ($reason) {
        echo 'Erreur : ' . $reason . PHP_EOL;
    }
);

echo 'Autre chose...' . PHP_EOL;

Exemple : 30 calls HTTP

@JamesHemery

<?php

// L'exemple classique

for ($i = 0; $i < 30; $i++) {
    $body = file_get_contents('https://catfact.ninja/fact');
    $data = json_decode($body, true);
    echo $data['fact'] . PHP_EOL;
}

// Avec ReactPHP

use Psr\Http\Message\ResponseInterface;
use React\Http\Browser;

$browser = new Browser();

for ($i = 0; $i < 30; $i++) {
    $browser->get('https://catfact.ninja/fact')->then(function(ResponseInterface $response){
        $data = json_decode($response->getBody(), true);
        echo $data['fact'] . PHP_EOL;
    });
}

Socket

@JamesHemery

Client et serveur socket TCP/IP et TLS, plaintext et asynchrones pour ReactPHP.

 

Permets d'implémenter des protocoles réseaux (HTTP par exemple) et de se connecter à d'autres réseaux (HTTP, Base de données...)

Créer un serveur socket

@JamesHemery

<?php

use React\EventLoop\Loop;
use React\Socket\ConnectionInterface;
use React\Socket\SocketServer;
use React\Stream\WritableResourceStream;

$loop = Loop::get();

$socket = new SocketServer('127.0.0.1:8010');

$out = new WritableResourceStream(STDOUT, $loop);

$socket->on(
    'connection', 
    function (ConnectionInterface $connection) use ($out) {
        $connection->on('data', function ($data) use ($connection, $out) {
        	$out->write('Nouveau message : ' . $data);
    	});
    }
);

Un tchat ?

@JamesHemery

<?php

use React\Socket\ConnectionInterface;
use React\Socket\LimitingServer;
use React\Socket\SocketServer;

$socket = new SocketServer('127.0.0.1:8010');
$socket = new LimitingServer($socket, null);

$socket->on(
    'connection',
    function (ConnectionInterface $connection) use ($socket) {
        $connection->on('data', function ($data) use ($connection, $socket) {
            if ($data === '') {
                return;
            }

            $data = $connection->getLocalAddress() . ' à dit : ' . $data;
            foreach ($socket->getConnections() as $c) {
                $c->write($data . PHP_EOL);
            }
        });
    }
);

Un serveur HTTP ?

@JamesHemery

<?php

use React\Socket\ConnectionInterface;
use React\Socket\SocketServer;

$http = new SocketServer('127.0.0.1:8080');
$http->on('connection', function (ConnectionInterface $connection) {
    $connection->once('data', function () use ($connection) {
        $body = '<h1>Hello AFUP Tours</h1>';
        $connection->end("HTTP/1.1 200 OK\r\nContent-Length: " . strlen($body) . "\r\nConnection: close\r\n\r\n" . $body);
    });
});

Envoyer des données directement au navigateur ?

@JamesHemery

🔥 Envie d'en savoir plus ?

@JamesHemery

⚡️ Ils utilisent ReactPHP

@JamesHemery

Merci !

Des questions ?

@JamesHemery

Introduction à ReactPHP

By jameshemery

Introduction à ReactPHP

  • 98