Wie man eine RESTful API mit ReactPHP baut (2019)

Auch im Jahr 2019 ist PHP noch nicht tot. Wir zeigen, wie man mit ReactPHP einen simplen HTTP Server für eine REST API baut.

ReactPHP ist eine Low-Level Bibliothek für die ereignisgesteuerte Programmierung in PHP. Das Herzstück ist eine Ereignisschleife, über die Low-Level-Dienstprogramme bereitgestellt werden, z. B .: Streams abstraction, async DNS resolver, network client/server, HTTP client/server und Interaktion mit Prozessen.

Wir wollen nun also eine RESTful API mittels ReactPHP und dem Routing-Modul „nikic/FastRoute“ erstellen.

Vorbereitung

Hier ist unsere Dateistruktur. Wir werden nicht viele Dateien benötigen damit dieses Beispiel einfach bleibt.

– src/ // project files
– index.php // Entry point zur Application
– composer.json // Composer-Settings – Komponenten und Abhängigkeiten
– vendor/ // von Composer erstellt, beinhaltet Komponenten und Abhängigkeiten

Zunächst installieren wir Composer im Projektverzeichnis. Eine ältere Anleitung findet man hier.


php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
php composer-setup.php
php -r "unlink('composer-setup.php');"

 

Composer wurde erfolgreich im Projektverzeichnis installiert

 

Nun benötigen wir zwei Pakete:

Wir installieren nun die beiden Pakete mit Composer.


php composer.phar require react/http

Installation von react/http mit allen Abhängigkeiten

 


php composer.phar require nikic/fast-route

Installation von nikic/fast-route mittels Composer

Die composer.json wird automatisch angelegt. Diese öffnen wir nun und ergänzen die Autoload-Optionen.
Das sorgt dafür, dass alle Klassen aus dem src-Verzeichnis auf den Namespace App gemappt werden.


{
  "require": {
    "react/http": "^0.8.4",
    "nikic/fast-route": "^1.3"
  },
  "autoload": {
    "psr-4": {
      "App\\": "src/"
    }
  }
}

 

Server

Zunächst benötigen wir einen laufenden Server, der eingehende Anfragen bearbeitet. Dazu erstellen wir einen leeren HTTP-Server in der index.php.
Der macht vorerst nix weiter, als ‚Hallo‘ zurückzugeben.

use React\Http\Response;
use React\Http\Server;
use React\MySQL\Factory;

require __DIR__ . '/vendor/autoload.php';

$loop = \React\EventLoop\Factory::create();

$hello = function () {
return new Response(200, ['Content-type' => 'text/plain'], 'Hello');
};

$server = new Server($hello);
$socket = new \React\Socket\Server('127.0.0.1:8082', $loop);
$server->listen($socket);

echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL;
$loop->run();

Das ist der Entry-Point der API.

Den Server können wir nun von der Kommandozeile starten.

php index.php

Jetzt können wir den Server testen und einen Request schicken.

GET http://127.0.0.1:8082

Und wir sollten folgenden Response erhalten:


HTTP/1.1 200 OK
Content-type: text/plain
X-Powered-By: React/alpha
Date: Mon, 08 Jul 2019 12:26:47 GMT
Content-Length: 5
Connection: close

Hello

Routing

Jetzt erstellen wir die Routen, um die Requests weiterzuleiten und verarbeiten zu können.
Dazu erstellen wir im src-Ordner eine neue Datei Router.php.

namespace App;
use FastRoute\Dispatcher;
use FastRoute\Dispatcher\GroupCountBased;
use FastRoute\RouteCollector;
use LogicException;
use Psr\Http\Message\ServerRequestInterface;
use React\Http\Response;
final class Router
{
    private $dispatcher;
    public function __construct(RouteCollector $routes)
    {
        $this->dispatcher = new GroupCountBased($routes->getData());
    }
    public function __invoke(ServerRequestInterface $request)
    {
        $routeInfo = $this->dispatcher->dispatch($request->getMethod(), $request->getUri()->getPath());
        switch ($routeInfo[0]) {
            case Dispatcher::NOT_FOUND:
                return new Response(404, ['Content-Type' => 'text/plain'], 'Not found');
            case Dispatcher::METHOD_NOT_ALLOWED:
                return new Response(405, ['Content-Type' => 'text/plain'], 'Method not allowed');
            case Dispatcher::FOUND:
                $params = $routeInfo[2];
                return $routeInfo[1]($request, ... array_values($params));
        }
        throw new LogicException('Something wrong with routing');
    }
}

