ReactPHP es una librería en estado alpha que nos permite implementar programación asíncrona basada en eventos. La parte más conocida es la que se encarga de levantar un servidor HTTP básico a partir de un script PHP, sin la necesidad de leer, reinstanciar y tratar el código en cada petición. Al estar ya levantado y preparado para atender las peticiones tampoco hay necesidad de hacer el bootstrap de PHP en cada petición. Es una librería añadida a nuestro proyecto, la cual podemos instanciar para generar un bucle infinito y atender todas las Requests que lleguen a un puerto determinado de la máquina. Gracias a ReactPHP podremos dotar a nuestros proyectos con su propio script de servidor y no depender de terceros para gestionar nuestras ejecuciones, ganando así control y una enorme mejora en los tiempos de respuesta. Tal y como muestran muchos benchmarks realizados con ab parece que las peticiones atendidas pueden llegar a multiplicarse varias veces. Para poder hacer alguna prueba vamos a preparar un script para servir Slim Framework, ya tengo algunas pequeñas API’s con este framework y así aprovecho y lo utilizo para mis experimentos.

Instalación

Como casi todo a día de hoy podemos hacer la instalación directamente con Composer, lo primero creamos el composer.json para el proyecto. Desde bash, en el directorio raíz del proyecto, podemos invocar directamente el comando composer init para inicializar el documento, o generarlo nosotros a mano para después hacer el install de las dependencias. El archivo json debería quedar más o menos parecido a este. Hay que tener en cuenta que yo modifico la ruta de la carpeta vendor a fin de tener todo el código PHP bajo un mismo directorio, por lo que añado la clausula config donde indico la nueva ruta.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"name": "mbarquin/react-2-slim",
"description": "Adaptación de servidor HTTP de reactPHP para Slim Framework",
"require": {
"php": "^7.0",
"react/http": "0.3.*",
"slim/slim": "^3.1",
"monolog/monolog": "1.0.*"
},
"config": {
"vendor-dir": "src/vendor"
},
"autoload": {
"psr-4": { "MyApp\\": "src/private" }
}
}

Ya solo hace falta el comando composer install para descargar las librerías y generar el archivo .lock, despues de esto pasamos a crear el script del servidor.

El desarrollo

Lo primero a tener en cuenta va a ser la forma de trabajo de ambas herramientas, por suerte tienen una dinámica de funcionamiento similar. ReactPHP ejecuta un bucle infinito en espera de una petición, en cuanto llega se ejecuta la closure asociada al evento Request, este callback recibirá como parámetros el objeto Request y Response propios de ReactPHP. Se espera que la respuesta sea inyectada en el objeto Response, las cabeceras se establecen mediante el método writeHead($responseCode, $headersArray) y el cuerpo de la respuesta puede escribirse con el método write o directamente como parámetro del método end($content=null) que se encarga de dar por terminada la generación del contenido.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Ejemplo básico de ReactPHP
require '../vendor/autoload.php';

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

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

$http->on('request', $app);
echo "Server running at http://127.0.0.1:1337\n";

$socket->listen(1337);
$loop->run();

Por su lado, Slim asocia métodos y rutas HTTP a closures que reciben como parámetros los objetos Request y Response propios de Slim, que extienden del interface standard PSR-7. Al hacer el run() de la aplicación, esta se encarga de coger automáticamente los parámetros de la petición de manera habitual para conformar los objetos que se pasan a la closure, que se ejecutará en caso de haber coincidencia con la ruta/método definido.

1
2
3
4
5
6
7
8
9
10
11
12
13
// Ejemplo básico de Slim.
require '../vendor/autoload.php';
$app = new Slim\App();

// Inyectamos una closure para resolver determinadas peticiones GET
$app->get('/hello/{name}', function ($request, $response) {
$name = $request->getAttribute('name');
$response->getBody()->write("Hello, $name");

return $response;
});

$app->run();

Script de servidor

Sencillamente vamos a asociar una closure al servidor ReactPHP que se encargará de que Slim procese la petición, pasará los datos de los objetos de React a Slim y ejecutará el proceso que atravesando todo el middleware resuelva el callback asociado a la ruta. Aprovecharemos la función process que es el núcleo del método run() y nos permite ejecutar Slim con el Request y Response que nosotros le inyectemos, sin que el framework los genere para nosotros.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
require '../vendor/autoload.php';
use MyApp\Http;

// Generamos una nueva instancia de Slim
$app = new \Slim\App();

// Añadimos una closure para atender determinadas peticiones GET
$app->get('/hello/{name}', function (
\MyApp\Http\Request $request,
\MyApp\Http\Response $response) {

$name = $request->getAttribute('name');
$response->getBody()->write("Hello, $name");

return $response;
});

// Creamos otra closure para resolver las requests.
$serverCallback = function (
\React\Http\Request $request,
\React\Http\Response $response) use ($app){

$slRequest = \MyApp\Http\Request::createFromReactRequest($request);
$slResponse = new \MyApp\Http\Response();

$app->process($slRequest, $slResponse);

$slResponse->setReactResponse($response, true);
};

// Hacemos el setup y levantamos el servidor React
$loop = React\EventLoop\Factory::create();
$socket = new React\Socket\Server($loop);
$http = new React\Http\Server($socket, $loop);

// Ligamos la closure al evento request.
$http->on('request', $serverCallback);

echo "Server running at http://127.0.0.1:1337\n";

$socket->listen(1337);
$loop->run();

Como se puede ver aquí mezclamos ambos códigos, por un lado hacemos el setup de Slim ($app) básico para atender determinadas peticiones GET. Creamos el callback de ReactPHP encargado de recibir el evento request y replicar la llamada al framework Slim. Desde esta closure le inyectamos como parámetros unos objetos Request y Response customizados que hemos extendido directamente de los nativos de Slim. Solamente para añadirles unas funciones que permitan trasladar los datos entre ambos sistemas, por lo que deberían funcionar exactamente igual. Estos extenderían los mismos interfaces que los nativos de Slim por lo que no debería haber problemas al atravesar la estructura del framework. Todavía faltan cosas por hacer, como trasladar la información del Body, las Cookies o los ficheros subidos por las Requests, intentaré pasar estos objetos a Github a fin de seguir haciendo algunos tests e intentaré implementar las cosas que faltan, pero para nuestro ejemplo actual, con esto llega. Tras definir este callback parametrizamos y hacemos el run() del servidor.

Con esto solo nos queda ejecutar el script para que se quede escuchando igual que lo haría apache o nginx, lanzamos el comando php server.php en la misma carpeta que se encuentra el fuente y abrimos en el navegador la url http://localhost:1337/hello/Moi. Hay que tener cuidado de llamar a esta ruta ya que es la única que va a devolver resultados.

Extendiendo la request

Para la Request he generado un método estático para alimentar la instancia de Slim, de la misma manera que en el objeto origen tienen el método estático createFromEnvironment.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
namespace MyApp\Http;

use Slim\Http\Headers;
use Slim\Http\Uri;
use Slim\Http\RequestBody;

use React\Http\Request;

class Request extends \Slim\Http\Request
{
static public function createFromReactRequest(\React\Http\Request $request)
{
$slimHeads = new Headers();
foreach($request->getHeaders() as $reactHeadKey => $reactHead) {
$slimHeads->add($reactHeadKey, $reactHead);
if($reactHeadKey === 'Host') {
$host = explode(':', $reactHead);
if(count($host) === 1) {
$host[1] = '80';
}
}
}

$slimUri = new Uri('http', $host[0], (int)$host[1], $request->getPath(), $request->getQuery());

$cookies = [];
$serverParams = $_SERVER;
$serverParams['SERVER_PROTOCOL'] = 'HTTP/'.$request->getHttpVersion();

$slimBody = new RequestBody();

return new self(
$request->getMethod(), $slimUri, $slimHeads, $cookies,
$serverParams, $slimBody
);
}
}

Extendiendo la response

A la Reponse, a través de herencia, le añadimos el método público setReactResponse($response, $endResponse=false) con el que traspasamos la información del objeto actual al objeto \React\Http\Response que va a transmitir el resultado de la operación al cliente. Como primer parámetro le pasaremos la Response de ReactPHP, que nos llega como parámetro de la closure, para inyectarle los datos del resultado. Y, como segundo parámetro le facilitaremos un booleano para indicar la ejecución del método end() de la Response React. Con esto cerramos la transmisión y volcamos el contenido al navegador.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
namespace MyApp\Http;

class Response extends \Slim\Http\Response
{
public function setReactResponse(\React\Http\Response $reactResp, $endRequest = false)
{
$reactResp->writeHead($this->getStatusCode(), $this->getHeaders());
$reactResp->write($this->getBody());

if ($endRequest === true) {
$reactResp->end();
}
}
}

Y aqui estamos

Con esto tenemos una setup básica con la que utilizar Slim en conjunción con ReactPHP, de momento React es alpha y no apto para ser usado en producción, de verdad tengo ganas de ver la release estable y empezar a trabajar de forma habitual con él, las ventajas que plantea son muy similares a las que han posicionado tan bien a NodeJs en tan poco tiempo aunque no debemos perder de vista sus limitaciones.

  • Este sistema deberá ser reiniciado tras hacer cualquier cambio en los fuentes
  • Si realmente queremos tener un sistema eficiente deberíamos balancear diferentes instancias tras un nginx (p.e.) para optimizar el rendimiento de la máquina.
  • La forma de trabajar de muchos de los frameworks actuales puede no ser compatible con la forma de trabajo de ReactPHP (Slim o Symfony parecen no dar problemas).
  • Una exception no tratada o cualquier error inesperado puede detener el servidor.

En mis tiempos libres intentaré completar los sources para transmitir correctamente las Cookies, los archivos subidos y el contenido del Body, en cuanto pueda crearé un proyecto en Github para poder mejorar y utilizar estos fuentes.