Das ist ein Middleware-Wrapper, der auf FastRoute audsetzt.
Diese Middleware kapselt Routen und überprüft Methode und Pfad der Anforderung. $routeInfo[0] enthält das Ergebnis des Abgleichs. Wenn die Anfrage mit einer der definierten Routen übereinstimmt, führen wir einen entsprechenden Controller mit einem Anfrageobjekt und übereinstimmenden Parametern aus. Andernfalls geben wir 404 oder 405 Antworten zurück.

 

Unsere API soll JSON-Responses zurückgeben. Um Wiederholungen der Logik zur Erstellung von Antworten zu vermeiden, ertellen wir eine eigene JsonResponse-Klasse, ein Wrapper über React\Http\Response. Sie akzeptiert einen Statuscode und die Daten, die wir zurückgeben möchten. Dazu erstellen wir eine Datei JsonResponse.php im src-Ordner:


namespace App;
use React\Http\Response;
final class JsonResponse extends Response
{
    public function __construct(int $statusCode, $data = null)
    {
        var_dump($data);
        $body = $data ? json_encode($data) : null;
        parent::__construct($statusCode, ['Content-Type' => 'application/json'], $body);
    }
    public static function ok($data = null): self
    {
        return new self(200, $data);
    }
    public static function noContent(): self
    {
        return new self(204);
    }
    public static function created(): self
    {
        return new self(201);
    }
    public static function badRequest(string $error): self
    {
        return new self(400, ['error' => $error]);
    }
    public static function notFound(string $error): self
    {
        return new self(404, ['error' => $error]);
    }
}

Jetzt können wir die Controller bauen, die die Anfragen vom Router übernehmen, verarbeiten und die Daten für die Antwort erstellen.
In diesem Beispiel wollen wir nur eine Liste von Items zurück geben.
Dazu erstellen wir unter src/Controller eine neue Datei ListItems.php.
Im Konstruktor erzeugen wir Beispieldaten, die wir bei Anfrage als JsonResponse zurückgeben.


namespace App\Controller;
use App\JsonResponse;
use Psr\Http\Message\ServerRequestInterface;
final class ListItems
{
    private $items;
    public function __construct()
    {
        $this->items = array();
        for($i = 0; $i<10; $i++) {
            $item = array(
                'id' => $i,
                'name' => "item-" . $i
            );
            $this->items[] = $item;
        }
    }
    public function __invoke(ServerRequestInterface $request)
    {
        return JsonResponse::ok($this->items);
    }
}

 

Nun können wir der index.php eine Route hinzufügen.
Beim Aufruf der Api über „/items“ sollen alle Items des ListItems-Controllers zurückgegeben werden.

$routes = new RouteCollector(new Std(), new GroupCountBased());
$routes->get('/items', new \App\Controller\ListItems());

Jetzt können weitere Routen und entsprechende Controller hinzugefügt werden. Z.B. einzelnes Item abrufen, Item erstellen, Item Updaten, Item löschen.
Doch da wie in diesem Beispiel auf eine Datenbankverbindung verzichtet haben, soll es vorerst genügen.
Der komplette Server sieht nun so aus:

use FastRoute\DataGenerator\GroupCountBased;
use FastRoute\RouteCollector;
use FastRoute\RouteParser\Std;
use Psr\Http\Message\ServerRequestInterface;
use React\Http\Response;
use React\Http\Server;

require __DIR__ . '/vendor/autoload.php';

$loop = \React\EventLoop\Factory::create();

$routes = new RouteCollector(new Std(), new GroupCountBased());
$routes->get('/items', new \App\Controller\ListItems());

$server = new Server(new \App\Router($routes));
$socket = new \React\Socket\Server('127.0.0.1:8082', $loop);
$server->listen($socket);

$server->on('error', function (Exception $exception) {
    echo $exception->getMessage() . PHP_EOL;
});

echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL;
$loop->run();

Jetzt starten wir den Server neu und feuern folgenden Request.

GET http://127.0.0.1:8082/items

Als Antwort erhalten wir unsere Liste der Items als Json.

HTTP/1.1 200 OK
Content-Type: application/json
X-Powered-By: React/alpha
Date: Tue, 09 Jul 2019 06:14:58 GMT
Content-Length: 251
Connection: close

[
  {
    "id": 0,
    "name": "item-0"
  },
  {
    "id": 1,
    "name": "item-1"
  },
  ...
  {
    "id": 9,
    "name": "item-9"
  }
]

Ich hoffe wie immer, diese erstbeste Anleitung war hilfreich.

 

Quellen:

Ähnliche Beiträge

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert